package test import ( "RenewCMS/internal/domain/role" "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/useCases" "errors" "testing" ) func missingKeyOf(t *testing.T, err error) string { t.Helper() var missing role.MissingPermissionError if !errors.As(err, &missing) { t.Fatalf("expected a MissingPermissionError, got %v", err) } return missing.Key } func TestCheckPermissionUseCase(t *testing.T) { newCheck := func() (*useCases.CheckPermissionUseCase, *mockRoleRepo) { roles := newSeededMockRoleRepo() return useCases.NewCheckPermissionUseCase(roles), roles } owner := user.User{ID: 3, RoleID: ownerRoleID} admin := user.User{ID: 5, RoleID: adminRoleID} writer := user.User{ID: 9, RoleID: writerRoleID} t.Run("Require passes for held keys and refuses the others", func(t *testing.T) { check, _ := newCheck() cases := []struct { name string account user.User key string allowed bool }{ {"owner holds an owner-only key", owner, "system.apply_updates", true}, {"admin lacks an owner-only key", admin, "system.apply_updates", false}, {"admin holds users.invite", admin, "users.invite", true}, {"writer lacks users.invite", writer, "users.invite", false}, {"writer holds schema.view", writer, "schema.view", true}, {"writer holds a scoped key at own", writer, "activity.view", true}, {"an unknown key is refused", owner, "no.such_key", false}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { _, err := check.Require(c.account, c.key) if c.allowed && err != nil { t.Fatalf("expected pass, got %v", err) } if !c.allowed { if !errors.Is(err, role.ErrMissingPermission) || missingKeyOf(t, err) != c.key { t.Fatalf("expected missing permission for %s, got %v", c.key, err) } } }) } }) t.Run("RequireAll refuses a scoped key held only at own", func(t *testing.T) { check, _ := newCheck() if _, err := check.RequireAll(writer, "activity.view"); !errors.Is(err, role.ErrMissingPermission) { t.Fatalf("expected refusal, got %v", err) } if _, err := check.RequireAll(admin, "activity.view"); err != nil { t.Fatalf("expected admin to pass, got %v", err) } }) t.Run("Require returns the resolved grants", func(t *testing.T) { check, _ := newCheck() grants, err := check.Require(writer, "authors.view") if err != nil { t.Fatal(err) } if grants.ScopeFor("authors.edit", "") != role.ScopeOwn || grants.ScopeFor("users.view", "") != role.ScopeNone { t.Errorf("unexpected grants %v", grants) } }) t.Run("A custom role is judged by its grants only, owner-only keys excluded", func(t *testing.T) { check, roles := newCheck() custom, _ := roles.Create(role.Role{Name: "Hand made", Version: 1, Grants: []role.Grant{{Key: "system.apply_updates", Scope: role.ScopeAll}}}) account := user.User{ID: 20, RoleID: custom.ID} if _, err := check.Require(account, "system.apply_updates"); !errors.Is(err, role.ErrMissingPermission) { t.Errorf("expected the hand-placed owner-only key to be ignored on an unlocked role, got %v", err) } if _, err := check.Require(account, "schema.view"); !errors.Is(err, role.ErrMissingPermission) { t.Errorf("expected no implied keys, got %v", err) } }) t.Run("An unresolvable role is refused as a missing permission", func(t *testing.T) { check, _ := newCheck() for name, account := range map[string]user.User{ "unknown role_id": {ID: 13, RoleID: 99}, "empty legacy and no id": {ID: 14, Role: ""}, "editor legacy and no id": {ID: 15, Role: "editor"}, "legacy owner string ignored when role_id names Writer": {ID: 16, Role: user.RoleOwner, RoleID: writerRoleID}, } { _, err := check.Require(account, "users.invite") if !errors.Is(err, role.ErrMissingPermission) || missingKeyOf(t, err) != "users.invite" { t.Errorf("%s: got %v", name, err) } } }) t.Run("The legacy string resolves only owner and admin when there is no role_id", func(t *testing.T) { check, _ := newCheck() if _, err := check.Require(user.User{ID: 17, Role: user.RoleOwner}, "system.apply_updates"); err != nil { t.Errorf("legacy owner: %v", err) } if _, err := check.Require(user.User{ID: 18, Role: user.RoleAdmin}, "system.apply_updates"); !errors.Is(err, role.ErrMissingPermission) { t.Errorf("legacy admin should lack owner-only keys, got %v", err) } }) t.Run("A repository failure is not a missing permission", func(t *testing.T) { check, roles := newCheck() roles.fail.failNext("Get") _, err := check.Require(owner, "users.view") if err == nil || errors.Is(err, role.ErrMissingPermission) { t.Fatalf("expected a plain failure, got %v", err) } }) t.Run("Grant changes apply to the next call with no cache", func(t *testing.T) { check, roles := newCheck() if _, err := check.Require(writer, "settings.view"); !errors.Is(err, role.ErrMissingPermission) { t.Fatalf("expected refusal first, got %v", err) } stored := roles.roles[writerRoleID] stored.Grants = append(stored.Grants, role.Grant{Key: "settings.view", Scope: role.ScopeAll}) roles.roles[writerRoleID] = stored if _, err := check.Require(writer, "settings.view"); err != nil { t.Fatalf("expected pass after the edit, got %v", err) } }) } func TestGrantSet(t *testing.T) { t.Run("an absent key is none and a nil set holds nothing", func(t *testing.T) { var empty role.GrantSet if empty.ScopeFor("users.view", "") != role.ScopeNone || empty.Holds("users.view") || empty.HoldsAll("users.view") { t.Error("a nil set must hold nothing") } }) t.Run("Holds is true for own and all, HoldsAll only for all", func(t *testing.T) { set := unlockedGrantSet("content.update=own", "content.delete=all", "content.read=none") if !set.Holds("content.update") || set.HoldsAll("content.update") || !set.Holds("content.delete") || !set.HoldsAll("content.delete") || set.Holds("content.read") { t.Errorf("unexpected %v", set) } }) t.Run("MissingPermissionError matches the sentinel and carries the key", func(t *testing.T) { err := error(role.MissingPermissionError{Key: "users.invite"}) if !errors.Is(err, role.ErrMissingPermission) || missingKeyOf(t, err) != "users.invite" { t.Errorf("unexpected %v", err) } }) } var ( activityViewAllGrants = unlockedGrantSet(role.KeyActivityView + "=all") activityViewOwnGrants = unlockedGrantSet(role.KeyActivityView + "=own") authorsEditOwnGrants = unlockedGrantSet(role.KeyAuthorsEdit + "=own") authorsEditAllGrants = unlockedGrantSet(role.KeyAuthorsEdit + "=all") )