Files

182 lines
6.0 KiB
Go
Raw Permalink Normal View History

2026-08-22 18:51:17 +02:00
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)
}
}