refac: debt on role backend

This commit is contained in:
Florian Sylvain committed 2026-10-26 23:34:35 +01:00
1 parent de142f6c5f
commit a043f7f41a
130 files changed
+3591 -512

No files matched your search

+295
View File
@@ -0,0 +1,295 @@
package test
import (
"RenewCMS/internal/domain/role"
"errors"
"go/ast"
"go/parser"
"go/token"
"path/filepath"
"reflect"
"slices"
"strconv"
"strings"
"testing"
)
func TestGrantSetScopeFor(t *testing.T) {
longKey := strings.Repeat("a", 101)
cases := []struct {
grants []string
key string
typeKey string
want role.Scope
}{
{[]string{"content.update=all"}, "content.update", "", role.ScopeAll},
{[]string{"content.update=all"}, "content.update", "blog", role.ScopeAll},
{[]string{"content.update=all", "content.update:blog=own"}, "content.update", "blog", role.ScopeOwn},
{[]string{"content.update=all", "content.update:blog=own"}, "content.update", "news", role.ScopeAll},
{[]string{"content.update=all", "content.update:blog=none"}, "content.update", "blog", role.ScopeNone},
{[]string{"content.update=own", "content.update:blog=all"}, "content.update", "blog", role.ScopeAll},
{[]string{"content.update:blog=all"}, "content.update", "blog", role.ScopeAll},
{[]string{"content.update:blog=all"}, "content.update", "news", role.ScopeNone},
{[]string{"content.update:blog=all"}, "content.update", "", role.ScopeNone},
{[]string{"content.create=all", "content.create:blog=none"}, "content.create", "blog", role.ScopeNone},
{[]string{"content.create=all", "content.create:blog=all"}, "content.create", "blog", role.ScopeAll},
{[]string{"content.read=all"}, "content.read", "Bad Key", role.ScopeNone},
{[]string{"content.read=all"}, "content.read", longKey, role.ScopeNone},
{[]string{"schema.view=all"}, "schema.view", "blog", role.ScopeNone},
{[]string{"schema.view=all"}, "schema.view", "", role.ScopeAll},
{[]string{"content.explode=all"}, "content.explode", "", role.ScopeNone},
{nil, "content.read", "", role.ScopeNone},
}
for _, c := range cases {
name := strings.Join(c.grants, ", ") + " | " + c.key + " | " + c.typeKey
if len(name) > 90 {
name = name[:90]
}
t.Run(name, func(t *testing.T) {
if got := unlockedGrantSet(c.grants...).ScopeFor(c.key, c.typeKey); got != c.want {
t.Errorf("expected %s, got %s", c.want, got)
}
})
}
t.Run("a zero GrantSet resolves everything to none", func(t *testing.T) {
var empty role.GrantSet
if empty.ScopeFor("content.read", "") != role.ScopeNone || empty.ScopeFor("content.read", "blog") != role.ScopeNone || empty.Holds("content.read") || empty.HoldsAll("content.read") || empty.HoldsFor("content.read", "blog") {
t.Error("a zero set must hold nothing")
}
})
t.Run("HoldsFor is true for own and all of the typed scope only", func(t *testing.T) {
set := unlockedGrantSet("content.update=all", "content.update:blog=own", "content.update:news=none")
if !set.HoldsFor("content.update", "blog") || !set.HoldsFor("content.update", "other") || set.HoldsFor("content.update", "news") {
t.Errorf("unexpected HoldsFor results")
}
})
}
func TestGrantSetPermits(t *testing.T) {
cases := []struct {
scope role.Scope
caller uint32
createdBy uint32
want bool
}{
{role.ScopeAll, 9, 21, true},
{role.ScopeAll, 9, 0, true},
{role.ScopeOwn, 21, 21, true},
{role.ScopeOwn, 9, 21, false},
{role.ScopeOwn, 21, 0, false},
{role.ScopeOwn, 0, 0, false},
{role.ScopeNone, 21, 21, false},
}
for _, c := range cases {
name := string(c.scope) + " caller " + strconv.Itoa(int(c.caller)) + " created_by " + strconv.Itoa(int(c.createdBy))
t.Run(name, func(t *testing.T) {
set := unlockedGrantSet("content.update:blog=" + string(c.scope))
if got := set.Permits("content.update", "blog", c.caller, c.createdBy); got != c.want {
t.Errorf("expected %v, got %v", c.want, got)
}
})
}
t.Run("a typed scope replaces the wildcard for Permits", func(t *testing.T) {
set := unlockedGrantSet("content.update=all", "content.update:blog=own")
if set.Permits("content.update", "blog", 9, 21) || !set.Permits("content.update", "news", 9, 21) {
t.Errorf("unexpected Permits results")
}
})
}
func TestRoleGrantSetNormalisation(t *testing.T) {
duplicates := []struct {
rows []string
key string
typeKey string
want role.Scope
}{
{[]string{"content.update=own", "content.update=all"}, "content.update", "", role.ScopeOwn},
{[]string{"content.update=all", "content.update=own"}, "content.update", "", role.ScopeOwn},
{[]string{"content.update:blog=all", "content.update:blog=own"}, "content.update", "blog", role.ScopeOwn},
{[]string{"content.update=all", "content.update:blog=own"}, "content.update", "blog", role.ScopeOwn},
{[]string{"content.read=all", "content.read=none"}, "content.read", "", role.ScopeNone},
}
for _, c := range duplicates {
t.Run("duplicates resolve to the most restrictive: "+strings.Join(c.rows, ", "), func(t *testing.T) {
forward := unlockedGrantSet(c.rows...)
reversed := slices.Clone(c.rows)
slices.Reverse(reversed)
backward := unlockedGrantSet(reversed...)
if forward.ScopeFor(c.key, c.typeKey) != c.want || backward.ScopeFor(c.key, c.typeKey) != c.want {
t.Errorf("expected %s both ways, got %s and %s", c.want, forward.ScopeFor(c.key, c.typeKey), backward.ScopeFor(c.key, c.typeKey))
}
})
}
t.Run("a toggle key stored at own is none", func(t *testing.T) {
if got := unlockedGrantSet("schema.view=own").ScopeFor("schema.view", ""); got != role.ScopeNone {
t.Errorf("expected none, got %s", got)
}
})
t.Run("an unknown scope string is none", func(t *testing.T) {
if got := unlockedGrantSet("content.read=everything").ScopeFor("content.read", ""); got != role.ScopeNone {
t.Errorf("expected none, got %s", got)
}
})
t.Run("a typed key on a non-content key or with an invalid type key is dropped", func(t *testing.T) {
set := unlockedGrantSet("schema.view:blog=all", "content.read:Bad Key=all", "content.read=all")
if set.ScopeFor("schema.view", "blog") != role.ScopeNone || set.ScopeFor("content.read", "Bad Key") != role.ScopeNone || set.ScopeFor("content.read", "x") != role.ScopeAll {
t.Errorf("unexpected scopes")
}
})
}
func TestRoleGrantSetOwnerOnlyKeys(t *testing.T) {
ownerOnly := []string{"settings.delete_site", "roles.edit_owner_only", "system.apply_updates", "system.edit_retention", "system.transfer_ownership"}
t.Run("owner-only keys are dropped on a role that is not locked", func(t *testing.T) {
for _, key := range ownerOnly {
set := role.Role{Locked: false, Grants: grantRows(key + "=all")}.GrantSet()
if set.ScopeFor(key, "") != role.ScopeNone || set.Holds(key) || set.HoldsAll(key) {
t.Errorf("%s must not be effective on an unlocked role", key)
}
}
})
t.Run("owner-only keys are kept on a locked role", func(t *testing.T) {
for _, key := range ownerOnly {
set := role.Role{Locked: true, Grants: grantRows(key + "=all")}.GrantSet()
if set.ScopeFor(key, "") != role.ScopeAll {
t.Errorf("%s must be effective on a locked role", key)
}
}
})
t.Run("the built-in roles keep their catalogue", func(t *testing.T) {
builtIns := role.BuiltInRoles()
owner, admin := builtIns[0].GrantSet(), builtIns[1].GrantSet()
for _, permission := range role.Catalogue() {
if owner.ScopeFor(permission.Key, "") != role.ScopeAll {
t.Errorf("Owner lacks %s", permission.Key)
}
if permission.OwnerOnly && admin.ScopeFor(permission.Key, "") != role.ScopeNone {
t.Errorf("Admin holds %s", permission.Key)
}
}
})
}
func TestValidateGrantsRows(t *testing.T) {
cases := []struct {
rows []string
want error
}{
{[]string{"content.update=all", "content.update=own"}, role.ErrDuplicateGrant},
{[]string{"content.update:blog=all", "content.update:blog=own"}, role.ErrDuplicateGrant},
{[]string{"content.update=all", "content.update:blog=own"}, nil},
{[]string{"content.update:blog=none"}, nil},
{[]string{"content.update=none"}, role.ErrInvalidScope},
{[]string{"content.create:blog=none"}, nil},
{[]string{"content.create:blog=own"}, role.ErrInvalidScope},
{[]string{"content.update:Bad Key=all"}, role.ErrUnknownPermission},
{[]string{"schema.view:blog=all"}, role.ErrUnknownPermission},
{[]string{"settings.delete_site=all"}, role.ErrOwnerOnlyGrant},
}
for _, c := range cases {
t.Run(strings.Join(c.rows, ", "), func(t *testing.T) {
err := role.ValidateGrants(false, grantRows(c.rows...))
if c.want == nil && err != nil || c.want != nil && !errors.Is(err, c.want) {
t.Errorf("expected %v, got %v", c.want, err)
}
})
}
}
func TestRoleHasNoParallelDecisionHelpers(t *testing.T) {
roleType := reflect.TypeOf(role.Role{})
for _, name := range []string{"Allows", "AllowsFor", "scopeOf"} {
if _, present := roleType.MethodByName(name); present {
t.Errorf("Role must not have %s", name)
}
}
for _, source := range roleDomainSources(t) {
if strings.Contains(source.text, "func NewGrantSet") || strings.Contains(source.text, "scopeOf(") {
t.Errorf("%s still holds a parallel implementation", source.path)
}
}
}
type goSource struct {
path string
text string
}
func roleDomainSources(t *testing.T) []goSource {
t.Helper()
files, err := filepath.Glob("../internal/domain/role/*.go")
if err != nil || len(files) == 0 {
t.Fatalf("no role domain files: %v", err)
}
sources := []goSource{}
for _, file := range files {
sources = append(sources, goSource{path: file, text: readTestFile(t, file)})
}
return sources
}
func TestRoleKeyConstantsMatchTheCatalogue(t *testing.T) {
file, err := parser.ParseFile(token.NewFileSet(), "../internal/domain/role/roleKeys.go", nil, 0)
if err != nil {
t.Fatal(err)
}
constants := map[string]string{}
ast.Inspect(file, func(node ast.Node) bool {
spec, ok := node.(*ast.ValueSpec)
if !ok {
return true
}
for index, name := range spec.Names {
literal, isLiteral := spec.Values[index].(*ast.BasicLit)
if !isLiteral || !name.IsExported() {
continue
}
value, _ := strconv.Unquote(literal.Value)
constants[name.Name] = value
}
return true
})
catalogueKeys := map[string]bool{}
for _, permission := range role.Catalogue() {
catalogueKeys[permission.Key] = true
}
constantValues := map[string]bool{}
for name, value := range constants {
constantValues[value] = true
if !catalogueKeys[value] {
t.Errorf("constant %s = %q is not a catalogue key", name, value)
}
if want := constantNameOf(value); name != want {
t.Errorf("constant for %q is named %s, want %s", value, name, want)
}
}
for key := range catalogueKeys {
if !constantValues[key] {
t.Errorf("catalogue key %q has no constant", key)
}
}
if len(constants) != len(catalogueKeys) {
t.Errorf("expected %d constants, got %d", len(catalogueKeys), len(constants))
}
}
func constantNameOf(key string) string {
group, action, _ := strings.Cut(key, ".")
words := append([]string{group}, strings.Split(action, "_")...)
name := "Key"
for _, word := range words {
if word == "apikeys" {
name += "APIKeys"
continue
}
name += strings.ToUpper(word[:1]) + word[1:]
}
return name
}