Skip to content

Commit ca9e67c

Browse files
committed
add unit tests with variants
1 parent 26f7b66 commit ca9e67c

File tree

3 files changed

+252
-108
lines changed

3 files changed

+252
-108
lines changed

codersdk/deployment.go

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,6 @@ const (
3333
EntitlementNotEntitled Entitlement = "not_entitled"
3434
)
3535

36-
func CompareEntitlements(a, b Entitlement) int {
37-
return entitlementWeight(a) - entitlementWeight(b)
38-
}
39-
4036
func entitlementWeight(e Entitlement) int {
4137
switch e {
4238
case EntitlementEntitled:
@@ -124,12 +120,12 @@ func (n FeatureName) AlwaysEnable() bool {
124120
}[n]
125121
}
126122

127-
// FeatureSet represents a grouping of features. This is easier
128-
// than manually assigning features al-la-carte when making a license.
129-
// These sets are dynamic in the sense a feature can be added to an existing
130-
// set to grant an additional feature to an existing license.
131-
// If features were granted al-la-carte, we would need to reissue the license
132-
// to include the new feature.
123+
// FeatureSet represents a grouping of features. Rather than manually
124+
// assigning features al-la-carte when making a license, a set can be specified.
125+
// Sets are dynamic in the sense a feature can be added to an existing
126+
// set, granting the feature to existing licenses.
127+
// If features were granted al-la-carte, we would need to reissue the existing
128+
// licenses to include the new feature.
133129
type FeatureSet string
134130

135131
const (
@@ -163,7 +159,7 @@ func (set FeatureSet) Features() []FeatureName {
163159
}
164160
case FeatureSetPremium:
165161
// FeatureSetPremium is a superset of Enterprise
166-
return append(FeatureSetEnterprise.Features())
162+
return append(FeatureSetEnterprise.Features(), FeatureMultipleOrganizations)
167163
}
168164
// By default, return an empty set.
169165
return []FeatureName{}
@@ -182,11 +178,11 @@ type Feature struct {
182178
// second feature. It is assumed the features are for the same FeatureName.
183179
//
184180
// A feature is considered greater than another feature if:
185-
// - Graceful & capable > Entitled & not capable
186-
// - The entitlement is greater
187-
// - The limit is greater
188-
// - Enabled is greater than disabled
189-
// - The actual is greater
181+
// 1. Graceful & capable > Entitled & not capable
182+
// 2. The entitlement is greater
183+
// 3. The limit is greater
184+
// 4. Enabled is greater than disabled
185+
// 5. The actual is greater
190186
func CompareFeatures(a, b Feature) int {
191187
if !a.Capable() || !b.Capable() {
192188
// If either is incapable, then it is possible a grace period
@@ -203,12 +199,10 @@ func CompareFeatures(a, b Feature) int {
203199
}
204200
}
205201

206-
entitlement := CompareEntitlements(a.Entitlement, b.Entitlement)
207-
if entitlement > 0 {
208-
return 1
209-
}
210-
if entitlement < 0 {
211-
return -1
202+
// Strict entitlement check. Higher is better
203+
entitlementDifference := entitlementWeight(a.Entitlement) - entitlementWeight(b.Entitlement)
204+
if entitlementDifference != 0 {
205+
return entitlementDifference
212206
}
213207

214208
// If the entitlement is the same, then we can compare the limits.

codersdk/deployment_test.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@ package codersdk_test
33
import (
44
"bytes"
55
"embed"
6+
"encoding/json"
67
"fmt"
78
"runtime"
89
"strings"
910
"testing"
1011
"time"
1112

13+
"github.com/stretchr/testify/assert"
1214
"github.com/stretchr/testify/require"
1315
"gopkg.in/yaml.v3"
1416

17+
"github.com/coder/coder/v2/coderd/util/ptr"
1518
"github.com/coder/coder/v2/codersdk"
1619
"github.com/coder/serpent"
1720
)
@@ -379,3 +382,236 @@ func TestExternalAuthYAMLConfig(t *testing.T) {
379382
output := strings.Replace(out.String(), "value:", "externalAuthProviders:", 1)
380383
require.Equal(t, inputYAML, output, "re-marshaled is the same as input")
381384
}
385+
386+
type featureVariants struct {
387+
original codersdk.Feature
388+
389+
variants []codersdk.Feature
390+
}
391+
392+
func variants(f codersdk.Feature) *featureVariants {
393+
return &featureVariants{original: f}
394+
}
395+
396+
func (f *featureVariants) Limits() *featureVariants {
397+
f.variant(func(v *codersdk.Feature) {
398+
if v.Limit == nil {
399+
v.Limit = ptr.Ref[int64](100)
400+
return
401+
}
402+
v.Limit = nil
403+
})
404+
return f
405+
}
406+
407+
func (f *featureVariants) Actual() *featureVariants {
408+
f.variant(func(v *codersdk.Feature) {
409+
if v.Actual == nil {
410+
v.Actual = ptr.Ref[int64](100)
411+
return
412+
}
413+
v.Actual = nil
414+
})
415+
return f
416+
}
417+
418+
func (f *featureVariants) Enabled() *featureVariants {
419+
f.variant(func(v *codersdk.Feature) {
420+
v.Enabled = !v.Enabled
421+
})
422+
return f
423+
}
424+
425+
func (f *featureVariants) variant(new func(f *codersdk.Feature)) {
426+
newVariants := make([]codersdk.Feature, 0, len(f.variants)*2)
427+
for _, v := range f.variants {
428+
cpy := v
429+
new(&cpy)
430+
newVariants = append(newVariants, v, cpy)
431+
}
432+
}
433+
434+
func (f *featureVariants) Features() []codersdk.Feature {
435+
return append([]codersdk.Feature{f.original}, f.variants...)
436+
}
437+
438+
func TestFeatureComparison(t *testing.T) {
439+
t.Parallel()
440+
441+
strictEntitlement := func(v codersdk.Feature) []codersdk.Feature {
442+
// Entitlement checks should ignore limits, actuals, and enables
443+
return variants(v).Limits().Actual().Enabled().Features()
444+
}
445+
446+
testCases := []struct {
447+
Name string
448+
A codersdk.Feature
449+
B codersdk.Feature
450+
Expected int
451+
// To assert variants do not affect the end result, a function can be
452+
// used to generate additional variants of each feature to check.
453+
Variants func(v codersdk.Feature) []codersdk.Feature
454+
}{
455+
{
456+
Name: "Empty",
457+
Expected: 0,
458+
},
459+
// Entitlement check
460+
// Entitled
461+
{
462+
Name: "EntitledVsGracePeriod",
463+
A: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled},
464+
B: codersdk.Feature{Entitlement: codersdk.EntitlementGracePeriod},
465+
Expected: 1,
466+
Variants: strictEntitlement,
467+
},
468+
{
469+
Name: "EntitledVsNotEntitled",
470+
A: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled},
471+
B: codersdk.Feature{Entitlement: codersdk.EntitlementNotEntitled},
472+
Expected: 3,
473+
Variants: strictEntitlement,
474+
},
475+
{
476+
Name: "EntitledVsUnknown",
477+
A: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled},
478+
B: codersdk.Feature{Entitlement: ""},
479+
Expected: 4,
480+
Variants: strictEntitlement,
481+
},
482+
// GracePeriod
483+
{
484+
Name: "GracefulVsNotEntitled",
485+
A: codersdk.Feature{Entitlement: codersdk.EntitlementGracePeriod},
486+
B: codersdk.Feature{Entitlement: codersdk.EntitlementNotEntitled},
487+
Expected: 2,
488+
Variants: strictEntitlement,
489+
},
490+
{
491+
Name: "GracefulVsUnknown",
492+
A: codersdk.Feature{Entitlement: codersdk.EntitlementGracePeriod},
493+
B: codersdk.Feature{Entitlement: ""},
494+
Expected: 3,
495+
Variants: strictEntitlement,
496+
},
497+
// NotEntitled
498+
{
499+
Name: "NotEntitledVsUnknown",
500+
A: codersdk.Feature{Entitlement: codersdk.EntitlementNotEntitled},
501+
B: codersdk.Feature{Entitlement: ""},
502+
Expected: 1,
503+
Variants: strictEntitlement,
504+
},
505+
// --
506+
{
507+
Name: "EntitledVsGracePeriodCapable",
508+
A: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled, Limit: ptr.Ref[int64](100), Actual: ptr.Ref[int64](200)},
509+
B: codersdk.Feature{Entitlement: codersdk.EntitlementGracePeriod, Limit: ptr.Ref[int64](300), Actual: ptr.Ref[int64](200)},
510+
Expected: -1,
511+
},
512+
// UserLimits
513+
{
514+
// Tests an exceeded limit that is entitled vs a graceful limit that
515+
// is not exceeded. This is the edge case that we should use the graceful period
516+
// instead of the entitled.
517+
Name: "UserLimitExceeded",
518+
A: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled, Limit: ptr.Ref(int64(100)), Actual: ptr.Ref(int64(200))},
519+
B: codersdk.Feature{Entitlement: codersdk.EntitlementGracePeriod, Limit: ptr.Ref(int64(300)), Actual: ptr.Ref(int64(200))},
520+
Expected: -1,
521+
},
522+
{
523+
Name: "UserLimitExceededNoEntitled",
524+
A: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled, Limit: ptr.Ref(int64(100)), Actual: ptr.Ref(int64(200))},
525+
B: codersdk.Feature{Entitlement: codersdk.EntitlementNotEntitled, Limit: ptr.Ref(int64(300)), Actual: ptr.Ref(int64(200))},
526+
Expected: 3,
527+
},
528+
{
529+
Name: "HigherLimit",
530+
A: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled, Limit: ptr.Ref(int64(110)), Actual: ptr.Ref(int64(200))},
531+
B: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled, Limit: ptr.Ref(int64(100)), Actual: ptr.Ref(int64(200))},
532+
Expected: 10, // Diff in the limit #
533+
},
534+
{
535+
Name: "HigherActual",
536+
A: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled, Limit: ptr.Ref(int64(100)), Actual: ptr.Ref(int64(300))},
537+
B: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled, Limit: ptr.Ref(int64(100)), Actual: ptr.Ref(int64(200))},
538+
Expected: 100, // Diff in the actual #
539+
},
540+
{
541+
Name: "LimitExists",
542+
A: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled, Limit: ptr.Ref(int64(100)), Actual: ptr.Ref(int64(50))},
543+
B: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled, Limit: nil, Actual: ptr.Ref(int64(200))},
544+
Expected: 1,
545+
},
546+
{
547+
Name: "LimitExistsGrace",
548+
A: codersdk.Feature{Entitlement: codersdk.EntitlementGracePeriod, Limit: ptr.Ref(int64(100)), Actual: ptr.Ref(int64(50))},
549+
B: codersdk.Feature{Entitlement: codersdk.EntitlementGracePeriod, Limit: nil, Actual: ptr.Ref(int64(200))},
550+
Expected: 1,
551+
},
552+
{
553+
Name: "ActualExists",
554+
A: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled, Limit: ptr.Ref(int64(100)), Actual: ptr.Ref(int64(50))},
555+
B: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled, Limit: ptr.Ref(int64(100)), Actual: nil},
556+
Expected: 1,
557+
},
558+
{
559+
Name: "NotNils",
560+
A: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled, Limit: ptr.Ref(int64(100)), Actual: ptr.Ref(int64(50))},
561+
B: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled, Limit: nil, Actual: nil},
562+
Expected: 1,
563+
},
564+
{
565+
// This is super strange, but it is possible to have a limit but no actual.
566+
// Just adding this test case to solidify the behavior.
567+
// Feel free to change this if you think it should be different.
568+
Name: "LimitVsActual",
569+
A: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled, Limit: ptr.Ref(int64(100)), Actual: nil},
570+
B: codersdk.Feature{Entitlement: codersdk.EntitlementEntitled, Limit: nil, Actual: ptr.Ref(int64(200))},
571+
Expected: 1,
572+
},
573+
}
574+
575+
for _, tc := range testCases {
576+
tc := tc
577+
578+
t.Run(tc.Name, func(t *testing.T) {
579+
t.Parallel()
580+
581+
if tc.Variants == nil {
582+
tc.Variants = func(v codersdk.Feature) []codersdk.Feature {
583+
return []codersdk.Feature{v}
584+
}
585+
}
586+
587+
VariantLoop:
588+
for i, a := range tc.Variants(tc.A) {
589+
for j, b := range tc.Variants(tc.B) {
590+
r := codersdk.CompareFeatures(a, b)
591+
logIt := !assert.Equalf(t, tc.Expected, r, "variant %d vs %d", i, j)
592+
593+
// Comparisons should be like addition. A - B = -1 * (B - A)
594+
r = codersdk.CompareFeatures(tc.B, tc.A)
595+
logIt = logIt || !assert.Equalf(t, tc.Expected*-1, r, "the inverse comparison should also be true, variant %d vs %d", j, i)
596+
if logIt {
597+
ad, _ := json.Marshal(a)
598+
bd, _ := json.Marshal(b)
599+
t.Logf("variant %d vs %d\ni = %s\nj = %s", i, j, ad, bd)
600+
// Do not iterate into more variants if the test fails.
601+
break VariantLoop
602+
}
603+
}
604+
}
605+
})
606+
}
607+
}
608+
609+
// TestPremiumSuperSet tests that the "premium" feature set is a superset of the
610+
// "enterprise" feature set.
611+
func TestPremiumSuperSet(t *testing.T) {
612+
t.Parallel()
613+
614+
enterprise := codersdk.FeatureSetEnterprise
615+
premium := codersdk.FeatureSetPremium
616+
require.Subset(t, premium.Features(), enterprise.Features(), "premium should be a superset of enterprise. If this fails, update the premium feature set to include all enterprise features.")
617+
}

0 commit comments

Comments
 (0)