package db import ( "errors" "testing" "git.wntrmute.dev/kyle/mcias/internal/model" ) func TestWebAuthnCRUD(t *testing.T) { database := openTestDB(t) acct, err := database.CreateAccount("webauthnuser", model.AccountTypeHuman, "hash") if err != nil { t.Fatalf("create account: %v", err) } // Empty state. has, err := database.HasWebAuthnCredentials(acct.ID) if err != nil { t.Fatalf("has credentials: %v", err) } if has { t.Error("expected no credentials") } count, err := database.CountWebAuthnCredentials(acct.ID) if err != nil { t.Fatalf("count credentials: %v", err) } if count != 0 { t.Errorf("expected 0 credentials, got %d", count) } creds, err := database.GetWebAuthnCredentials(acct.ID) if err != nil { t.Fatalf("get credentials (empty): %v", err) } if len(creds) != 0 { t.Errorf("expected 0 credentials, got %d", len(creds)) } // Create credential. cred := &model.WebAuthnCredential{ AccountID: acct.ID, Name: "Test Key", CredentialIDEnc: []byte("enc-cred-id"), CredentialIDNonce: []byte("nonce-cred-id"), PublicKeyEnc: []byte("enc-pubkey"), PublicKeyNonce: []byte("nonce-pubkey"), AAGUID: "2fc0579f811347eab116bb5a8db9202a", SignCount: 0, Discoverable: true, Transports: "usb,nfc", } id, err := database.CreateWebAuthnCredential(cred) if err != nil { t.Fatalf("create credential: %v", err) } if id == 0 { t.Error("expected non-zero credential ID") } // Now has credentials. has, err = database.HasWebAuthnCredentials(acct.ID) if err != nil { t.Fatalf("has credentials after create: %v", err) } if !has { t.Error("expected credentials to exist") } count, err = database.CountWebAuthnCredentials(acct.ID) if err != nil { t.Fatalf("count after create: %v", err) } if count != 1 { t.Errorf("expected 1 credential, got %d", count) } // Get by ID. got, err := database.GetWebAuthnCredentialByID(id) if err != nil { t.Fatalf("get by ID: %v", err) } if got.Name != "Test Key" { t.Errorf("Name = %q, want %q", got.Name, "Test Key") } if !got.Discoverable { t.Error("expected discoverable=true") } if got.Transports != "usb,nfc" { t.Errorf("Transports = %q, want %q", got.Transports, "usb,nfc") } if got.AccountID != acct.ID { t.Errorf("AccountID = %d, want %d", got.AccountID, acct.ID) } // Get list. creds, err = database.GetWebAuthnCredentials(acct.ID) if err != nil { t.Fatalf("get credentials: %v", err) } if len(creds) != 1 { t.Fatalf("expected 1 credential, got %d", len(creds)) } if creds[0].ID != id { t.Errorf("credential ID = %d, want %d", creds[0].ID, id) } // Update sign count. if err := database.UpdateWebAuthnSignCount(id, 5); err != nil { t.Fatalf("update sign count: %v", err) } got, _ = database.GetWebAuthnCredentialByID(id) if got.SignCount != 5 { t.Errorf("SignCount = %d, want 5", got.SignCount) } // Update last used. if err := database.UpdateWebAuthnLastUsed(id); err != nil { t.Fatalf("update last used: %v", err) } got, _ = database.GetWebAuthnCredentialByID(id) if got.LastUsedAt == nil { t.Error("expected LastUsedAt to be set") } } func TestWebAuthnDeleteOwnership(t *testing.T) { database := openTestDB(t) acct1, _ := database.CreateAccount("wa1", model.AccountTypeHuman, "hash") acct2, _ := database.CreateAccount("wa2", model.AccountTypeHuman, "hash") cred := &model.WebAuthnCredential{ AccountID: acct1.ID, Name: "Key", CredentialIDEnc: []byte("enc"), CredentialIDNonce: []byte("nonce"), PublicKeyEnc: []byte("enc"), PublicKeyNonce: []byte("nonce"), } id, _ := database.CreateWebAuthnCredential(cred) // Delete with wrong owner should fail. err := database.DeleteWebAuthnCredential(id, acct2.ID) if !errors.Is(err, ErrNotFound) { t.Errorf("expected ErrNotFound for wrong owner, got %v", err) } // Delete with correct owner succeeds. if err := database.DeleteWebAuthnCredential(id, acct1.ID); err != nil { t.Fatalf("delete with correct owner: %v", err) } // Verify gone. _, err = database.GetWebAuthnCredentialByID(id) if !errors.Is(err, ErrNotFound) { t.Errorf("expected ErrNotFound after delete, got %v", err) } } func TestWebAuthnDeleteAdmin(t *testing.T) { database := openTestDB(t) acct, _ := database.CreateAccount("waadmin", model.AccountTypeHuman, "hash") cred := &model.WebAuthnCredential{ AccountID: acct.ID, Name: "Key", CredentialIDEnc: []byte("enc"), CredentialIDNonce: []byte("nonce"), PublicKeyEnc: []byte("enc"), PublicKeyNonce: []byte("nonce"), } id, _ := database.CreateWebAuthnCredential(cred) // Admin delete (no ownership check). if err := database.DeleteWebAuthnCredentialAdmin(id); err != nil { t.Fatalf("admin delete: %v", err) } // Non-existent should return ErrNotFound. if err := database.DeleteWebAuthnCredentialAdmin(id); !errors.Is(err, ErrNotFound) { t.Errorf("expected ErrNotFound for non-existent, got %v", err) } } func TestWebAuthnDeleteAll(t *testing.T) { database := openTestDB(t) acct, _ := database.CreateAccount("wada", model.AccountTypeHuman, "hash") for i := range 3 { cred := &model.WebAuthnCredential{ AccountID: acct.ID, Name: "Key", CredentialIDEnc: []byte{byte(i)}, CredentialIDNonce: []byte("n"), PublicKeyEnc: []byte{byte(i)}, PublicKeyNonce: []byte("n"), } if _, err := database.CreateWebAuthnCredential(cred); err != nil { t.Fatalf("create %d: %v", i, err) } } deleted, err := database.DeleteAllWebAuthnCredentials(acct.ID) if err != nil { t.Fatalf("delete all: %v", err) } if deleted != 3 { t.Errorf("expected 3 deleted, got %d", deleted) } count, _ := database.CountWebAuthnCredentials(acct.ID) if count != 0 { t.Errorf("expected 0 after delete all, got %d", count) } } func TestWebAuthnCascadeDelete(t *testing.T) { database := openTestDB(t) acct, _ := database.CreateAccount("wacascade", model.AccountTypeHuman, "hash") cred := &model.WebAuthnCredential{ AccountID: acct.ID, Name: "Key", CredentialIDEnc: []byte("enc"), CredentialIDNonce: []byte("nonce"), PublicKeyEnc: []byte("enc"), PublicKeyNonce: []byte("nonce"), } id, _ := database.CreateWebAuthnCredential(cred) // Delete the account — credentials should cascade. if err := database.UpdateAccountStatus(acct.ID, model.AccountStatusDeleted); err != nil { t.Fatalf("update status: %v", err) } // The credential should still be retrievable (soft delete on account doesn't cascade). // But if we hard-delete via SQL, the FK cascade should clean up. // For now just verify the credential still exists after a status change. got, err := database.GetWebAuthnCredentialByID(id) if err != nil { t.Fatalf("get after account status change: %v", err) } if got.ID != id { t.Errorf("credential ID = %d, want %d", got.ID, id) } }