/** * console-auth.ts —— D61 舊實例相容(帳密只在舊 SESSIONS_KV,尚未搬遷過) * * 拆成獨立檔案的理由:portal-auth-store.ts 的 per-isolate overlay 是模組級全域變數, * 一旦某個測試讓 console 帳密的認證儲存寫入成功,overlay.console 就會在**同一支測試檔案** * 剩下的測試裡持續存在(不同檔案=不同 worker 執行個體,互不污染,已用小型探針驗證過)。 * tests/console-auth.test.ts 一開始就會走一次「首次設定成功」,之後整支檔案都是「已設定」 * 的世界;「認證儲存還是空的、帳密只活在舊 KV」這個起始狀態只有在全新檔案才測得出來。 */ import { SELF, env, fetchMock } from 'cloudflare:test'; import { beforeAll, afterEach, describe, it, expect } from 'vitest'; const CF_API = 'https://api.cloudflare.com'; const CREDS_KEY = 'console:credentials'; beforeAll(() => { fetchMock.activate(); fetchMock.disableNetConnect(); }); afterEach(() => fetchMock.assertNoPendingInterceptors()); function json(method: string, path: string, body?: unknown) { return SELF.fetch(`http://localhost${path}`, { method, headers: { 'Content-Type': 'application/json' }, body: body === undefined ? undefined : JSON.stringify(body), }); } function mockAuthStoreWrite(times = 1): { puts: () => Array<{ name: string; text: string }> } { const captured: Array<{ name: string; text: string }> = []; fetchMock .get(CF_API) .intercept({ path: (p: string) => p.includes('/secrets'), method: 'PUT' }) .reply(200, (opts) => { const body = JSON.parse(String(opts.body)) as { name: string; text: string }; captured.push(body); return { success: true }; }) .times(times); return { puts: () => captured }; } /** 複刻 console-auth.ts 內未 export 的私有迭代雜湊(sha256(salt+password) 迭代 3 次), * 單純為了在測試端準備一筆能通過驗證的 legacy fixture,不是重新實作生產邏輯。 */ async function legacyHash(password: string, salt: string): Promise { async function sha256Hex(input: string): Promise { const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(input)); return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, '0')).join(''); } let h = `${salt}:${password}`; for (let i = 0; i < 3; i++) h = await sha256Hex(h); return h; } const EMAIL = 'legacy-owner@example.com'; const PASSWORD = 'legacy-owner-pw-1'; const SALT = 'deadbeef00112233'; describe('D61 舊實例相容:console 帳密只在舊 KV(尚未搬遷)', () => { it('GET /console/auth-status:讀到舊 KV 這筆、順手搬進認證儲存', async () => { const hash = await legacyHash(PASSWORD, SALT); await env.SESSIONS_KV.put( CREDS_KEY, JSON.stringify({ email: EMAIL, salt: SALT, hash, created_at: '2026-01-01T00:00:00.000Z' }), ); const { puts } = mockAuthStoreWrite(); const res = await json('GET', '/console/auth-status'); expect(res.status).toBe(200); const data = (await res.json()) as { configured: boolean; credentials_source: string; auth_store: { console_configured: boolean }; }; expect(data.configured).toBe(true); expect(data.credentials_source).toBe('legacy-kv'); // 這次是靠回退讀到的 // loadCredentials 內的 best-effort 搬遷在回應組出來之前就已 await 完成, // 故 authStoreStatus 已經反映搬遷後的狀態 expect(data.auth_store.console_configured).toBe(true); const shards = puts(); expect(shards.length).toBe(1); const shard = JSON.parse(shards[0].text) as { console: { email: string; hash: string } }; expect(shard.console.email).toBe(EMAIL); expect(shard.console.hash).toBe(hash); // 原樣搬過去,不重新雜湊 }); it('搬遷後再打一次:新家已經有了,直接命中新家(不用再查舊 KV)', async () => { const res = await json('GET', '/console/auth-status'); const data = (await res.json()) as { credentials_source: string }; expect(data.credentials_source).toBe('secrets'); }); it('用搬遷過去的帳密登入 → 200(搬遷沒有讓帳密變得登不進去)', async () => { const res = await json('POST', '/console/login', { email: EMAIL, password: PASSWORD }); expect(res.status).toBe(200); const data = (await res.json()) as { success: boolean }; expect(data.success).toBe(true); }); });