feat: Users API, ownership transfer and removal of the legacy role string
This commit is contained in:
1 parent
094df63a7f
commit
80f7037d3a
124 files changed
+4644
-824
No files matched your search
@@ -0,0 +1,152 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/domain/builtintype"
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/domain/invite"
|
||||
"RenewCMS/internal/domain/role"
|
||||
"RenewCMS/internal/domain/user"
|
||||
"errors"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUsersAPIDomain(t *testing.T) {
|
||||
t.Run("user.User has no role string and invite.Invite carries RoleID", func(t *testing.T) {
|
||||
if _, present := reflect.TypeOf(user.User{}).FieldByName("Role"); present {
|
||||
t.Errorf("user.User.Role must not exist")
|
||||
}
|
||||
if field, present := reflect.TypeOf(invite.Invite{}).FieldByName("RoleID"); !present || field.Type.Kind() != reflect.Uint32 {
|
||||
t.Errorf("invite.Invite.RoleID is missing")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("the legacy role vocabulary is gone from the source tree", func(t *testing.T) {
|
||||
for _, path := range nonTestGoFiles(t, "../internal", "../api", "../cmd") {
|
||||
ast.Inspect(parsedGoFile(t, path), func(node ast.Node) bool {
|
||||
switch found := node.(type) {
|
||||
case *ast.SelectorExpr:
|
||||
if owner, ok := found.X.(*ast.Ident); ok && (owner.Name == "user" || owner.Name == "domainUser") && (found.Sel.Name == "Role" || found.Sel.Name == "RoleOwner" || found.Sel.Name == "RoleAdmin") {
|
||||
t.Errorf("%s reads %s.%s", path, owner.Name, found.Sel.Name)
|
||||
}
|
||||
case *ast.Ident:
|
||||
if found.Name == "legacyBuiltInNames" || found.Name == "legacyRoleNames" || found.Name == "BackfillUserRoleIDs" {
|
||||
t.Errorf("%s names %s", path, found.Name)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("the new sentinels map with errors.Is", func(t *testing.T) {
|
||||
wrapped := errors.Join(errors.New("context"), user.ErrRoleChanged, user.ErrInvalidTransfer, user.ErrConfirmationMismatch, role.ErrUnknownRole)
|
||||
for _, sentinel := range []error{user.ErrRoleChanged, user.ErrInvalidTransfer, user.ErrConfirmationMismatch, role.ErrUnknownRole} {
|
||||
if !errors.Is(wrapped, sentinel) {
|
||||
t.Errorf("%v is not matched", sentinel)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("user and invite import the standard library and domain packages only", func(t *testing.T) {
|
||||
for _, pattern := range []string{"../internal/domain/user/*.go", "../internal/domain/invite/*.go"} {
|
||||
files, _ := filepath.Glob(pattern)
|
||||
if len(files) == 0 {
|
||||
t.Fatalf("no files for %s", pattern)
|
||||
}
|
||||
for _, file := range files {
|
||||
for _, imported := range importsOf(t, strings.TrimPrefix(filepath.ToSlash(file), "../")) {
|
||||
if !isStandardLibrary(imported) && !strings.HasPrefix(imported, "RenewCMS/internal/domain/") {
|
||||
t.Errorf("%s imports %s", file, imported)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GrantSet.Covers rows used by the slice", func(t *testing.T) {
|
||||
manager := unlockedGrantSet("content.read=all", "content.create=all", "content.update=own", "content.publish=own", "schema.view=all")
|
||||
cases := []struct {
|
||||
name string
|
||||
grants []string
|
||||
key string
|
||||
}{
|
||||
{"Writer", []string{"content.read=all", "content.create=all", "content.update=all"}, "content.update"},
|
||||
{"Content lead", []string{"content.read=all", "content.update=all", "content.publish=all"}, "content.update"},
|
||||
{"Intern", []string{"content.read=own"}, ""},
|
||||
{"Contributor", []string{"content.read=all", "content.create=all", "content.update=own"}, ""},
|
||||
}
|
||||
for _, row := range cases {
|
||||
err := manager.Covers(unlockedGrantSet(row.grants...), nil)
|
||||
if row.key == "" && err != nil {
|
||||
t.Errorf("%s: unexpected %v", row.name, err)
|
||||
}
|
||||
var escalation role.EscalationError
|
||||
if row.key != "" && (!errors.As(err, &escalation) || escalation.Key != row.key) {
|
||||
t.Errorf("%s: expected an escalation on %s, got %v", row.name, row.key, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("the builtin user type keeps an optional plain text role field", func(t *testing.T) {
|
||||
found, err := builtintype.Find(builtintype.KeyUser)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, field := range found.Fields {
|
||||
if field.Key != "role" {
|
||||
continue
|
||||
}
|
||||
if field.Type != contenttype.TypeText || field.Required || len(field.Options) != 0 {
|
||||
t.Errorf("unexpected role field %+v", field)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Errorf("the user type has no role field")
|
||||
})
|
||||
|
||||
t.Run("api/router.go uses the key constants for the new routes", func(t *testing.T) {
|
||||
wanted := map[string]bool{"KeyUsersChangeRole": false, "KeySystemTransferOwnership": false}
|
||||
ast.Inspect(parsedGoFile(t, "../api/router.go"), func(node ast.Node) bool {
|
||||
if selector, ok := node.(*ast.SelectorExpr); ok {
|
||||
if _, tracked := wanted[selector.Sel.Name]; tracked {
|
||||
wanted[selector.Sel.Name] = true
|
||||
}
|
||||
}
|
||||
if literal, ok := node.(*ast.BasicLit); ok && literal.Kind == token.STRING {
|
||||
if value, err := strconv.Unquote(literal.Value); err == nil && (value == "users.change_role" || value == "system.transfer_ownership") {
|
||||
t.Errorf("router holds the literal %q", value)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
for name, seen := range wanted {
|
||||
if !seen {
|
||||
t.Errorf("router does not use role.%s", name)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("handlers of the slice import no persistence, mailer, models or gorm", func(t *testing.T) {
|
||||
for _, pattern := range []string{"../api/handlers/user/*.go", "../api/handlers/ownership/*.go"} {
|
||||
files, _ := filepath.Glob(pattern)
|
||||
if len(files) == 0 {
|
||||
t.Fatalf("no files for %s", pattern)
|
||||
}
|
||||
for _, file := range files {
|
||||
for _, imported := range importsOf(t, strings.TrimPrefix(filepath.ToSlash(file), "../")) {
|
||||
for _, forbidden := range []string{"persistence", "mailer", "models", "gorm"} {
|
||||
if strings.Contains(imported, forbidden) {
|
||||
t.Errorf("%s imports %s", file, imported)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user