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) } } } } } }) }