package repository import ( "context" "testing" "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/dhao2001/mygo/internal/model" ) func setupCredentialRepo(t *testing.T) CredentialRepository { t.Helper() db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { t.Fatalf("open db: %v", err) } if err := db.AutoMigrate(&model.Credential{}); err != nil { t.Fatalf("migrate: %v", err) } 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() cred := &model.Credential{ ID: "cred-1", UserID: "user-1", Type: "app_passkey", Label: "My Phone", SecretHash: "hash-abc", } if err := createPasskeyRecord(ctx, repo, cred); err != nil { t.Fatalf("Create = %v", err) } } func TestCredentialRepository_CreateDuplicateHash(t *testing.T) { repo := setupCredentialRepo(t) ctx := context.Background() 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 := createPasskeyRecord(ctx, repo, c1); err != nil { t.Fatalf("Create = %v", err) } err := createPasskeyRecord(ctx, repo, c2) if err != model.ErrDuplicate { t.Fatalf("expected ErrDuplicate, got %v", err) } } func TestCredentialRepository_FindByID(t *testing.T) { repo := setupCredentialRepo(t) ctx := context.Background() cred := &model.Credential{ID: "cred-1", UserID: "user-1", Type: "app_passkey", Label: "Phone", SecretHash: "h1"} if err := createPasskeyRecord(ctx, repo, cred); err != nil { t.Fatalf("Create = %v", err) } found, err := repo.FindPasskeyByID(ctx, "cred-1") if err != nil { t.Fatalf("FindByID = %v", err) } if found.Label != "Phone" { t.Errorf("Label = %q, want %q", found.Label, "Phone") } } func TestCredentialRepository_FindByIDNotFound(t *testing.T) { repo := setupCredentialRepo(t) ctx := context.Background() _, err := repo.FindPasskeyByID(ctx, "nonexistent") if err != model.ErrNotFound { t.Fatalf("expected ErrNotFound, got %v", err) } } func TestCredentialRepository_ListPasskeys(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: "app_passkey", Label: "B", SecretHash: "h2"} 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 := createPasskeyRecord(ctx, repo, c); err != nil { t.Fatalf("Create = %v", err) } } creds, err := repo.ListPasskeys(ctx, "user-1") if err != nil { t.Fatalf("FindByUserID = %v", err) } if len(creds) != 2 { t.Errorf("len(creds) = %d, want 2", len(creds)) } } func TestCredentialRepository_CreatePasskeyControlsType(t *testing.T) { repo := setupCredentialRepo(t) ctx := context.Background() 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.ListPasskeys(ctx, "user-1") if err != nil { t.Fatalf("ListPasskeys = %v", err) } if len(passkeys) != 1 { t.Errorf("len(passkeys) = %d, want 1", len(passkeys)) } if passkeys[0].Type != model.CredentialTypeAppPasskey { t.Errorf("type = %q, want %q", passkeys[0].Type, model.CredentialTypeAppPasskey) } } func TestCredentialRepository_FindByHash(t *testing.T) { repo := setupCredentialRepo(t) ctx := context.Background() cred := &model.Credential{ID: "c-1", UserID: "user-1", Type: "app_passkey", Label: "Phone", SecretHash: "hash-find"} if err := createPasskeyRecord(ctx, repo, cred); err != nil { t.Fatalf("Create = %v", err) } found, err := repo.FindPasskeyByHash(ctx, "hash-find") if err != nil { t.Fatalf("FindByHash = %v", err) } if found.UserID != "user-1" { t.Errorf("UserID = %q, want %q", found.UserID, "user-1") } } func TestCredentialRepository_UpdateLastUsed(t *testing.T) { repo := setupCredentialRepo(t) ctx := context.Background() cred := &model.Credential{ID: "c-1", UserID: "user-1", Type: "app_passkey", Label: "Phone", SecretHash: "h1"} if err := createPasskeyRecord(ctx, repo, cred); err != nil { t.Fatalf("Create = %v", err) } if err := repo.RecordPasskeyUsed(ctx, "c-1"); err != nil { t.Fatalf("UpdateLastUsed = %v", err) } found, err := repo.FindPasskeyByID(ctx, "c-1") if err != nil { t.Fatalf("FindByID = %v", err) } if found.LastUsedAt == nil { t.Fatal("LastUsedAt should not be nil after update") } } func TestCredentialRepository_Delete(t *testing.T) { repo := setupCredentialRepo(t) ctx := context.Background() cred := &model.Credential{ID: "c-1", UserID: "user-1", Type: "app_passkey", Label: "Phone", SecretHash: "h1"} if err := createPasskeyRecord(ctx, repo, cred); err != nil { t.Fatalf("Create = %v", err) } if err := repo.RevokeOwnedPasskey(ctx, "user-1", "c-1"); err != nil { t.Fatalf("Delete = %v", err) } _, 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) } }