package test import ( "RenewCMS/internal/domain/audit" "RenewCMS/internal/domain/role" "errors" "reflect" "testing" ) func escalationKeyOf(t *testing.T, err error) string { t.Helper() var escalation role.EscalationError if !errors.As(err, &escalation) || !errors.Is(err, role.ErrEscalation) { t.Fatalf("expected an EscalationError, got %v", err) } return escalation.Key } func TestGrantSetCovers(t *testing.T) { typeKeys := []string{"article", "events", "venues"} builtIns := role.BuiltInRoles() ownerSet, adminSet, writerSet := builtIns[0].GrantSet(), builtIns[1].GrantSet(), builtIns[2].GrantSet() cases := []struct { name string caller role.GrantSet candidate role.GrantSet wantKey string }{ {"owner covers admin", ownerSet, adminSet, ""}, {"owner covers writer", ownerSet, writerSet, ""}, {"admin does not cover owner and names the first owner-only key", adminSet, ownerSet, role.KeySettingsDeleteSite}, {"admin covers writer", adminSet, writerSet, ""}, {"writer does not cover admin", writerSet, adminSet, role.KeyContentDelete}, {"own covers own", unlockedGrantSet("content.publish=own"), unlockedGrantSet("content.publish=own"), ""}, {"own does not cover all", unlockedGrantSet("content.publish=own"), unlockedGrantSet("content.publish=all"), role.KeyContentPublish}, {"none does not cover own", unlockedGrantSet("content.read=all"), unlockedGrantSet("content.delete=own"), role.KeyContentDelete}, {"denying never escalates", unlockedGrantSet("content.publish=own"), unlockedGrantSet("content.publish:events=none"), ""}, {"typed all above wildcard own", unlockedGrantSet("content.publish=own"), unlockedGrantSet("content.publish:events=all"), "content.publish:events"}, {"typed own under wildcard own", unlockedGrantSet("content.update=own"), unlockedGrantSet("content.update:events=own"), ""}, {"wildcard all would raise article", unlockedGrantSet("content.update=all", "content.update:article=none"), unlockedGrantSet("content.update=all"), "content.update:article"}, {"typed-only reader does not cover wildcard read", unlockedGrantSet("content.read:events=all"), unlockedGrantSet("content.read=all"), role.KeyContentRead}, {"toggle missing", unlockedGrantSet("content.read=all"), unlockedGrantSet("schema.edit=all"), role.KeySchemaEdit}, {"first violation follows the catalogue order", unlockedGrantSet(), unlockedGrantSet("content.delete=all", "content.read=all"), role.KeyContentRead}, {"wildcard is checked before its typed keys", unlockedGrantSet(), unlockedGrantSet("content.read:events=all", "content.read=all"), role.KeyContentRead}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { err := c.caller.Covers(c.candidate, typeKeys) if c.wantKey == "" { if err != nil { t.Fatalf("expected the grants to be covered, got %v", err) } return } if got := escalationKeyOf(t, err); got != c.wantKey { t.Fatalf("expected the key %q, got %q", c.wantKey, got) } }) } t.Run("a set always covers itself", func(t *testing.T) { for _, set := range []role.GrantSet{ownerSet, adminSet, writerSet, unlockedGrantSet(), unlockedGrantSet("content.update=all", "content.update:article=none", "content.delete:events=none")} { if err := set.Covers(set, typeKeys); err != nil { t.Fatalf("expected a set to cover itself, got %v", err) } } }) } func TestGrantSetEffectiveGrants(t *testing.T) { t.Run("the events editor lists typed entries after the wildcard in catalogue then type order", func(t *testing.T) { got := role.Role{Grants: grantRows("content.delete:events=none", "content.update:venues=all", "content.read=all", "content.update:events=own")}.EffectiveGrants() want := grantRows("content.read=all", "content.update:events=own", "content.update:venues=all", "content.delete:events=none") if !reflect.DeepEqual(got, want) { t.Fatalf("expected %v, got %v", want, got) } }) t.Run("duplicates resolve to the most restrictive scope and unknown keys are dropped", func(t *testing.T) { got := role.Role{Grants: grantRows("content.update=all", "content.update=own", "content.explode=all", "content.update:Bad Key=all")}.EffectiveGrants() want := grantRows("content.update=own") if !reflect.DeepEqual(got, want) { t.Fatalf("expected %v, got %v", want, got) } }) t.Run("owner-only keys are dropped on an unlocked role and kept on a locked one", func(t *testing.T) { grants := grantRows("system.apply_updates=all", "content.read=all") unlocked := role.Role{Grants: grants}.EffectiveGrants() if !reflect.DeepEqual(unlocked, grantRows("content.read=all")) { t.Fatalf("unexpected unlocked grants %v", unlocked) } locked := role.Role{Locked: true, Grants: grants}.EffectiveGrants() if !reflect.DeepEqual(locked, grantRows("content.read=all", "system.apply_updates=all")) { t.Fatalf("unexpected locked grants %v", locked) } }) t.Run("a role with nothing granted lists an empty slice, never nil", func(t *testing.T) { if got := (role.Role{}).EffectiveGrants(); got == nil || len(got) != 0 { t.Fatalf("expected an empty non-nil slice, got %#v", got) } }) t.Run("wildcard entries without effect are not listed", func(t *testing.T) { if got := (role.Role{Grants: grantRows("content.create=own", "content.read=none")}).EffectiveGrants(); len(got) != 0 { t.Fatalf("expected nothing, got %v", got) } }) t.Run("the effective list is valid as the grants of a write", func(t *testing.T) { listed := role.Role{Grants: grantRows("content.update=own", "content.update=all", "content.delete:events=none", "media.view=all")}.EffectiveGrants() if err := role.ValidateGrants(false, listed); err != nil { t.Fatalf("expected the list to validate, got %v", err) } }) } func TestDiffGrants(t *testing.T) { t.Run("the example edit gives added, removed and changed", func(t *testing.T) { before := grantRows("content.read=all", "content.update=own", "media.view=all") after := grantRows("content.read=all", "content.update=all", "content.delete=own") diff := role.DiffGrants(before, after) if !reflect.DeepEqual(diff.Added, grantRows("content.delete=own")) { t.Errorf("unexpected added %v", diff.Added) } if !reflect.DeepEqual(diff.Removed, grantRows("media.view=all")) { t.Errorf("unexpected removed %v", diff.Removed) } if !reflect.DeepEqual(diff.Changed, []role.GrantChange{{Key: "content.update", From: role.ScopeOwn, To: role.ScopeAll}}) { t.Errorf("unexpected changed %v", diff.Changed) } }) t.Run("an absent typed entry against a typed none is an addition", func(t *testing.T) { diff := role.DiffGrants(nil, grantRows("content.delete:events=none")) if len(diff.Added) != 1 || len(diff.Removed) != 0 || len(diff.Changed) != 0 { t.Fatalf("unexpected diff %+v", diff) } }) t.Run("an unchanged list gives empty non-nil slices", func(t *testing.T) { diff := role.DiffGrants(grantRows("content.read=all"), grantRows("content.read=all")) if diff.Added == nil || diff.Removed == nil || diff.Changed == nil || !diff.Empty() { t.Fatalf("expected empty non-nil slices, got %+v", diff) } }) } func TestRoleWriteRules(t *testing.T) { t.Run("the new sentinels are distinct and match with errors.Is", func(t *testing.T) { sentinels := []error{role.ErrRoleHasMembers, role.ErrRoleNameTaken, role.ErrRoleLocked, role.ErrBuiltInRole, role.ErrBuiltInName, role.ErrOwnRole, role.ErrInvalidRoleName, role.ErrUnknownContentType, role.ErrNotBuiltIn, role.ErrInvalidReassign, role.ErrEscalation} for index, sentinel := range sentinels { for other, candidate := range sentinels { if (index == other) != errors.Is(sentinel, candidate) { t.Fatalf("sentinels %d and %d are not distinct", index, other) } } } }) t.Run("a name is trimmed and a blank one is refused", func(t *testing.T) { name, err := role.NormalizeName(" Reviewer ") if err != nil || name != "Reviewer" { t.Fatalf("expected Reviewer, got %q %v", name, err) } if _, err := role.NormalizeName(" "); !errors.Is(err, role.ErrInvalidRoleName) { t.Fatalf("expected ErrInvalidRoleName, got %v", err) } }) t.Run("names collide case-insensitively", func(t *testing.T) { if !role.SameName(" contributor ", "Contributor") || role.SameName("Intern", "Contributor") { t.Fatal("unexpected name comparison") } }) t.Run("typed keys must belong to an allowed type", func(t *testing.T) { allowed := []string{"article", "events"} if err := role.ValidateGrantTypes(grantRows("content.update:events=all", "content.read=all"), allowed); err != nil { t.Fatalf("expected valid types, got %v", err) } for _, key := range []string{"content.update:nope=all", "content.update:author=all"} { if err := role.ValidateGrantTypes(grantRows(key), allowed); !errors.Is(err, role.ErrUnknownContentType) { t.Fatalf("%s: expected ErrUnknownContentType, got %v", key, err) } } }) t.Run("the typed keys of a content type are the five per-type permissions", func(t *testing.T) { want := []string{"content.read:events", "content.create:events", "content.update:events", "content.delete:events", "content.publish:events"} if got := role.TypedKeys("events"); !reflect.DeepEqual(got, want) { t.Fatalf("expected %v, got %v", want, got) } }) t.Run("the repository no longer lists grants on their own", func(t *testing.T) { if _, present := reflect.TypeOf((*role.Repository)(nil)).Elem().MethodByName("ListGrants"); present { t.Fatal("role.Repository must not have ListGrants") } for _, method := range []string{"List", "Get", "GetByName", "Create", "Save", "Delete", "ReassignMembers", "RemoveTypedGrants"} { if _, present := reflect.TypeOf((*role.Repository)(nil)).Elem().MethodByName(method); !present { t.Fatalf("role.Repository lacks %s", method) } } }) } func TestRoleActivityVocabulary(t *testing.T) { t.Run("the roles category, target and actions exist", func(t *testing.T) { if audit.CategoryRoles != "roles" || audit.TargetRole != "role" { t.Fatalf("unexpected category %q or target %q", audit.CategoryRoles, audit.TargetRole) } actions := []string{audit.ActionRoleCreate, audit.ActionRoleEdit, audit.ActionRoleDelete, audit.ActionRoleReassign} if !reflect.DeepEqual(actions, []string{"role.create", "role.edit", "role.delete", "role.reassign"}) { t.Fatalf("unexpected actions %v", actions) } }) t.Run("an event in the roles category is valid and the filter accepts it", func(t *testing.T) { event := audit.NewEvent(audit.ActionRoleCreate, audit.CategoryRoles) event.Source = audit.SourceWeb event.Actor = audit.AnonymousActor("x") if err := event.Validate(); err != nil { t.Fatalf("expected a valid event, got %v", err) } if category, err := audit.ParseCategory("roles"); err != nil || category != audit.CategoryRoles { t.Fatalf("expected the roles filter, got %q %v", category, err) } }) }