package billing import ( "errors" "reflect" "testing" ) func TestIsGrantableFeatureKey(t *testing.T) { grantable := []string{ "stores.hub", "settings.api_keys", "catalog.products", "catalog.products.tab_error", "marketing.campaigns.send", } for _, key := range grantable { if !IsGrantableFeatureKey(key) { t.Errorf("expected %q to be grantable", key) } } notGrantable := []string{ "shell.navigation", // app chrome "capability.ai_credits", // plan metering, not UI access "dashboard.overview", // always-on landing page "settings.profile", // always-on: own password / sign out "catalog.not_a_real_key", // unknown "", } for _, key := range notGrantable { if IsGrantableFeatureKey(key) { t.Errorf("expected %q to NOT be grantable", key) } } } func TestSanitizeMemberPermissions(t *testing.T) { t.Run("keeps only denials of grantable keys", func(t *testing.T) { got, err := SanitizeMemberPermissions(map[string]bool{ "stores.hub": false, "settings.api_keys": false, "catalog.products": true, // allow is the default — never stored }) if err != nil { t.Fatalf("unexpected error: %v", err) } want := map[string]bool{"stores.hub": false, "settings.api_keys": false} if !reflect.DeepEqual(got, want) { t.Fatalf("got %v want %v", got, want) } }) t.Run("rejects protected keys instead of silently dropping them", func(t *testing.T) { if _, err := SanitizeMemberPermissions(map[string]bool{"shell.navigation": false}); !errors.Is(err, ErrFeatureNotGrantable) { t.Fatalf("expected ErrFeatureNotGrantable, got %v", err) } if _, err := SanitizeMemberPermissions(map[string]bool{"dashboard.overview": false}); !errors.Is(err, ErrFeatureNotGrantable) { t.Fatalf("expected ErrFeatureNotGrantable for always-on key, got %v", err) } }) t.Run("rejects unknown keys", func(t *testing.T) { if _, err := SanitizeMemberPermissions(map[string]bool{"nope.nope": false}); !errors.Is(err, ErrFeatureNotGrantable) { t.Fatalf("expected ErrFeatureNotGrantable, got %v", err) } if _, err := SanitizeMemberPermissions(map[string]bool{"nope.nope": true}); !errors.Is(err, ErrUnknownFeatureKey) { t.Fatalf("expected ErrUnknownFeatureKey, got %v", err) } }) } func TestMemberDeniesFeature(t *testing.T) { perms := map[string]bool{"stores.hub": false, "catalog.products": false} if !MemberDeniesFeature(perms, "stores.hub") { t.Error("direct denial should apply") } if !MemberDeniesFeature(perms, "catalog.products.tab_error") { t.Error("denying a parent must deny its descendants") } if MemberDeniesFeature(perms, "catalog.categories") { t.Error("sibling keys must stay allowed") } if MemberDeniesFeature(nil, "stores.hub") { t.Error("an empty overlay denies nothing") } // Protected keys survive even a hand-edited overlay that names them. hostile := map[string]bool{"shell.navigation": false, "dashboard.overview": false, "capability.ai_credits": false} for key := range hostile { if MemberDeniesFeature(hostile, key) { t.Errorf("%q must never be deniable", key) } } } func TestApplyMemberPermissions(t *testing.T) { features := map[string]bool{ "stores.hub": true, "catalog.products": true, "catalog.products.tab_error": true, "marketing.campaigns": false, // already off by plan "dashboard.overview": true, } perms := map[string]bool{"stores.hub": false, "catalog.products": false, "marketing.campaigns": false} got, denied := ApplyMemberPermissions(features, perms) for _, key := range []string{"stores.hub", "catalog.products", "catalog.products.tab_error"} { if got[key] { t.Errorf("expected %q to be denied", key) } } if !got["dashboard.overview"] { t.Error("always-on keys must survive the overlay") } // Already-off plan keys are not reported as member denials — the member did not lose them. want := []string{"catalog.products", "catalog.products.tab_error", "stores.hub"} if !reflect.DeepEqual(denied, want) { t.Fatalf("denied = %v, want %v", denied, want) } // The input map must not be mutated — it is shared with the company-level cache. if !features["stores.hub"] { t.Error("ApplyMemberPermissions mutated its input") } } func TestApplyMemberPermissionsIsNoopWithoutOverlay(t *testing.T) { features := map[string]bool{"stores.hub": true} got, denied := ApplyMemberPermissions(features, nil) if len(denied) != 0 { t.Fatalf("expected no denials, got %v", denied) } if !reflect.DeepEqual(got, features) { t.Fatalf("expected the map unchanged, got %v", got) } } func TestGrantableFeatureKeysExcludeProtectedPrefixes(t *testing.T) { keys := GrantableFeatureKeys() if len(keys) == 0 { t.Fatal("expected a non-empty grantable catalog") } for _, key := range keys { if !IsGrantableFeatureKey(key) { t.Errorf("GrantableFeatureKeys returned non-grantable %q", key) } } } func TestGrantableParent(t *testing.T) { if got := grantableParent("catalog.products.tab_error"); got != "catalog.products" { t.Errorf("got %q, want catalog.products", got) } // dashboard.overview is always-on, so its children have no grantable parent. if got := grantableParent("dashboard.stats"); got != "" { t.Errorf("got %q, want empty", got) } if got := grantableParent("stores.hub"); got != "" { t.Errorf("got %q, want empty", got) } } func TestDecodeMemberPermissionsIsDefensive(t *testing.T) { // Garbage must not brick the dashboard — it resolves to "unrestricted". if got, _ := decodeMemberPermissions([]byte("not json")); got != nil { t.Errorf("expected nil for invalid JSON, got %v", got) } if got, _ := decodeMemberPermissions([]byte(`{}`)); got != nil { t.Errorf("expected nil for an empty overlay, got %v", got) } // Stale / protected keys are dropped at read time too. got, err := decodeMemberPermissions([]byte(`{"stores.hub":false,"shell.navigation":false,"gone.key":false,"feeds.list":true}`)) if err != nil { t.Fatalf("unexpected error: %v", err) } want := map[string]bool{"stores.hub": false} if !reflect.DeepEqual(got, want) { t.Fatalf("got %v, want %v", got, want) } }