package test import ( "RenewCMS/internal/domain/role" "RenewCMS/internal/domain/user" "RenewCMS/internal/infrastructure/persistence/database" "RenewCMS/internal/infrastructure/persistence/models" persistence "RenewCMS/internal/infrastructure/persistence/repositories" "RenewCMS/internal/infrastructure/useCases" "errors" "slices" "testing" "gorm.io/gorm" ) func roleWithGrant(key string, scope role.Scope) role.Role { return role.Role{Name: "Probe", Grants: []role.Grant{{Key: key, Scope: scope}}} } func TestRoleGrantSetDecisions(t *testing.T) { cases := []struct { key string scope role.Scope owned bool allowed bool }{ {"content.update", role.ScopeAll, false, true}, {"content.update", role.ScopeAll, true, true}, {"content.update", role.ScopeOwn, true, true}, {"content.update", role.ScopeOwn, false, false}, {"content.update", role.ScopeNone, true, false}, {"content.create", role.ScopeAll, false, true}, {"schema.view", role.ScopeNone, false, false}, } for _, c := range cases { t.Run(string(c.scope)+" "+c.key, func(t *testing.T) { createdBy := uint32(2) if c.owned { createdBy = 1 } if got := roleWithGrant(c.key, c.scope).GrantSet().Permits(c.key, "", 1, createdBy); got != c.allowed { t.Errorf("owned=%v: expected %v, got %v", c.owned, c.allowed, got) } }) } t.Run("A key absent from the role is not allowed", func(t *testing.T) { if (role.Role{}).GrantSet().Permits("content.read", "", 1, 1) { t.Errorf("expected false") } }) t.Run("An unknown key is rejected, never silently allowed", func(t *testing.T) { everything := role.BuiltInRoles()[0] everything.Grants = append(everything.Grants, role.Grant{Key: "content.explode", Scope: role.ScopeAll}) if everything.GrantSet().Permits("content.explode", "", 1, 1) { t.Errorf("expected false") } if _, err := role.Lookup("content.explode"); !errors.Is(err, role.ErrUnknownPermission) { t.Errorf("expected ErrUnknownPermission, got %v", err) } }) } func TestRoleCatalogue(t *testing.T) { keys := []string{ "content.read", "content.create", "content.update", "content.delete", "content.publish", "schema.view", "schema.edit", "schema.delete", "media.view", "media.upload", "media.delete", "authors.view", "authors.create", "authors.edit", "authors.delete", "activity.view", "activity.export", "users.view", "users.invite", "users.change_role", "users.remove", "settings.view", "settings.edit_general", "settings.manage_sites", "settings.delete_site", "apikeys.view", "apikeys.create", "apikeys.revoke", "roles.view", "roles.manage", "roles.edit_owner_only", "system.apply_updates", "system.edit_retention", "system.transfer_ownership", } t.Run("The catalogue is stable and complete", func(t *testing.T) { catalogue := role.Catalogue() got := []string{} ownerOnly := []string{} scoped := []string{} for _, permission := range catalogue { got = append(got, permission.Key) if permission.OwnerOnly { ownerOnly = append(ownerOnly, permission.Key) } if permission.Kind == role.KindScoped { scoped = append(scoped, permission.Key) } if permission.Group == "content" && permission.Kind == role.KindToggle && permission.Key != "content.create" { t.Errorf("unexpected toggle %s", permission.Key) } } if !slices.Equal(got, keys) { t.Errorf("catalogue order differs: %v", got) } wantOwnerOnly := []string{"settings.delete_site", "roles.edit_owner_only", "system.apply_updates", "system.edit_retention", "system.transfer_ownership"} if !slices.Equal(ownerOnly, wantOwnerOnly) { t.Errorf("owner-only keys: %v", ownerOnly) } wantScoped := []string{"content.read", "content.update", "content.delete", "content.publish", "media.delete", "authors.edit", "activity.view", "apikeys.view", "apikeys.revoke"} if !slices.Equal(scoped, wantScoped) { t.Errorf("scoped keys: %v", scoped) } wantGroups := []string{"content", "schema", "media", "authors", "activity", "users", "settings", "roles", "system"} if !slices.Equal(role.Groups(), wantGroups) { t.Errorf("groups: %v", role.Groups()) } }) } func grantMap(grants []role.Grant) map[string]role.Scope { scopes := map[string]role.Scope{} for _, grant := range grants { scopes[grant.Key] = grant.Scope } return scopes } func openMigratedRolesDB(t *testing.T) *gorm.DB { t.Helper() db := openUnmigratedSQLite(t) if err := database.Migrate(db, migrationPublicURL); err != nil { t.Fatalf("migrate: %v", err) } return db } func storedRole(t *testing.T, db *gorm.DB, name string) role.Role { t.Helper() found, err := persistence.NewRoleRepository(db).GetByName(name) if err != nil { t.Fatalf("role %s: %v", name, err) } return found } func countRows(t *testing.T, db *gorm.DB, model any) int64 { t.Helper() var total int64 if err := db.Model(model).Count(&total).Error; err != nil { t.Fatalf("count: %v", err) } return total } func TestSeedBuiltInRoles(t *testing.T) { t.Run("Seeding an empty database creates the three built-in roles", func(t *testing.T) { db := openMigratedRolesDB(t) owner, admin, writer := storedRole(t, db, "Owner"), storedRole(t, db, "Admin"), storedRole(t, db, "Writer") for _, found := range []role.Role{owner, admin, writer} { if !found.BuiltIn || found.Version != 1 { t.Errorf("%s: built_in %v version %d", found.Name, found.BuiltIn, found.Version) } } if !owner.Locked || admin.Locked || writer.Locked { t.Errorf("only Owner is locked") } if len(owner.Grants) != 34 || len(admin.Grants) != 29 || len(writer.Grants) != 9 { t.Fatalf("grant counts %d %d %d", len(owner.Grants), len(admin.Grants), len(writer.Grants)) } for _, grant := range append(slices.Clone(owner.Grants), admin.Grants...) { if grant.Scope != role.ScopeAll { t.Errorf("%s not at all", grant.Key) } } for _, grant := range admin.Grants { if permission, _ := role.Lookup(grant.Key); permission.OwnerOnly { t.Errorf("admin holds owner-only %s", grant.Key) } } scopes := grantMap(writer.Grants) if scopes["authors.edit"] != role.ScopeOwn || scopes["activity.view"] != role.ScopeOwn || scopes["content.create"] != role.ScopeAll { t.Errorf("writer grants %v", scopes) } }) t.Run("Seeding twice changes nothing", func(t *testing.T) { db := openMigratedRolesDB(t) ownerID := storedRole(t, db, "Owner").ID if err := database.SeedBuiltInRoles(db); err != nil { t.Fatal(err) } if countRows(t, db, &models.Role{}) != 3 || countRows(t, db, &models.RolePermission{}) != 72 || storedRole(t, db, "Owner").ID != ownerID { t.Errorf("seeding was not idempotent") } }) t.Run("Seeding never overwrites an edited built-in role", func(t *testing.T) { db := openMigratedRolesDB(t) writer := storedRole(t, db, "Writer") if err := db.Model(&models.Role{}).Where("id = ?", writer.ID).Update("version", 4).Error; err != nil { t.Fatal(err) } if err := db.Create(&models.RolePermission{RoleID: writer.ID, Key: "media.delete", Scope: "own"}).Error; err != nil { t.Fatal(err) } if err := database.SeedBuiltInRoles(db); err != nil { t.Fatal(err) } after := storedRole(t, db, "Writer") if after.Version != 4 || grantMap(after.Grants)["media.delete"] != role.ScopeOwn { t.Errorf("edited writer overwritten: %+v", after) } }) t.Run("Seeding never touches a custom role that reuses a built-in name", func(t *testing.T) { db := openUnmigratedSQLite(t) if err := db.AutoMigrate(&models.Role{}, &models.RolePermission{}); err != nil { t.Fatal(err) } custom := models.Role{Name: "Writer", Version: 7} if err := db.Create(&custom).Error; err != nil { t.Fatal(err) } if err := database.SeedBuiltInRoles(db); err != nil { t.Fatalf("startup aborted: %v", err) } var kept models.Role if err := db.First(&kept, custom.ID).Error; err != nil || kept.BuiltIn || kept.Version != 7 { t.Errorf("custom row changed: %+v %v", kept, err) } if found := storedRole(t, db, "Writer"); !found.BuiltIn || found.ID == custom.ID { t.Errorf("built-in writer missing: %+v", found) } }) } func createUserRow(t *testing.T, db *gorm.DB, id uint32, roleID *uint32) { t.Helper() row := models.User{ID: id, Username: "u" + string(rune('a'+id)), Email: string(rune('a'+id)) + "@x.example", Password: "x", RoleID: roleID} if err := db.Create(&row).Error; err != nil { t.Fatalf("user %d: %v", id, err) } } func userRow(t *testing.T, db *gorm.DB, id uint32) models.User { t.Helper() var row models.User if err := db.First(&row, id).Error; err != nil { t.Fatal(err) } return row } func roleIDOf(row models.User) uint32 { if row.RoleID == nil { return 0 } return *row.RoleID } func ownerIDPtr(t *testing.T, db *gorm.DB) *uint32 { t.Helper() id := storedRole(t, db, "Owner").ID return &id } func createdByOf(t *testing.T, db *gorm.DB, table string, id uint32) uint32 { t.Helper() var value uint32 if err := db.Table(table).Unscoped().Select("created_by").Where("id = ?", id).Scan(&value).Error; err != nil { t.Fatal(err) } return value } func createArticleRow(t *testing.T, db *gorm.DB, id uint32, createdBy uint32) { t.Helper() if err := db.Create(&models.Article{ID: id, Title: "Article", Slug: "a-" + string(rune('a'+id)), CreatedBy: createdBy, SiteID: 1}).Error; err != nil { t.Fatal(err) } } func createEntryRow(t *testing.T, db *gorm.DB, id uint32, createdBy uint32) { t.Helper() if err := db.Create(&models.Entry{ID: id, ContentTypeID: 1, Data: "{}", CreatedBy: createdBy}).Error; err != nil { t.Fatal(err) } } func TestBackfillCreatedBy(t *testing.T) { t.Run("Existing articles are attributed to the first owner", func(t *testing.T) { db := openMigratedRolesDB(t) createUserRow(t, db, 3, ownerIDPtr(t, db)) createUserRow(t, db, 4, ownerIDPtr(t, db)) createArticleRow(t, db, 10, 0) createArticleRow(t, db, 11, 0) if err := database.BackfillCreatedBy(db); err != nil { t.Fatal(err) } if createdByOf(t, db, "articles", 10) != 3 || createdByOf(t, db, "articles", 11) != 3 { t.Errorf("articles not attributed") } }) t.Run("Entries without an author are attributed to the first owner", func(t *testing.T) { db := openMigratedRolesDB(t) createUserRow(t, db, 3, ownerIDPtr(t, db)) createEntryRow(t, db, 20, 0) createEntryRow(t, db, 21, 7) if err := database.BackfillCreatedBy(db); err != nil { t.Fatal(err) } if createdByOf(t, db, "entries", 20) != 3 || createdByOf(t, db, "entries", 21) != 7 { t.Errorf("entries wrong") } }) t.Run("The created_by backfill never rewrites an existing value, and is idempotent", func(t *testing.T) { db := openMigratedRolesDB(t) createUserRow(t, db, 3, ownerIDPtr(t, db)) createArticleRow(t, db, 10, 7) for range 2 { if err := database.BackfillCreatedBy(db); err != nil { t.Fatal(err) } } if createdByOf(t, db, "articles", 10) != 7 { t.Errorf("existing value rewritten") } }) t.Run("With no owner yet the backfill does nothing", func(t *testing.T) { db := openMigratedRolesDB(t) createArticleRow(t, db, 10, 0) if err := database.BackfillCreatedBy(db); err != nil { t.Fatalf("unexpected error %v", err) } if createdByOf(t, db, "articles", 10) != 0 { t.Errorf("created_by changed") } }) } func TestNewArticlesRecordTheirCreator(t *testing.T) { t.Run("New articles record their creator", func(t *testing.T) { repo := newMockArticleRepo() uc := useCases.NewCreateArticleUseCase(repo, &fakeBodyRenderer{}, discardRecorder, plainSites(), plainAuthors()) created, err := uc.CreateArticle(testCtx, useCases.CreateArticleCommand{Title: "Valid Title", Body: "x", Caller: ownerCaller(7)}) if err != nil { t.Fatal(err) } if repo.articles[created.ID].CreatedBy != 7 { t.Errorf("stored created_by %d", repo.articles[created.ID].CreatedBy) } }) } func TestGetMyAccessUseCase(t *testing.T) { t.Run("A user with role_id 0 gets an error and is never promoted", func(t *testing.T) { uc := useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo(), newMockUserRepo()) if _, err := uc.Get(user.User{ID: 11}); !errors.Is(err, role.ErrRoleNotFound) { t.Errorf("expected ErrRoleNotFound, got %v", err) } }) t.Run("A user whose role cannot be resolved gets an error", func(t *testing.T) { uc := useCases.NewGetMyAccessUseCase(newSeededMockRoleRepo(), newMockUserRepo()) if _, err := uc.Get(user.User{ID: 13, RoleID: 99}); !errors.Is(err, role.ErrRoleNotFound) { t.Errorf("expected ErrRoleNotFound, got %v", err) } if _, err := uc.Get(user.User{ID: 13}); !errors.Is(err, role.ErrRoleNotFound) { t.Errorf("expected ErrRoleNotFound, got %v", err) } }) }