feat(repository): refactor query/mutation ports with capability-safe
interfaces - refactor: split UserRepository into AuthUserRepository and AdminUserRepository capabilities - refactor: split SessionRepository into AuthSessionRepository and repository-owned CLI/admin methods - refactor: replace generic Repository Update/Delete with operation-specific params and ownership predicates - refactor: replace CredentialRepository generic CRUD with passkey-specific methods - refactor: replace FileRepository generic Create/Update/Delete with UploadedFileParams, DirectoryParams, and owned soft-delete - refactor: remove repository fields from WebApp struct; repositories are now composition-time wiring only - feat: add domain error kinds ErrParentNotFound, ErrParentNotDir, ErrDirectoryNotEmpty, ErrInvalidMove - feat: add CredentialTypeAppPasskey constant - feat: add testutil.SetUserAdmin for test fixture setup that bypasses production service ports - test: add architecture test banning GORM Save in repository package - test: add capability interface contract tests ensuring each service receives the minimal interface - test: add blockingStorage helper for concurrent promotion tests - test: add preserved DSN parameter test for sqliteImmediateDSN - docs: update architecture decisions with repository write rules and capability separation - docs: update roadmap to clarify atomic single-use refresh sessions - docs: add -race test target to development docs
This commit is contained in:
@@ -24,6 +24,16 @@ func setupUserRepo(t *testing.T) UserRepository {
|
||||
return NewUserRepository(db)
|
||||
}
|
||||
|
||||
func createUserRecord(ctx context.Context, repo UserRepository, user *model.User) error {
|
||||
_, err := repo.CreateRegisteredUser(ctx, RegisteredUserParams{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
PasswordHash: user.PasswordHash,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func TestUserRepository_Create(t *testing.T) {
|
||||
repo := setupUserRepo(t)
|
||||
ctx := context.Background()
|
||||
@@ -36,7 +46,7 @@ func TestUserRepository_Create(t *testing.T) {
|
||||
Status: model.StatusActive,
|
||||
}
|
||||
|
||||
if err := repo.Create(ctx, user); err != nil {
|
||||
if err := createUserRecord(ctx, repo, user); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -48,11 +58,11 @@ func TestUserRepository_CreateDuplicateUsername(t *testing.T) {
|
||||
u1 := &model.User{ID: "user-1", Username: "alice", Email: "alice@example.com", PasswordHash: "hash", Status: model.StatusActive}
|
||||
u2 := &model.User{ID: "user-2", Username: "alice", Email: "alice2@example.com", PasswordHash: "hash", Status: model.StatusActive}
|
||||
|
||||
if err := repo.Create(ctx, u1); err != nil {
|
||||
if err := createUserRecord(ctx, repo, u1); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, u2)
|
||||
err := createUserRecord(ctx, repo, u2)
|
||||
if err != model.ErrDuplicate {
|
||||
t.Fatalf("expected ErrDuplicate, got %v", err)
|
||||
}
|
||||
@@ -63,7 +73,7 @@ func TestUserRepository_FindByID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
user := &model.User{ID: "user-1", Username: "alice", Email: "alice@example.com", PasswordHash: "hash", Status: model.StatusActive}
|
||||
if err := repo.Create(ctx, user); err != nil {
|
||||
if err := createUserRecord(ctx, repo, user); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
|
||||
@@ -91,7 +101,7 @@ func TestUserRepository_FindByEmail(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
user := &model.User{ID: "user-1", Username: "alice", Email: "alice@example.com", PasswordHash: "hash", Status: model.StatusActive}
|
||||
if err := repo.Create(ctx, user); err != nil {
|
||||
if err := createUserRecord(ctx, repo, user); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
|
||||
@@ -109,7 +119,7 @@ func TestUserRepository_FindByUsername(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
user := &model.User{ID: "user-1", Username: "alice", Email: "alice@example.com", PasswordHash: "hash", Status: model.StatusActive}
|
||||
if err := repo.Create(ctx, user); err != nil {
|
||||
if err := createUserRecord(ctx, repo, user); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
|
||||
@@ -122,26 +132,24 @@ func TestUserRepository_FindByUsername(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserRepository_Update(t *testing.T) {
|
||||
func TestUserRepository_CreateRegisteredUserControlsPrivilegedFields(t *testing.T) {
|
||||
repo := setupUserRepo(t)
|
||||
ctx := context.Background()
|
||||
|
||||
user := &model.User{ID: "user-1", Username: "alice", Email: "alice@example.com", PasswordHash: "hash", Status: model.StatusActive}
|
||||
if err := repo.Create(ctx, user); err != nil {
|
||||
if err := createUserRecord(ctx, repo, user); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
|
||||
user.Username = "alice2"
|
||||
if err := repo.Update(ctx, user); err != nil {
|
||||
t.Fatalf("Update = %v", err)
|
||||
}
|
||||
|
||||
found, err := repo.FindByID(ctx, "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("FindByID = %v", err)
|
||||
}
|
||||
if found.Username != "alice2" {
|
||||
t.Errorf("username = %q, want %q", found.Username, "alice2")
|
||||
if found.IsAdmin {
|
||||
t.Error("registered user must not be an administrator")
|
||||
}
|
||||
if found.Status != model.StatusActive {
|
||||
t.Errorf("status = %q, want %q", found.Status, model.StatusActive)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,11 +158,11 @@ func TestUserRepository_Delete(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
user := &model.User{ID: "user-1", Username: "alice", Email: "alice@example.com", PasswordHash: "hash", Status: model.StatusActive}
|
||||
if err := repo.Create(ctx, user); err != nil {
|
||||
if err := createUserRecord(ctx, repo, user); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
|
||||
if err := repo.Delete(ctx, "user-1"); err != nil {
|
||||
if err := repo.MarkAdminDeleted(ctx, "user-1"); err != nil {
|
||||
t.Fatalf("Delete = %v", err)
|
||||
}
|
||||
|
||||
@@ -176,7 +184,7 @@ func TestUserRepository_List(t *testing.T) {
|
||||
PasswordHash: "hash",
|
||||
Status: model.StatusActive,
|
||||
}
|
||||
if err := repo.Create(ctx, user); err != nil {
|
||||
if err := createUserRecord(ctx, repo, user); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -204,11 +212,11 @@ func TestUserRepository_SoftDelete(t *testing.T) {
|
||||
PasswordHash: "hash",
|
||||
Status: model.StatusActive,
|
||||
}
|
||||
if err := repo.Create(ctx, user); err != nil {
|
||||
if err := createUserRecord(ctx, repo, user); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
|
||||
if err := repo.Delete(ctx, "user-1"); err != nil {
|
||||
if err := repo.MarkAdminDeleted(ctx, "user-1"); err != nil {
|
||||
t.Fatalf("Delete = %v", err)
|
||||
}
|
||||
|
||||
@@ -231,11 +239,11 @@ func TestUserRepository_DisabledLogin(t *testing.T) {
|
||||
PasswordHash: "hash",
|
||||
Status: model.StatusActive,
|
||||
}
|
||||
if err := repo.Create(ctx, user); err != nil {
|
||||
if err := createUserRecord(ctx, repo, user); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
|
||||
if err := repo.Delete(ctx, "user-1"); err != nil {
|
||||
if err := repo.MarkAdminDeleted(ctx, "user-1"); err != nil {
|
||||
t.Fatalf("Delete = %v", err)
|
||||
}
|
||||
|
||||
@@ -258,7 +266,7 @@ func TestUserRepository_StatusFilterList(t *testing.T) {
|
||||
PasswordHash: "hash",
|
||||
Status: model.StatusActive,
|
||||
}
|
||||
if err := repo.Create(ctx, u1); err != nil {
|
||||
if err := createUserRecord(ctx, repo, u1); err != nil {
|
||||
t.Fatalf("Create u1 = %v", err)
|
||||
}
|
||||
|
||||
@@ -269,12 +277,12 @@ func TestUserRepository_StatusFilterList(t *testing.T) {
|
||||
PasswordHash: "hash",
|
||||
Status: model.StatusActive,
|
||||
}
|
||||
if err := repo.Create(ctx, u2); err != nil {
|
||||
if err := createUserRecord(ctx, repo, u2); err != nil {
|
||||
t.Fatalf("Create u2 = %v", err)
|
||||
}
|
||||
|
||||
// Soft-delete bob
|
||||
if err := repo.Delete(ctx, "user-2"); err != nil {
|
||||
if err := repo.MarkAdminDeleted(ctx, "user-2"); err != nil {
|
||||
t.Fatalf("Delete u2 = %v", err)
|
||||
}
|
||||
|
||||
@@ -305,15 +313,15 @@ func TestUserRepository_DeleteIdempotent(t *testing.T) {
|
||||
PasswordHash: "hash",
|
||||
Status: model.StatusActive,
|
||||
}
|
||||
if err := repo.Create(ctx, user); err != nil {
|
||||
if err := createUserRecord(ctx, repo, user); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
|
||||
// Soft-delete the same user twice should not error.
|
||||
if err := repo.Delete(ctx, "user-1"); err != nil {
|
||||
if err := repo.MarkAdminDeleted(ctx, "user-1"); err != nil {
|
||||
t.Fatalf("first Delete = %v", err)
|
||||
}
|
||||
if err := repo.Delete(ctx, "user-1"); err != nil {
|
||||
if err := repo.MarkAdminDeleted(ctx, "user-1"); err != nil {
|
||||
t.Fatalf("second Delete = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user