@@ -3,15 +3,18 @@ package codersdk_test
3
3
import (
4
4
"bytes"
5
5
"embed"
6
+ "encoding/json"
6
7
"fmt"
7
8
"runtime"
8
9
"strings"
9
10
"testing"
10
11
"time"
11
12
13
+ "github.com/stretchr/testify/assert"
12
14
"github.com/stretchr/testify/require"
13
15
"gopkg.in/yaml.v3"
14
16
17
+ "github.com/coder/coder/v2/coderd/util/ptr"
15
18
"github.com/coder/coder/v2/codersdk"
16
19
"github.com/coder/serpent"
17
20
)
@@ -379,3 +382,236 @@ func TestExternalAuthYAMLConfig(t *testing.T) {
379
382
output := strings .Replace (out .String (), "value:" , "externalAuthProviders:" , 1 )
380
383
require .Equal (t , inputYAML , output , "re-marshaled is the same as input" )
381
384
}
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\n i = %s\n j = %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