clavitor/clavis/clavis-vault/api/integration_test.go

730 lines
23 KiB
Go

package api
// Integration tests for the Clavitor vault API (rewrite).
//
// Uses a fake PRF to derive L0/L1/L2/L3 from a deterministic seed.
// Tests:
// - Full CRUD with legacy L1 bearer (vault owner)
// - CVT token agent auth with scoped access
// - Agent creation, scope assignment, deletion
// - Key leak detection: L1, L2, L3 never appear in API responses
// - Agents cannot manage other agents or scopes
// - all_access agents see everything
// - Owner-only (0000 scope) entries invisible to scoped agents
import (
"bytes"
"embed"
"encoding/base64"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/johanj/clavitor/lib"
)
// ---------------------------------------------------------------------------
// Fake PRF — deterministic key hierarchy for testing
// ---------------------------------------------------------------------------
// fakePRF simulates the PRF output (32 bytes) from which all tiers derive.
// In production this comes from WebAuthn PRF extension.
var fakePRF = []byte{
0xAA, 0xBB, 0xCC, 0xDD, // L0 (4 bytes)
0x11, 0x22, 0x33, 0x44, // L1 = L0 + these 4 bytes (8 total)
0x55, 0x66, 0x77, 0x88, 0x99, 0xA0, 0xB1, 0xC2, // L2 = L1 + these 8 bytes (16 total)
0xD3, 0xE4, 0xF5, 0x06, 0x17, 0x28, 0x39, 0x4A, // L3 = L2 + these 8 bytes (24 total)
0x5B, 0x6C, 0x7D, 0x8E, 0x9F, 0xA0, 0xB1, 0xC2, // L3 continued (32 total)
}
func fakeL0() []byte { return fakePRF[:4] }
func fakeL1() []byte { return fakePRF[:8] }
func fakeL2() []byte { return fakePRF[:16] }
func fakeL3() []byte { return fakePRF[:32] }
// ---------------------------------------------------------------------------
// Test client
// ---------------------------------------------------------------------------
type tc struct {
srv *httptest.Server
bearer string // base64url-encoded L1 key (8 bytes) for legacy auth
t *testing.T
}
func newTestClient(t *testing.T) *tc {
t.Helper()
tmpDir := t.TempDir()
cfg := &lib.Config{Port: "0", DataDir: tmpDir, SessionTTL: 86400}
l1Raw := fakeL1()
bearer := base64.RawURLEncoding.EncodeToString(l1Raw)
// Create vault DB matching L1Middleware's naming convention
prefix := base64.RawURLEncoding.EncodeToString(l1Raw[:4])
dbPath := tmpDir + "/clavitor-" + prefix
db, err := lib.OpenDB(dbPath)
if err != nil {
t.Fatalf("opendb: %v", err)
}
if err := lib.InitSchema(db); err != nil {
t.Fatalf("init schema: %v", err)
}
db.Close()
var emptyFS embed.FS
srv := httptest.NewServer(NewRouter(cfg, emptyFS))
t.Cleanup(srv.Close)
return &tc{srv: srv, bearer: bearer, t: t}
}
// req sends a legacy L1 bearer request (vault owner).
func (c *tc) req(method, path string, body any) *http.Response {
c.t.Helper()
var r io.Reader
if body != nil {
b, _ := json.Marshal(body)
r = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, c.srv.URL+path, r)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("Authorization", "Bearer "+c.bearer)
resp, err := c.srv.Client().Do(req)
if err != nil {
c.t.Fatalf("req %s %s: %v", method, path, err)
}
return resp
}
// reqNoAuth sends an unauthenticated request.
func (c *tc) reqNoAuth(method, path string, body any) *http.Response {
c.t.Helper()
var r io.Reader
if body != nil {
b, _ := json.Marshal(body)
r = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, c.srv.URL+path, r)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := c.srv.Client().Do(req)
if err != nil {
c.t.Fatalf("req %s %s: %v", method, path, err)
}
return resp
}
// reqAgent sends a request with CVT wire token authentication (agent).
// The wireToken is a type 0x00 CVT token containing L1 + agent_id.
func (c *tc) reqAgent(method, path string, body any, wireToken string) *http.Response {
c.t.Helper()
var r io.Reader
if body != nil {
b, _ := json.Marshal(body)
r = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, c.srv.URL+path, r)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
req.Header.Set("Authorization", "Bearer "+wireToken)
resp, err := c.srv.Client().Do(req)
if err != nil {
c.t.Fatalf("reqAgent %s %s: %v", method, path, err)
}
return resp
}
// mintWireToken creates a CVT wire token (type 0x00) for agent authentication.
func (c *tc) mintWireToken(agentID []byte) string {
c.t.Helper()
token, err := lib.MintWireToken(fakeL0(), fakeL1(), agentID)
if err != nil {
c.t.Fatalf("mintWireToken: %v", err)
}
return token
}
// must asserts status code and returns parsed JSON object.
func (c *tc) must(resp *http.Response, wantStatus int) map[string]any {
c.t.Helper()
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != wantStatus {
c.t.Fatalf("expected %d, got %d: %s", wantStatus, resp.StatusCode, body)
}
var out map[string]any
json.Unmarshal(body, &out)
return out
}
// mustList asserts status and returns JSON array.
func (c *tc) mustList(resp *http.Response, wantStatus int) []map[string]any {
c.t.Helper()
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != wantStatus {
c.t.Fatalf("expected %d, got %d: %s", wantStatus, resp.StatusCode, body)
}
var out []map[string]any
json.Unmarshal(body, &out)
return out
}
// mustRaw asserts status and returns raw body bytes.
func (c *tc) mustRaw(resp *http.Response, wantStatus int) []byte {
c.t.Helper()
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != wantStatus {
c.t.Fatalf("expected %d, got %d: %s", wantStatus, resp.StatusCode, body)
}
return body
}
// createAgent creates an agent via owner API, returns the agent_id.
// NOTE: Credential is now generated client-side by web UI using crypto.js
func (c *tc) createAgent(name string, allAccess bool) string {
c.t.Helper()
resp := c.req("POST", "/api/agents", map[string]any{
"name": name, "all_access": allAccess,
})
result := c.must(resp, 201)
agentID, ok := result["agent_id"].(string)
if !ok || agentID == "" {
c.t.Fatalf("createAgent: no agent_id in response: %v", result)
}
return agentID
}
// ---------------------------------------------------------------------------
// Test data
// ---------------------------------------------------------------------------
func credentialEntry(title, username, password string, urls []string) map[string]any {
return map[string]any{
"title": title, "type": "credential",
"data": map[string]any{
"title": title, "type": "credential",
"fields": []map[string]any{
{"label": "username", "value": username, "kind": "text"},
{"label": "password", "value": password, "kind": "password"},
},
"urls": urls,
},
}
}
// ---------------------------------------------------------------------------
// Health & Ping
// ---------------------------------------------------------------------------
func TestHealth(t *testing.T) {
c := newTestClient(t)
result := c.must(c.req("GET", "/health", nil), 200)
if result["status"] != "ok" {
t.Fatal("health not ok")
}
}
func TestPing(t *testing.T) {
c := newTestClient(t)
result := c.must(c.reqNoAuth("GET", "/ping", nil), 200)
if result["ok"] != true {
t.Fatal("ping not ok")
}
}
// ---------------------------------------------------------------------------
// Entry CRUD (legacy L1 bearer = vault owner)
// ---------------------------------------------------------------------------
func TestCreateEntry(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
entry := c.must(c.req("POST", "/api/entries", credentialEntry("GitHub", "octocat", "pass123", []string{"https://github.com"})), 201)
if entry["title"] != "GitHub" {
t.Fatalf("title = %v", entry["title"])
}
if entry["scopes"] != "0000" {
t.Fatalf("default scope should be 0000, got %v", entry["scopes"])
}
}
func TestGetEntry(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
created := c.must(c.req("POST", "/api/entries", credentialEntry("Test", "user", "pass", nil)), 201)
id := created["entry_id"].(string)
got := c.must(c.req("GET", "/api/entries/"+id, nil), 200)
if got["title"] != "Test" {
t.Fatalf("title = %v", got["title"])
}
}
func TestListEntries(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
c.must(c.req("POST", "/api/entries", credentialEntry("A", "u", "p", nil)), 201)
c.must(c.req("POST", "/api/entries", credentialEntry("B", "u", "p", nil)), 201)
list := c.mustList(c.req("GET", "/api/entries", nil), 200)
if len(list) != 2 {
t.Fatalf("expected 2 entries, got %d", len(list))
}
}
func TestDeleteEntry(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
created := c.must(c.req("POST", "/api/entries", credentialEntry("Del", "u", "p", nil)), 201)
id := created["entry_id"].(string)
c.must(c.req("DELETE", "/api/entries/"+id, nil), 200)
// Should be gone
list := c.mustList(c.req("GET", "/api/entries", nil), 200)
if len(list) != 0 {
t.Fatalf("expected 0, got %d", len(list))
}
}
func TestUpsert_creates_when_new(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
c.must(c.req("PUT", "/api/entries", credentialEntry("Fresh", "u", "p", nil)), 201)
}
func TestUpsert_updates_when_exists(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
c.must(c.req("PUT", "/api/entries", credentialEntry("Same", "u1", "p1", nil)), 201)
c.must(c.req("PUT", "/api/entries", credentialEntry("Same", "u2", "p2", nil)), 200) // update, not create
list := c.mustList(c.req("GET", "/api/entries", nil), 200)
if len(list) != 1 {
t.Fatalf("expected 1, got %d", len(list))
}
}
func TestSearch(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
c.must(c.req("POST", "/api/entries", credentialEntry("GitHub Token", "u", "p", nil)), 201)
c.must(c.req("POST", "/api/entries", credentialEntry("AWS Key", "u", "p", nil)), 201)
results := c.mustList(c.req("GET", "/api/search?q=GitHub", nil), 200)
if len(results) != 1 {
t.Fatalf("expected 1 search result, got %d", len(results))
}
}
func TestPasswordGenerator(t *testing.T) {
c := newTestClient(t)
result := c.must(c.req("GET", "/api/generate", nil), 200)
pw := result["password"].(string)
if len(pw) < 10 {
t.Fatalf("password too short: %s", pw)
}
}
func TestAuditLog(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
c.must(c.req("POST", "/api/entries", credentialEntry("Audit Test", "u", "p", nil)), 201)
events := c.mustList(c.req("GET", "/api/audit", nil), 200)
if len(events) < 1 {
t.Fatal("expected at least 1 audit event")
}
}
// ---------------------------------------------------------------------------
// Scoped Access
// ---------------------------------------------------------------------------
func TestScopedAccess_agent_sees_only_scoped_entries(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
// Create entries with different scopes
c.must(c.req("POST", "/api/entries", credentialEntry("Work Entry", "user", "pass", nil)), 201)
c.must(c.req("POST", "/api/entries", credentialEntry("Personal Entry", "user", "pass", nil)), 201)
// Create agent with specific scope (work only)
agentResp := c.must(c.req("POST", "/api/agents", map[string]any{
"name": "Work Agent",
"all_access": false,
"scope_whitelist": []string{"work"},
}), 201)
agentIDHex := agentResp["agent_id"].(string)
agentID, _ := hex.DecodeString(agentIDHex)
// Mint wire token for agent authentication
wireToken := c.mintWireToken(agentID)
// Agent with work scope should see entries (scope filtering happens at data level)
// With empty scope_whitelist, agent sees nothing
// With ["work"], agent sees work entries
list := c.mustList(c.reqAgent("GET", "/api/entries", nil, wireToken), 200)
// Agent with work scope should see entries (default scope is "0000" which is owner-only)
// Since entries have no explicit scope, they default to "0000" (owner-only)
// So agent should see 0 entries
if len(list) != 0 {
t.Fatalf("agent with work scope should see 0 owner-scoped entries, got %d", len(list))
}
}
func TestScopedAccess_agent_forbidden_on_unscoped(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
// Create an entry
entry := c.must(c.req("POST", "/api/entries", credentialEntry("Test Entry", "user", "pass", nil)), 201)
entryID := entry["entry_id"].(string)
// Create agent with no scope whitelist (cannot access owner-only entries)
agentResp := c.must(c.req("POST", "/api/agents", map[string]any{
"name": "Limited Agent",
"all_access": false,
"scope_whitelist": []string{}, // Empty whitelist
}), 201)
agentIDHex := agentResp["agent_id"].(string)
agentID, _ := hex.DecodeString(agentIDHex)
// Mint wire token for agent authentication
wireToken := c.mintWireToken(agentID)
// Agent should get 403 when trying to access owner-only entry
resp := c.reqAgent("GET", "/api/entries/"+entryID, nil, wireToken)
if resp.StatusCode != 403 {
t.Fatalf("agent without scope access should get 403, got %d", resp.StatusCode)
}
resp.Body.Close()
}
func TestScopedAccess_all_access_sees_everything(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
// Create entries
c.must(c.req("POST", "/api/entries", credentialEntry("Entry 1", "user", "pass", nil)), 201)
c.must(c.req("POST", "/api/entries", credentialEntry("Entry 2", "user", "pass", nil)), 201)
// Create agent with all_access flag
agentResp := c.must(c.req("POST", "/api/agents", map[string]any{
"name": "All Access Agent",
"all_access": true,
}), 201)
agentIDHex := agentResp["agent_id"].(string)
agentID, _ := hex.DecodeString(agentIDHex)
// Mint wire token for agent authentication
wireToken := c.mintWireToken(agentID)
// Agent with all_access should see all entries
list := c.mustList(c.reqAgent("GET", "/api/entries", nil, wireToken), 200)
if len(list) != 2 {
t.Fatalf("all_access agent should see 2 entries, got %d", len(list))
}
}
func TestScopedAccess_agent_cannot_manage_agents(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
// Create agent
agentResp := c.must(c.req("POST", "/api/agents", map[string]any{
"name": "Regular Agent",
"all_access": false,
}), 201)
agentIDHex := agentResp["agent_id"].(string)
agentID, _ := hex.DecodeString(agentIDHex)
// Mint wire token for agent authentication
wireToken := c.mintWireToken(agentID)
// Agent should get 403 when trying to create another agent
resp := c.reqAgent("POST", "/api/agents", map[string]any{
"name": "New Agent",
"all_access": false,
}, wireToken)
if resp.StatusCode != 403 {
t.Fatalf("agent should not be able to create agents, expected 403 got %d", resp.StatusCode)
}
resp.Body.Close()
// Agent should get 403 when trying to list agents
resp = c.reqAgent("GET", "/api/agents", nil, wireToken)
if resp.StatusCode != 403 {
t.Fatalf("agent should not be able to list agents, expected 403 got %d", resp.StatusCode)
}
resp.Body.Close()
}
func TestScopedAccess_agent_cannot_create_system_types(t *testing.T) {
// Agents cannot create entries with system types (agent, scope)
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
// Create agent
agentResp := c.must(c.req("POST", "/api/agents", map[string]any{
"name": "Regular Agent",
"all_access": false,
}), 201)
agentIDHex := agentResp["agent_id"].(string)
agentID, _ := hex.DecodeString(agentIDHex)
// Mint wire token for agent authentication
wireToken := c.mintWireToken(agentID)
// Agent should get 403 when trying to create entry with type=agent
resp := c.reqAgent("POST", "/api/entries", map[string]any{
"title": "Fake Agent",
"type": "agent",
"data": map[string]any{
"title": "Fake Agent",
"type": "agent",
},
}, wireToken)
if resp.StatusCode != 403 {
t.Fatalf("agent should not be able to create agent-type entries, expected 403 got %d", resp.StatusCode)
}
resp.Body.Close()
// Agent should get 403 when trying to create entry with type=scope
resp = c.reqAgent("POST", "/api/entries", map[string]any{
"title": "Fake Scope",
"type": "scope",
"data": map[string]any{
"title": "Fake Scope",
"type": "scope",
},
}, wireToken)
if resp.StatusCode != 403 {
t.Fatalf("agent should not be able to create scope-type entries, expected 403 got %d", resp.StatusCode)
}
resp.Body.Close()
}
func TestScopedAccess_agent_entries_invisible(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
// Create entries
c.must(c.req("POST", "/api/entries", credentialEntry("Entry 1", "user1", "pass1", nil)), 201)
c.must(c.req("POST", "/api/entries", credentialEntry("Entry 2", "user2", "pass2", nil)), 201)
// Create agent with limited scope
agentResp := c.must(c.req("POST", "/api/agents", map[string]any{
"name": "Limited Agent",
"all_access": false,
"scope_whitelist": []string{"work"},
}), 201)
agentIDHex := agentResp["agent_id"].(string)
agentID, _ := hex.DecodeString(agentIDHex)
// Mint wire token for agent authentication
wireToken := c.mintWireToken(agentID)
// Agent with work scope should not see owner-only entries (default scope "0000")
list := c.mustList(c.reqAgent("GET", "/api/entries", nil, wireToken), 200)
// Entries default to "0000" scope (owner-only), so agent sees nothing
if len(list) != 0 {
t.Fatalf("agent with work scope should see 0 owner-only entries, got %d", len(list))
}
}
// ---------------------------------------------------------------------------
// Key Leak Detection
// ---------------------------------------------------------------------------
func TestKeyLeak_L1_not_in_responses(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
c.must(c.req("POST", "/api/entries", credentialEntry("Leak Test", "u", "p", nil)), 201)
l1Hex := hex.EncodeToString(fakeL1())
l1B64 := base64.StdEncoding.EncodeToString(fakeL1())
// Check list response
body := c.mustRaw(c.req("GET", "/api/entries", nil), 200)
bodyStr := string(body)
if strings.Contains(bodyStr, l1Hex) {
t.Fatal("L1 key (hex) found in entries response")
}
if strings.Contains(bodyStr, l1B64) {
t.Fatal("L1 key (b64) found in entries response")
}
}
func TestKeyLeak_L2_not_in_responses(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
// Create agent — response includes credential token (which contains L2 encrypted)
// but should NOT contain raw L2
l2Hex := hex.EncodeToString(fakeL2())
l2B64 := base64.StdEncoding.EncodeToString(fakeL2())
body := c.mustRaw(c.req("POST", "/api/agents", map[string]any{
"name": "Leak Check Agent",
}), 201)
bodyStr := string(body)
if strings.Contains(bodyStr, l2Hex) {
t.Fatal("L2 key (hex) found in agent creation response")
}
if strings.Contains(bodyStr, l2B64) {
t.Fatal("L2 key (b64) found in agent creation response")
}
}
func TestKeyLeak_L3_never_appears(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
c.must(c.req("POST", "/api/entries", credentialEntry("L3 Test", "u", "p", nil)), 201)
c.createAgent("L3 Agent", false)
l3Hex := hex.EncodeToString(fakeL3())
l3B64 := base64.StdEncoding.EncodeToString(fakeL3())
// Check multiple endpoints
endpoints := []struct{ method, path string }{
{"GET", "/api/entries"},
{"GET", "/api/agents"},
{"GET", "/api/audit"},
{"GET", "/health"},
}
for _, ep := range endpoints {
body := c.mustRaw(c.req(ep.method, ep.path, nil), 200)
bodyStr := string(body)
if strings.Contains(bodyStr, l3Hex) {
t.Fatalf("L3 key (hex) found in %s %s", ep.method, ep.path)
}
if strings.Contains(bodyStr, l3B64) {
t.Fatalf("L3 key (b64) found in %s %s", ep.method, ep.path)
}
}
}
func TestKeyLeak_agent_credential_is_opaque(t *testing.T) {
// CREDENTIAL GENERATION IS NOW CLIENT-SIDE
// Server returns agent_id only, web UI generates credential using crypto.js
// This test verifies that the server response contains no key material
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
// Create agent - server returns only agent_id, no credential
body := c.mustRaw(c.req("POST", "/api/agents", map[string]any{
"name": "Leak Check Agent",
}), 201)
bodyStr := string(body)
// Verify response contains agent_id but no credential token
if !strings.Contains(bodyStr, "agent_id") {
t.Fatal("agent creation response should contain agent_id")
}
// Response should NOT contain cvt_ prefix (credential tokens start with cvt_)
if strings.Contains(bodyStr, "cvt_") {
t.Fatal("agent creation response should not contain cvt_ credential (now client-side)")
}
// Verify no raw L2 in response
l2Hex := hex.EncodeToString(fakeL2())
if strings.Contains(bodyStr, l2Hex) {
t.Fatal("L2 key (hex) found in agent creation response")
}
}
// ---------------------------------------------------------------------------
// TOTP
// ---------------------------------------------------------------------------
func TestTOTP_valid_code(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
entry := c.must(c.req("POST", "/api/entries", map[string]any{
"title": "TOTP Test", "type": "credential",
"data": map[string]any{
"title": "TOTP Test", "type": "credential",
"fields": []map[string]any{
{"label": "TOTP", "value": "JBSWY3DPEHPK3PXP", "kind": "totp"},
},
},
}), 201)
id := entry["entry_id"].(string)
result := c.must(c.req("GET", "/api/ext/totp/"+id, nil), 200)
code := result["code"].(string)
if len(code) != 6 {
t.Fatalf("expected 6-digit code, got %s", code)
}
}
func TestURLMatch(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
c.must(c.req("POST", "/api/entries", credentialEntry("GitHub", "octocat", "p", []string{"https://github.com"})), 201)
results := c.mustList(c.req("GET", "/api/ext/match?url=https://github.com/login", nil), 200)
if len(results) != 1 {
t.Fatalf("expected 1 match, got %d", len(results))
}
}
// ---------------------------------------------------------------------------
// Auth endpoints
// ---------------------------------------------------------------------------
func TestAuthStatus_fresh(t *testing.T) {
c := newTestClient(t)
result := c.must(c.reqNoAuth("GET", "/api/auth/status", nil), 200)
// Without authentication, we cannot determine vault state (multi-vault hosted mode)
if result["state"] != "auth_required" {
t.Fatalf("expected auth_required, got %v", result["state"])
}
}
func TestAuthRegisterBegin(t *testing.T) {
c := newTestClient(t)
result := c.must(c.reqNoAuth("POST", "/api/auth/register/begin", nil), 200)
pk := result["publicKey"].(map[string]any)
if pk["challenge"] == nil {
t.Fatal("no challenge in response")
}
}
// ---------------------------------------------------------------------------
// Metadata list
// ---------------------------------------------------------------------------
func TestListEntries_meta(t *testing.T) {
c := newTestClient(t)
c.must(c.req("POST", "/api/auth/setup", nil), 200)
c.must(c.req("POST", "/api/entries", credentialEntry("Meta A", "u", "p", nil)), 201)
c.must(c.req("POST", "/api/entries", credentialEntry("Meta B", "u", "p", nil)), 201)
list := c.mustList(c.req("GET", "/api/entries?meta=1", nil), 200)
if len(list) != 2 {
t.Fatalf("expected 2, got %d", len(list))
}
// Meta mode should not include VaultData
if list[0]["data"] != nil {
t.Fatal("meta mode should not include data")
}
}