253 lines
9.0 KiB
Go
253 lines
9.0 KiB
Go
package test
|
|
|
|
import (
|
|
"RenewCMS/internal/domain/role"
|
|
"RenewCMS/internal/infrastructure/persistence/models"
|
|
persistence "RenewCMS/internal/infrastructure/persistence/repositories"
|
|
"errors"
|
|
"reflect"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func rolesAPIGrantKeys(grants []role.Grant) []string {
|
|
keys := []string{}
|
|
for _, grant := range grants {
|
|
keys = append(keys, grant.Key+"="+string(grant.Scope))
|
|
}
|
|
return keys
|
|
}
|
|
|
|
func rolesAPICreateRole(t *testing.T, repo *persistence.RoleRepository, name string, rows ...string) role.Role {
|
|
t.Helper()
|
|
created, err := repo.Create(role.Role{Name: name, Version: 1, Grants: grantRows(rows...)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return created
|
|
}
|
|
|
|
func TestRoleRepositoryRolesAPI(t *testing.T) {
|
|
t.Run("List returns the grants of every role in one pass", func(t *testing.T) {
|
|
db := openMigratedRolesDB(t)
|
|
repo := persistence.NewRoleRepository(db)
|
|
rolesAPICreateRole(t, repo, "Contributor", "content.read=all", "content.update:events=own")
|
|
listed, err := repo.List()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(listed) != 4 {
|
|
t.Fatalf("expected 4 roles, got %d", len(listed))
|
|
}
|
|
for index, expected := range []int{34, 29, 9, 2} {
|
|
if len(listed[index].Grants) != expected {
|
|
t.Errorf("role %s: expected %d grants, got %d", listed[index].Name, expected, len(listed[index].Grants))
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("seeded roles have no editor and no edit time", func(t *testing.T) {
|
|
repo := persistence.NewRoleRepository(openMigratedRolesDB(t))
|
|
listed, _ := repo.List()
|
|
for _, seeded := range listed {
|
|
if seeded.UpdatedBy != nil || !seeded.UpdatedAt.IsZero() {
|
|
t.Errorf("role %s: unexpected editor %v at %v", seeded.Name, seeded.UpdatedBy, seeded.UpdatedAt)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Save writes the editor and the time and bumps the version", func(t *testing.T) {
|
|
db := openMigratedRolesDB(t)
|
|
repo := persistence.NewRoleRepository(db)
|
|
created := rolesAPICreateRole(t, repo, "Contributor", "content.read=all")
|
|
editor := uint32(5)
|
|
edited := created
|
|
edited.Description = "changed"
|
|
edited.UpdatedAt = time.Date(2026, time.October, 5, 11, 2, 0, 0, time.UTC)
|
|
edited.UpdatedBy = &editor
|
|
saved, err := repo.Save(edited)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if saved.Version != 2 || saved.UpdatedBy == nil || *saved.UpdatedBy != 5 || !saved.UpdatedAt.Equal(edited.UpdatedAt) {
|
|
t.Fatalf("unexpected saved role %+v", saved)
|
|
}
|
|
})
|
|
|
|
t.Run("Save is a compare-and-swap on the version", func(t *testing.T) {
|
|
db := openMigratedRolesDB(t)
|
|
repo := persistence.NewRoleRepository(db)
|
|
created := rolesAPICreateRole(t, repo, "Contributor", "content.read=all")
|
|
first := created
|
|
first.Description = "first"
|
|
second := created
|
|
second.Description = "second"
|
|
if _, err := repo.Save(first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := repo.Save(second); !errors.Is(err, role.ErrRoleConflict) {
|
|
t.Fatalf("expected ErrRoleConflict, got %v", err)
|
|
}
|
|
stored, _ := repo.Get(created.ID)
|
|
if stored.Description != "first" || stored.Version != 2 {
|
|
t.Fatalf("unexpected stored role %+v", stored)
|
|
}
|
|
})
|
|
|
|
t.Run("two concurrent saves of version 1 give exactly one winner", func(t *testing.T) {
|
|
db := openMigratedRolesDB(t)
|
|
repo := persistence.NewRoleRepository(db)
|
|
created := rolesAPICreateRole(t, repo, "Contributor", "content.read=all")
|
|
results := make([]error, 2)
|
|
var group sync.WaitGroup
|
|
for index := range results {
|
|
group.Add(1)
|
|
go func() {
|
|
defer group.Done()
|
|
attempt := created
|
|
attempt.Description = []string{"a", "b"}[index]
|
|
_, results[index] = repo.Save(attempt)
|
|
}()
|
|
}
|
|
group.Wait()
|
|
successes := 0
|
|
for _, err := range results {
|
|
if err == nil {
|
|
successes++
|
|
} else if !errors.Is(err, role.ErrRoleConflict) {
|
|
t.Fatalf("unexpected error %v", err)
|
|
}
|
|
}
|
|
if successes != 1 {
|
|
t.Fatalf("expected exactly one winner, got %d", successes)
|
|
}
|
|
})
|
|
|
|
t.Run("Delete removes the role and its grants", func(t *testing.T) {
|
|
db := openMigratedRolesDB(t)
|
|
repo := persistence.NewRoleRepository(db)
|
|
created := rolesAPICreateRole(t, repo, "Intern", "content.read=own")
|
|
rowsBefore := countRows(t, db, &models.RolePermission{})
|
|
if err := repo.Delete(created.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := repo.Get(created.ID); !errors.Is(err, role.ErrRoleNotFound) {
|
|
t.Fatalf("expected the role to be gone, got %v", err)
|
|
}
|
|
if countRows(t, db, &models.RolePermission{}) != rowsBefore-1 {
|
|
t.Error("the grant rows were not removed")
|
|
}
|
|
})
|
|
|
|
t.Run("Delete refuses a role that still has members", func(t *testing.T) {
|
|
db := openMigratedRolesDB(t)
|
|
repo := persistence.NewRoleRepository(db)
|
|
created := rolesAPICreateRole(t, repo, "Contributor", "content.read=all")
|
|
createUserRow(t, db, 1, "", &created.ID)
|
|
if err := repo.Delete(created.ID); !errors.Is(err, role.ErrRoleHasMembers) {
|
|
t.Fatalf("expected ErrRoleHasMembers, got %v", err)
|
|
}
|
|
if _, err := repo.Get(created.ID); err != nil {
|
|
t.Fatalf("the role must still exist: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Delete reports a missing role", func(t *testing.T) {
|
|
repo := persistence.NewRoleRepository(openMigratedRolesDB(t))
|
|
if err := repo.Delete(9999); !errors.Is(err, role.ErrRoleNotFound) {
|
|
t.Fatalf("expected ErrRoleNotFound, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ReassignMembers counts the moved users and touches only role_id", func(t *testing.T) {
|
|
db := openMigratedRolesDB(t)
|
|
repo := persistence.NewRoleRepository(db)
|
|
source := rolesAPICreateRole(t, repo, "Contributor", "content.read=all")
|
|
writer := storedRole(t, db, "Writer")
|
|
createUserRow(t, db, 1, "", &source.ID)
|
|
createUserRow(t, db, 2, "", &source.ID)
|
|
createUserRow(t, db, 3, "", &writer.ID)
|
|
before := userRow(t, db, 1)
|
|
moved, err := repo.ReassignMembers(source.ID, writer.ID)
|
|
if err != nil || moved != 2 {
|
|
t.Fatalf("expected 2 moved, got %d %v", moved, err)
|
|
}
|
|
after := userRow(t, db, 1)
|
|
if roleIDOf(after) != writer.ID {
|
|
t.Errorf("user 1 was not moved")
|
|
}
|
|
after.RoleID, before.RoleID = nil, nil
|
|
if !reflect.DeepEqual(before, after) {
|
|
t.Errorf("only role_id may change: %+v vs %+v", before, after)
|
|
}
|
|
if moved, _ := repo.ReassignMembers(source.ID, writer.ID); moved != 0 {
|
|
t.Errorf("a second move must move nobody, got %d", moved)
|
|
}
|
|
})
|
|
|
|
t.Run("RemoveTypedGrants removes the five typed forms of one type only and bumps affected roles once", func(t *testing.T) {
|
|
db := openMigratedRolesDB(t)
|
|
repo := persistence.NewRoleRepository(db)
|
|
affected := rolesAPICreateRole(t, repo, "Events editor", "content.read=all", "content.update:events=own", "content.delete:events=none", "content.read:events=all", "content.create:events=all", "content.publish:events=own", "content.update:venues=all")
|
|
untouched := rolesAPICreateRole(t, repo, "Venues only", "content.update:venues=all", "content.read=all")
|
|
removed, err := repo.RemoveTypedGrants("events")
|
|
if err != nil || removed != 1 {
|
|
t.Fatalf("expected 1 affected role, got %d %v", removed, err)
|
|
}
|
|
stored, _ := repo.Get(affected.ID)
|
|
if !reflect.DeepEqual(rolesAPIGrantKeys(stored.Grants), []string{"content.read=all", "content.update:venues=all"}) || stored.Version != 2 {
|
|
t.Fatalf("unexpected affected role %v version %d", rolesAPIGrantKeys(stored.Grants), stored.Version)
|
|
}
|
|
other, _ := repo.Get(untouched.ID)
|
|
if other.Version != 1 || len(other.Grants) != 2 {
|
|
t.Fatalf("the other role must be untouched: %+v", other)
|
|
}
|
|
if again, _ := repo.RemoveTypedGrants("events"); again != 0 {
|
|
t.Errorf("a second removal must affect nothing, got %d", again)
|
|
}
|
|
})
|
|
|
|
t.Run("a content type key that prefixes another is not matched", func(t *testing.T) {
|
|
db := openMigratedRolesDB(t)
|
|
repo := persistence.NewRoleRepository(db)
|
|
kept := rolesAPICreateRole(t, repo, "Long key", "content.update:events_extra=all")
|
|
if removed, _ := repo.RemoveTypedGrants("events"); removed != 0 {
|
|
t.Fatalf("expected nothing removed, got %d", removed)
|
|
}
|
|
stored, _ := repo.Get(kept.ID)
|
|
if len(stored.Grants) != 1 {
|
|
t.Fatal("the grant of events_extra must be kept")
|
|
}
|
|
})
|
|
|
|
t.Run("the new methods fail on a broken database", func(t *testing.T) {
|
|
for name, call := range map[string]func(*persistence.RoleRepository) error{
|
|
"Delete": func(r *persistence.RoleRepository) error { return r.Delete(4) },
|
|
"ReassignMembers": func(r *persistence.RoleRepository) error { _, err := r.ReassignMembers(4, 3); return err },
|
|
"RemoveTypedGrants": func(r *persistence.RoleRepository) error { _, err := r.RemoveTypedGrants("events"); return err },
|
|
} {
|
|
db := openMigratedRolesDB(t)
|
|
for _, table := range []string{"role_permissions", "users"} {
|
|
if err := db.Migrator().DropTable(table); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
sqlDB, _ := db.DB()
|
|
_ = sqlDB.Close()
|
|
if err := call(persistence.NewRoleRepository(db)); err == nil || errors.Is(err, role.ErrRoleNotFound) {
|
|
t.Errorf("%s: expected a storage failure, got %v", name, err)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("the role columns exist after Migrate", func(t *testing.T) {
|
|
db := openMigratedRolesDB(t)
|
|
for _, column := range []string{"updated_at", "updated_by"} {
|
|
if !db.Migrator().HasColumn(&models.Role{}, column) {
|
|
t.Errorf("roles.%s is missing", column)
|
|
}
|
|
}
|
|
})
|
|
}
|