Files
mcias/internal/db/webauthn_test.go
Kyle Isom 41d01edfb4 Migrate module path from kyle/ to mc/ org
All import paths updated from git.wntrmute.dev/kyle/mcias to
git.wntrmute.dev/mc/mcias to match the Gitea organization.
Includes main module and clients/go submodule.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 02:03:46 -07:00

252 lines
6.8 KiB
Go

package db
import (
"errors"
"testing"
"git.wntrmute.dev/mc/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)
}
}