diff --git a/cinny b/cinny index 4120ccd..5f3c5d3 160000 --- a/cinny +++ b/cinny @@ -1 +1 @@ -Subproject commit 4120ccdbcc24975d0bba259d55f48aa578c866e9 +Subproject commit 5f3c5d370ad438e37cd815c609e9967e12b303a5 diff --git a/scripts/windows-smoke.mjs b/scripts/windows-smoke.mjs index c4aa55c..c2b5636 100644 --- a/scripts/windows-smoke.mjs +++ b/scripts/windows-smoke.mjs @@ -375,6 +375,71 @@ record( }; app still responsive: ${alive}`, ); +// 11. Login tokens only in the keychain (cinny #105 step 2). Simulate an +// install from before step 2: a full session in localStorage, pointed at an +// unreachable server so nothing ever rejects (and logs out) the fake token. +// After a reload the tokens must have moved: localStorage keeps a token-less +// shell, Credential Manager holds them; after another reload the app is still +// signed in. Builds without step 2 only mirror (tokens stay): n/a. +{ + const name = 'login tokens move to the keychain and stay signed in (#105)'; + const KEY = 'cinny_session_v1'; + const fake = { + accessToken: `syt_smoke_${Date.now()}`, + refreshToken: 'mar_smoke', + deviceId: 'SMOKEDEV', + userId: '@smoke:ci.invalid', + baseUrl: 'https://127.0.0.1:9', + }; + const state = () => + page.evaluate(async (k) => { + const blob = JSON.parse(localStorage.getItem(k) ?? 'null'); + const kc = await window.__TAURI_INTERNALS__.invoke('secure_session_get').catch(() => null); + return { + tokensIn: blob?.tokensIn, + blobHasToken: typeof blob?.accessToken === 'string', + legacyToken: localStorage.getItem('cinny_access_token') !== null, + kcToken: kc?.accessToken ?? null, + loginScreen: /Username or email/.test(document.body.innerText), + }; + }, KEY); + try { + await page.evaluate(([k, f]) => localStorage.setItem(k, JSON.stringify(f)), [KEY, fake]); + await page.reload(); + const moved = await until(async () => { + const st = await state(); + return st.tokensIn === 'keychain' || (st.blobHasToken && st.kcToken === fake.accessToken) ? st : undefined; + }, 30_000); + if (moved && moved.tokensIn !== 'keychain') { + record(name, 'n/a', 'build mirrors the tokens but keeps them in localStorage (before step 2)'); + } else { + await page.reload(); + await new Promise((r) => setTimeout(r, 8000)); + const after = await state(); + const ok = + moved && + !moved.blobHasToken && + !moved.legacyToken && + moved.kcToken === fake.accessToken && + after.tokensIn === 'keychain' && + after.kcToken === fake.accessToken && + !after.loginScreen; + record(name, ok ? 'pass' : 'fail', `after migration: ${JSON.stringify(moved)}; after restart: ${JSON.stringify(after)}`); + } + } catch (e) { + record(name, 'fail', String(e).slice(0, 200)); + } finally { + // Leave the runner as we found it: no session, no keychain entry. + await page + .evaluate(async (k) => { + localStorage.removeItem(k); + await window.__TAURI_INTERNALS__.invoke('secure_session_clear').catch(() => undefined); + }, KEY) + .catch(() => undefined); + await page.reload().catch(() => undefined); + } +} + await page.screenshot({ path: `${OUT}/02-end.png` }).catch(() => undefined); writeFileSync(`${OUT}/results.json`, JSON.stringify(results, null, 2));