/** * credential 治理端點測試。 * * 範圍限制(誠實記錄,非本檔缺陷):`putWorkerSecret` / `deleteWorkerSecret` 呼叫真實 * Cloudflare API(`fetch` 到 api.cloudflare.com)。測試環境(wrangler.test.toml)刻意不設 * CF_SECRETS_API_TOKEN/CF_ACCOUNT_ID,所以本檔只覆蓋「不需要真的打 CF API」的路徑: * - D1-only 的 GET /credentials、/credentials/catalog * - DELETE 在 D1 無 row 時 fallback 刪舊 KV(不會走到 deleteWorkerSecret) * 真正打 CF Workers Secrets API 成功寫入/刪除的路徑,由部署到 leo21c 帳號後的端到端 * curl 驗證覆蓋(見 credential-store-migration.md T8/T9 完成記錄)。 */ import { describe, it, expect, beforeEach } from 'vitest'; import { env, SELF } from 'cloudflare:test'; const API_KEY = 'test-tenant-t89'; async function insertCredentialRow( name: string, secretRef: string, extra: Partial<{ service: string | null; sensitivity: string; last_used_at: number | null }> = {}, ): Promise { await env.CREDENTIALS_DB .prepare( `INSERT INTO credentials (api_key, name, service, sensitivity, secret_ref, created_at, last_used_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, ) .bind( API_KEY, name, extra.service ?? null, extra.sensitivity ?? 'standard', secretRef, Math.floor(Date.now() / 1000), extra.last_used_at ?? null, ) .run(); } async function clearTenantRows(): Promise { await env.CREDENTIALS_DB.prepare(`DELETE FROM credentials WHERE api_key = ?`).bind(API_KEY).run(); } describe('GET /credentials (D1, T9)', () => { beforeEach(clearTenantRows); it('缺 X-Arcrun-API-Key → 401', async () => { const res = await SELF.fetch('https://cypher.test/credentials'); expect(res.status).toBe(401); }); it('無資料 → 空陣列(非拋錯)', async () => { const res = await SELF.fetch('https://cypher.test/credentials', { headers: { 'X-Arcrun-API-Key': API_KEY }, }); expect(res.status).toBe(200); const body = await res.json() as { success: boolean; credentials: unknown[]; total: number }; expect(body.success).toBe(true); expect(body.credentials).toEqual([]); expect(body.total).toBe(0); }); it('回傳 metadata,絕不含 secret_ref 或值', async () => { await insertCredentialRow('telegram_bot_token', 'CRED_TELEGRAM_BOT_TOKEN_ABCDEF01', { service: 'telegram' }); const res = await SELF.fetch('https://cypher.test/credentials', { headers: { 'X-Arcrun-API-Key': API_KEY }, }); const body = await res.json() as { success: boolean; credentials: Array> }; expect(body.success).toBe(true); expect(body.credentials).toHaveLength(1); const row = body.credentials[0]; expect(row.name).toBe('telegram_bot_token'); expect(row.service).toBe('telegram'); expect(row).not.toHaveProperty('secret_ref'); expect(row).not.toHaveProperty('value'); expect(JSON.stringify(row)).not.toMatch(/CRED_/); }); it('/credentials/catalog 回同一份資料(Console 相容別名)', async () => { await insertCredentialRow('notion_token', 'CRED_NOTION_TOKEN_ABCDEF01'); const [listRes, catalogRes] = await Promise.all([ SELF.fetch('https://cypher.test/credentials', { headers: { 'X-Arcrun-API-Key': API_KEY } }), SELF.fetch('https://cypher.test/credentials/catalog', { headers: { 'X-Arcrun-API-Key': API_KEY } }), ]); const [listBody, catalogBody] = await Promise.all([listRes.json(), catalogRes.json()]) as Array<{ credentials: Array<{ name: string }>; }>; expect(listBody.credentials.map(r => r.name)).toEqual(catalogBody.credentials.map(r => r.name)); }); }); describe('DELETE /credentials/:name (T9)', () => { beforeEach(clearTenantRows); it('D1 無 row(從未回填)→ fallback 刪舊 KV,不誤報找不到', async () => { await env.CREDENTIALS_KV.put( `${API_KEY}:cred:legacy_only`, JSON.stringify({ encrypted: 'x', iv: 'y' }), ); const res = await SELF.fetch('https://cypher.test/credentials/legacy_only', { method: 'DELETE', headers: { 'X-Arcrun-API-Key': API_KEY }, }); const body = await res.json() as { success: boolean; source: string }; expect(res.status).toBe(200); expect(body.success).toBe(true); expect(body.source).toBe('legacy-kv'); const raw = await env.CREDENTIALS_KV.get(`${API_KEY}:cred:legacy_only`); expect(raw).toBeNull(); }); });