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>
252 lines
6.8 KiB
Go
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)
|
|
}
|
|
}
|