feat: Users API, ownership transfer and removal of the legacy role string

This commit is contained in:
Florian Sylvain committed 2026-10-27 03:08:15 +01:00
1 parent 094df63a7f
commit 80f7037d3a
124 files changed
+4644 -824

No files matched your search

+13 -76
View File
@@ -229,9 +229,9 @@ func TestSeedBuiltInRoles(t *testing.T) {
})
}
func createUserRow(t *testing.T, db *gorm.DB, id uint32, legacy string, roleID *uint32) {
func createUserRow(t *testing.T, db *gorm.DB, id uint32, roleID *uint32) {
t.Helper()
row := models.User{ID: id, Username: "u" + legacy + string(rune('a'+id)), Email: legacy + string(rune('a'+id)) + "@x.example", Password: "x", Role: legacy, RoleID: roleID}
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)
}
@@ -253,62 +253,10 @@ func roleIDOf(row models.User) uint32 {
return *row.RoleID
}
func TestBackfillUserRoleIDs(t *testing.T) {
t.Run("Existing owner and admin users get the matching built-in role", func(t *testing.T) {
db := openMigratedRolesDB(t)
createUserRow(t, db, 3, "owner", nil)
createUserRow(t, db, 5, "admin", nil)
createUserRow(t, db, 9, "admin", nil)
if err := database.BackfillUserRoleIDs(db); err != nil {
t.Fatal(err)
}
ownerID, adminID := storedRole(t, db, "Owner").ID, storedRole(t, db, "Admin").ID
if roleIDOf(userRow(t, db, 3)) != ownerID || roleIDOf(userRow(t, db, 5)) != adminID || roleIDOf(userRow(t, db, 9)) != adminID {
t.Errorf("role ids not backfilled")
}
if userRow(t, db, 3).Role != "owner" || userRow(t, db, 5).Role != "admin" {
t.Errorf("legacy role changed")
}
})
t.Run("The backfill runs after BackfillUserRoles on a database that predates both", func(t *testing.T) {
db := openMigratedRolesDB(t)
createUserRow(t, db, 2, "", nil)
createUserRow(t, db, 4, "", nil)
if err := database.Migrate(db, migrationPublicURL); err != nil {
t.Fatal(err)
}
two, four := userRow(t, db, 2), userRow(t, db, 4)
if two.Role != "owner" || roleIDOf(two) != storedRole(t, db, "Owner").ID {
t.Errorf("user 2: %+v", two)
}
if four.Role != "admin" || roleIDOf(four) != storedRole(t, db, "Admin").ID {
t.Errorf("user 4: %+v", four)
}
})
t.Run("The role_id backfill is idempotent and respects an existing role_id", func(t *testing.T) {
db := openMigratedRolesDB(t)
custom, err := persistence.NewRoleRepository(db).Create(role.Role{Name: "Contributor", Version: 1})
if err != nil {
t.Fatal(err)
}
createUserRow(t, db, 5, "admin", &custom.ID)
for range 2 {
if err := database.BackfillUserRoleIDs(db); err != nil {
t.Fatal(err)
}
}
if roleIDOf(userRow(t, db, 5)) != custom.ID {
t.Errorf("custom role replaced")
}
})
t.Run("A database with no users is left alone", func(t *testing.T) {
if err := database.BackfillUserRoleIDs(openMigratedRolesDB(t)); err != nil {
t.Errorf("unexpected error %v", err)
}
})
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 {
@@ -337,8 +285,8 @@ func createEntryRow(t *testing.T, db *gorm.DB, id uint32, createdBy uint32) {
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, "owner", nil)
createUserRow(t, db, 4, "owner", nil)
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 {
@@ -351,7 +299,7 @@ func TestBackfillCreatedBy(t *testing.T) {
t.Run("Entries without an author are attributed to the first owner", func(t *testing.T) {
db := openMigratedRolesDB(t)
createUserRow(t, db, 3, "owner", nil)
createUserRow(t, db, 3, ownerIDPtr(t, db))
createEntryRow(t, db, 20, 0)
createEntryRow(t, db, 21, 7)
if err := database.BackfillCreatedBy(db); err != nil {
@@ -364,7 +312,7 @@ func TestBackfillCreatedBy(t *testing.T) {
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, "owner", nil)
createUserRow(t, db, 3, ownerIDPtr(t, db))
createArticleRow(t, db, 10, 7)
for range 2 {
if err := database.BackfillCreatedBy(db); err != nil {
@@ -403,13 +351,10 @@ func TestNewArticlesRecordTheirCreator(t *testing.T) {
}
func TestGetMyAccessUseCase(t *testing.T) {
t.Run("A user with no role_id falls back to the built-in role named by the legacy role", func(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())
for legacy, name := range map[user.Role]string{user.RoleOwner: "Owner", user.RoleAdmin: "Admin"} {
access, err := uc.Get(user.User{ID: 11, Role: legacy})
if err != nil || access.Role.Name != name {
t.Errorf("%s: %v %v", legacy, access.Role.Name, err)
}
if _, err := uc.Get(user.User{ID: 11}); !errors.Is(err, role.ErrRoleNotFound) {
t.Errorf("expected ErrRoleNotFound, got %v", err)
}
})
@@ -422,12 +367,4 @@ func TestGetMyAccessUseCase(t *testing.T) {
t.Errorf("expected ErrRoleNotFound, got %v", err)
}
})
t.Run("The fallback never picks a custom role that reuses a built-in name", func(t *testing.T) {
repo := newMockRoleRepo()
_, _ = repo.Create(role.Role{Name: "Owner"})
if _, err := useCases.NewGetMyAccessUseCase(repo).Get(user.User{Role: user.RoleOwner}); !errors.Is(err, role.ErrRoleNotFound) {
t.Errorf("expected ErrRoleNotFound, got %v", err)
}
})
}