/** * credential-store-migration T6/T7(方案 A,D19)測試:auth-dispatcher 的 * `resolveSecretsFromNewHome`——「查 D1 拿 secret_ref → secret_get(env[ref]) 取值 → 組 map」。 * * 覆蓋 T6 主路徑 + T7 雙讀分流的判準: * - D1 有 ref 且 secret 存在(新家有值)→ name 進 map(payload 會含 resolved_secrets[name]) * - D1 無 ref(從未回填)→ name 缺席 map(讓 WASM fallback 舊 KV,T7) * - D1 有 ref 但 secret 不存在(新家還沒寫值)→ secret_get 回 null → name 缺席(不放空字串!) * - 取到值的 name 更新 last_used_at * * secret_get 的實作是 host function `env[ref]`(wasi-shim),故測試把 CRED_* 值放進傳入的 * env 物件模擬「per-script secret 已注入」,用真實 D1 binding 存目錄(同 credentials.test.ts)。 */ import { describe, it, expect, beforeEach } from 'vitest'; import { env } from 'cloudflare:test'; import type { Bindings } from '../src/types'; import { resolveSecretsFromNewHome } from '../src/actions/auth-dispatcher'; const API_KEY = 'test-tenant-t67'; async function insertRefRow(name: string, secretRef: string): Promise { await env.CREDENTIALS_DB .prepare( `INSERT INTO credentials (api_key, name, service, sensitivity, secret_ref, created_at, last_used_at) VALUES (?, ?, ?, ?, ?, ?, NULL)`, ) .bind(API_KEY, name, null, 'standard', secretRef, Math.floor(Date.now() / 1000)) .run(); } async function clearRows(): Promise { await env.CREDENTIALS_DB.prepare(`DELETE FROM credentials WHERE api_key = ?`).bind(API_KEY).run(); } /** 把 CRED_* secret 值疊到真實 env 上(模擬 per-script secret 已注入 runtime)。 */ function envWithSecrets(secrets: Record): Bindings { return { ...(env as unknown as Record), ...secrets } as unknown as Bindings; } describe('resolveSecretsFromNewHome (T6/T7 方案 A)', () => { beforeEach(clearRows); it('D1 有 ref 且新家有值 → name 進 map(T6 命中新家)', async () => { await insertRefRow('openai_key', 'CRED_OPENAI_KEY_ABCDEF01'); const testEnv = envWithSecrets({ CRED_OPENAI_KEY_ABCDEF01: 'sk-live-123' }); const resolved = await resolveSecretsFromNewHome(testEnv, API_KEY, ['openai_key']); expect(resolved).toEqual({ openai_key: 'sk-live-123' }); }); it('D1 無 ref(從未回填)→ name 缺席 map(走 WASM fallback 舊 KV,T7)', async () => { // D1 沒有這個 name 的 row const testEnv = envWithSecrets({ CRED_ANYTHING: 'unused' }); const resolved = await resolveSecretsFromNewHome(testEnv, API_KEY, ['never_migrated']); expect(resolved).not.toHaveProperty('never_migrated'); expect(resolved).toEqual({}); }); it('D1 有 ref 但新家沒值 → secret_get 回 null → name 缺席(不放空字串)', async () => { await insertRefRow('notion_token', 'CRED_NOTION_TOKEN_ABCDEF01'); // env 裡沒有 CRED_NOTION_TOKEN_ABCDEF01 → env[ref] 為 undefined → secret_get 回 null const testEnv = envWithSecrets({}); const resolved = await resolveSecretsFromNewHome(testEnv, API_KEY, ['notion_token']); // 關鍵:缺席,不是 { notion_token: '' }(空字串會讓 WASM 誤判命中用空值) expect(resolved).not.toHaveProperty('notion_token'); expect(resolved).toEqual({}); }); it('混合:一把有值一把沒 ref → 只有有值的進 map', async () => { await insertRefRow('has_value', 'CRED_HAS_VALUE_ABCDEF01'); const testEnv = envWithSecrets({ CRED_HAS_VALUE_ABCDEF01: 'the-value' }); const resolved = await resolveSecretsFromNewHome(testEnv, API_KEY, ['has_value', 'no_ref']); expect(resolved).toEqual({ has_value: 'the-value' }); }); it('空 names → 空 map(零開銷)', async () => { const resolved = await resolveSecretsFromNewHome(envWithSecrets({}), API_KEY, []); expect(resolved).toEqual({}); }); it('取到值後更新 last_used_at(治理面)', async () => { await insertRefRow('used_key', 'CRED_USED_KEY_ABCDEF01'); const before = await env.CREDENTIALS_DB .prepare(`SELECT last_used_at FROM credentials WHERE api_key = ? AND name = ?`) .bind(API_KEY, 'used_key') .first<{ last_used_at: number | null }>(); expect(before?.last_used_at).toBeNull(); const testEnv = envWithSecrets({ CRED_USED_KEY_ABCDEF01: 'v' }); await resolveSecretsFromNewHome(testEnv, API_KEY, ['used_key']); const after = await env.CREDENTIALS_DB .prepare(`SELECT last_used_at FROM credentials WHERE api_key = ? AND name = ?`) .bind(API_KEY, 'used_key') .first<{ last_used_at: number | null }>(); expect(typeof after?.last_used_at).toBe('number'); }); });