feat: more role backend and admin/writer roles additions and adjustments

This commit is contained in:
Florian Sylvain committed 2026-10-26 18:15:26 +01:00
1 parent 8c6c6456da
commit de142f6c5f
131 files changed
+2663 -435

No files matched your search

+172
View File
@@ -0,0 +1,172 @@
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.Scope("authors.edit") != role.ScopeOwn || grants.Scope("users.view") != role.ScopeNone {
t.Errorf("unexpected grants %v", grants)
}
})
t.Run("A custom role is judged by its grants only, owner-only keys included", 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"); err != nil {
t.Errorf("expected the hand-placed owner-only key to be honoured, 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)
}
roles.fail.failNext("ListGrants")
_, 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.Scope("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 := role.NewGrantSet([]role.Grant{{Key: "a", Scope: role.ScopeOwn}, {Key: "b", Scope: role.ScopeAll}, {Key: "c", Scope: role.ScopeNone}})
if !set.Holds("a") || set.HoldsAll("a") || !set.Holds("b") || !set.HoldsAll("b") || set.Holds("c") {
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 = role.NewGrantSet([]role.Grant{{Key: role.KeyActivityView, Scope: role.ScopeAll}})
activityViewOwnGrants = role.NewGrantSet([]role.Grant{{Key: role.KeyActivityView, Scope: role.ScopeOwn}})
authorsEditOwnGrants = role.NewGrantSet([]role.Grant{{Key: role.KeyAuthorsEdit, Scope: role.ScopeOwn}})
authorsEditAllGrants = role.NewGrantSet([]role.Grant{{Key: role.KeyAuthorsEdit, Scope: role.ScopeAll}})
)