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,12 @@ func setupCredentialRepo(t *testing.T) CredentialRepository {
|
||||
return NewCredentialRepository(db)
|
||||
}
|
||||
|
||||
func createPasskeyRecord(ctx context.Context, repo CredentialRepository, cred *model.Credential) error {
|
||||
return repo.CreatePasskey(ctx, PasskeyParams{
|
||||
ID: cred.ID, UserID: cred.UserID, Label: cred.Label, SecretHash: cred.SecretHash,
|
||||
})
|
||||
}
|
||||
|
||||
func TestCredentialRepository_Create(t *testing.T) {
|
||||
repo := setupCredentialRepo(t)
|
||||
ctx := context.Background()
|
||||
@@ -36,7 +42,7 @@ func TestCredentialRepository_Create(t *testing.T) {
|
||||
SecretHash: "hash-abc",
|
||||
}
|
||||
|
||||
if err := repo.Create(ctx, cred); err != nil {
|
||||
if err := createPasskeyRecord(ctx, repo, cred); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
}
|
||||
@@ -48,11 +54,11 @@ func TestCredentialRepository_CreateDuplicateHash(t *testing.T) {
|
||||
c1 := &model.Credential{ID: "cred-1", UserID: "user-1", Type: "app_passkey", Label: "A", SecretHash: "hash-abc"}
|
||||
c2 := &model.Credential{ID: "cred-2", UserID: "user-1", Type: "app_passkey", Label: "B", SecretHash: "hash-abc"}
|
||||
|
||||
if err := repo.Create(ctx, c1); err != nil {
|
||||
if err := createPasskeyRecord(ctx, repo, c1); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
|
||||
err := repo.Create(ctx, c2)
|
||||
err := createPasskeyRecord(ctx, repo, c2)
|
||||
if err != model.ErrDuplicate {
|
||||
t.Fatalf("expected ErrDuplicate, got %v", err)
|
||||
}
|
||||
@@ -63,11 +69,11 @@ func TestCredentialRepository_FindByID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
cred := &model.Credential{ID: "cred-1", UserID: "user-1", Type: "app_passkey", Label: "Phone", SecretHash: "h1"}
|
||||
if err := repo.Create(ctx, cred); err != nil {
|
||||
if err := createPasskeyRecord(ctx, repo, cred); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
|
||||
found, err := repo.FindByID(ctx, "cred-1")
|
||||
found, err := repo.FindPasskeyByID(ctx, "cred-1")
|
||||
if err != nil {
|
||||
t.Fatalf("FindByID = %v", err)
|
||||
}
|
||||
@@ -80,13 +86,13 @@ func TestCredentialRepository_FindByIDNotFound(t *testing.T) {
|
||||
repo := setupCredentialRepo(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := repo.FindByID(ctx, "nonexistent")
|
||||
_, err := repo.FindPasskeyByID(ctx, "nonexistent")
|
||||
if err != model.ErrNotFound {
|
||||
t.Fatalf("expected ErrNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialRepository_FindByUserID(t *testing.T) {
|
||||
func TestCredentialRepository_ListPasskeys(t *testing.T) {
|
||||
repo := setupCredentialRepo(t)
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -95,12 +101,12 @@ func TestCredentialRepository_FindByUserID(t *testing.T) {
|
||||
c3 := &model.Credential{ID: "c-3", UserID: "user-2", Type: "app_passkey", Label: "C", SecretHash: "h3"}
|
||||
|
||||
for _, c := range []*model.Credential{c1, c2, c3} {
|
||||
if err := repo.Create(ctx, c); err != nil {
|
||||
if err := createPasskeyRecord(ctx, repo, c); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
creds, err := repo.FindByUserID(ctx, "user-1")
|
||||
creds, err := repo.ListPasskeys(ctx, "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("FindByUserID = %v", err)
|
||||
}
|
||||
@@ -109,28 +115,24 @@ func TestCredentialRepository_FindByUserID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialRepository_FindByUserIDAndType(t *testing.T) {
|
||||
func TestCredentialRepository_CreatePasskeyControlsType(t *testing.T) {
|
||||
repo := setupCredentialRepo(t)
|
||||
ctx := context.Background()
|
||||
|
||||
c1 := &model.Credential{ID: "c-1", UserID: "user-1", Type: "app_passkey", Label: "A", SecretHash: "h1"}
|
||||
c2 := &model.Credential{ID: "c-2", UserID: "user-1", Type: "oauth", Label: "Github", SecretHash: "h2"}
|
||||
|
||||
for _, c := range []*model.Credential{c1, c2} {
|
||||
if err := repo.Create(ctx, c); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
cred := &model.Credential{ID: "c-1", UserID: "user-1", Label: "A", SecretHash: "h1"}
|
||||
if err := createPasskeyRecord(ctx, repo, cred); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
|
||||
passkeys, err := repo.FindByUserIDAndType(ctx, "user-1", "app_passkey")
|
||||
passkeys, err := repo.ListPasskeys(ctx, "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("FindByUserIDAndType = %v", err)
|
||||
t.Fatalf("ListPasskeys = %v", err)
|
||||
}
|
||||
if len(passkeys) != 1 {
|
||||
t.Errorf("len(passkeys) = %d, want 1", len(passkeys))
|
||||
}
|
||||
if passkeys[0].Type != "app_passkey" {
|
||||
t.Errorf("type = %q, want %q", passkeys[0].Type, "app_passkey")
|
||||
if passkeys[0].Type != model.CredentialTypeAppPasskey {
|
||||
t.Errorf("type = %q, want %q", passkeys[0].Type, model.CredentialTypeAppPasskey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,11 +141,11 @@ func TestCredentialRepository_FindByHash(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
cred := &model.Credential{ID: "c-1", UserID: "user-1", Type: "app_passkey", Label: "Phone", SecretHash: "hash-find"}
|
||||
if err := repo.Create(ctx, cred); err != nil {
|
||||
if err := createPasskeyRecord(ctx, repo, cred); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
|
||||
found, err := repo.FindByHash(ctx, "hash-find")
|
||||
found, err := repo.FindPasskeyByHash(ctx, "hash-find")
|
||||
if err != nil {
|
||||
t.Fatalf("FindByHash = %v", err)
|
||||
}
|
||||
@@ -157,15 +159,15 @@ func TestCredentialRepository_UpdateLastUsed(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
cred := &model.Credential{ID: "c-1", UserID: "user-1", Type: "app_passkey", Label: "Phone", SecretHash: "h1"}
|
||||
if err := repo.Create(ctx, cred); err != nil {
|
||||
if err := createPasskeyRecord(ctx, repo, cred); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
|
||||
if err := repo.UpdateLastUsed(ctx, "c-1"); err != nil {
|
||||
if err := repo.RecordPasskeyUsed(ctx, "c-1"); err != nil {
|
||||
t.Fatalf("UpdateLastUsed = %v", err)
|
||||
}
|
||||
|
||||
found, err := repo.FindByID(ctx, "c-1")
|
||||
found, err := repo.FindPasskeyByID(ctx, "c-1")
|
||||
if err != nil {
|
||||
t.Fatalf("FindByID = %v", err)
|
||||
}
|
||||
@@ -179,16 +181,35 @@ func TestCredentialRepository_Delete(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
cred := &model.Credential{ID: "c-1", UserID: "user-1", Type: "app_passkey", Label: "Phone", SecretHash: "h1"}
|
||||
if err := repo.Create(ctx, cred); err != nil {
|
||||
if err := createPasskeyRecord(ctx, repo, cred); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
|
||||
if err := repo.Delete(ctx, "c-1"); err != nil {
|
||||
if err := repo.RevokeOwnedPasskey(ctx, "user-1", "c-1"); err != nil {
|
||||
t.Fatalf("Delete = %v", err)
|
||||
}
|
||||
|
||||
_, err := repo.FindByID(ctx, "c-1")
|
||||
_, err := repo.FindPasskeyByID(ctx, "c-1")
|
||||
if err != model.ErrNotFound {
|
||||
t.Fatalf("expected ErrNotFound after delete, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialRepository_RevokeOwnedPasskeyEnforcesOwner(t *testing.T) {
|
||||
repo := setupCredentialRepo(t)
|
||||
ctx := context.Background()
|
||||
cred := &model.Credential{ID: "c-1", UserID: "user-1", Label: "Phone", SecretHash: "h1"}
|
||||
if err := createPasskeyRecord(ctx, repo, cred); err != nil {
|
||||
t.Fatalf("Create = %v", err)
|
||||
}
|
||||
|
||||
if err := repo.RevokeOwnedPasskey(ctx, "user-2", cred.ID); err != model.ErrNotFound {
|
||||
t.Fatalf("cross-owner revoke = %v, want ErrNotFound", err)
|
||||
}
|
||||
if _, err := repo.FindPasskeyByID(ctx, cred.ID); err != nil {
|
||||
t.Fatalf("cross-owner revoke removed passkey: %v", err)
|
||||
}
|
||||
if err := repo.RevokeOwnedPasskey(ctx, cred.UserID, cred.ID); err != nil {
|
||||
t.Fatalf("owner revoke = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user