From a80df869a92c330fbad9695057720b11a18af901 Mon Sep 17 00:00:00 2001 From: Doan Luu Duc Tai Date: Sat, 31 Oct 2026 13:08:08 -0500 Subject: Forbid Rust lints and enforce strict typed TypeScript linting --- crates/chat-server/tests/browser.ts | 104 +++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 49 deletions(-) (limited to 'crates/chat-server/tests/browser.ts') diff --git a/crates/chat-server/tests/browser.ts b/crates/chat-server/tests/browser.ts index 74a9df2..468a5f5 100644 --- a/crates/chat-server/tests/browser.ts +++ b/crates/chat-server/tests/browser.ts @@ -19,20 +19,20 @@ import { mkdtemp, readFile, writeFile, symlink, rm, mkdir, rename, stat, readdir import { tmpdir } from 'node:os'; import { resolve, join } from 'node:path'; import { randomBytes } from 'node:crypto'; -import { spawn, execFileSync, spawnSync, execFile } from 'node:child_process'; +import { spawn, execFileSync, spawnSync, execFile, type SpawnSyncReturns } from 'node:child_process'; import { promisify } from 'node:util'; import { chromium, firefox, type Browser, type BrowserContext, type Page } from 'playwright'; -import { libreJSAddon, list, record, requestBody, required, serverOrigin, string } from '../../../tests/browser-support.js'; +import { closeBrowser, libreJSAddon, list, record, requestBody, required, serverOrigin, string } from '../../../tests/browser-support.js'; const run = promisify(execFile); const testRoot = process.env['CHAT_TEST_ROOT']; const packaged = Boolean(testRoot); -let root = testRoot ? resolve(testRoot) : await mkdtemp(join(tmpdir(), 'stack-chat-e2e-')); +let root = testRoot !== undefined && testRoot.length > 0 ? resolve(testRoot) : await mkdtemp(join(tmpdir(), 'stack-chat-e2e-')); const initialRoot = root; const token = packaged - ? (await readFile(join(root,'.env'),'utf8')).split('\n').find(line => line.startsWith('CHAT_SETUP_TOKEN='))?.slice('CHAT_SETUP_TOKEN='.length) || '' + ? ((await readFile(join(root,'.env'),'utf8')).split('\n').find(line => line.startsWith('CHAT_SETUP_TOKEN='))?.slice('CHAT_SETUP_TOKEN='.length) ?? '') : randomBytes(32).toString('hex'); -const origin = process.env['CHAT_TEST_ORIGIN'] || `http://127.0.0.1:${process.env['CHAT_TEST_PORT'] || '9081'}`; +const origin = (process.env['CHAT_TEST_ORIGIN'] ?? `http://127.0.0.1:${(process.env['CHAT_TEST_PORT'] ?? '9081')}`); if (packaged) { const fixture = await readFile(join(root,'.env'),'utf8'); const address = new URL(origin); @@ -53,32 +53,32 @@ if (packaged) { await mkdir(join(root,'var'), {recursive:true}); await symlink(resolve('var/build'),join(root,'var/build')); } -async function stack(...args: string[]) { +async function stack(...args: string[]): Promise<{ stdout: string; stderr: string; }> { return run(join(root,'bin/stack'),args,{cwd:root,env:{...process.env,GNUPGHOME:gpgHome},maxBuffer:8*1024*1024,timeout:300000}); } -async function restorePackage() { +async function restorePackage(): Promise { const listed = await run('gpg',['--no-options','--homedir',gpgHome,'--with-colons','--list-keys']); const fingerprint = required(required(listed.stdout.split('\n').find(line => line.startsWith('fpr:')), 'Missing key fingerprint').split(':')[9], 'Missing key fingerprint field'); const key = join(testHome,'recipient.asc'); const exported = await run('gpg',['--no-options','--homedir',gpgHome,'--armor','--export',fingerprint]); await writeFile(key,exported.stdout); const backup = await stack('backup','--include-images','--recipient',fingerprint,'--key',key); - const archive = backup.stdout.match(/Encrypted export: (.+)/)?.[1]; - assert.ok(archive,backup.stdout); + const archive = (/Encrypted export: (.+)/.exec(backup.stdout))?.[1]; + assert.ok(archive !== undefined && archive.length > 0,backup.stdout); assert.equal((await stat(archive)).mode & 0o777,0o600); assert.equal((await stat(archive + '.sha256')).mode & 0o777,0o600); await stopServer(); - const destination = `${initialRoot}-restored-${++restorations}`; + const destination = `${initialRoot}-restored-${String(++restorations)}`; await stack('restore',archive,destination); root = destination; assert.equal(await readFile(join(root,'.env'),'utf8'),await readFile(join(initialRoot,'.env'),'utf8')); for (const name of ['target','.tools']) assert.equal(spawnSync('test',['-e',join(root,name)]).status,1); await launchServer(); - console.log(`PASS: compiler-free HTTPS stack recovered from encrypted export ${restorations}.`); + console.log(`PASS: compiler-free HTTPS stack recovered from encrypted export ${String(restorations)}.`); } let server: ReturnType | undefined; let serverErrors = ''; -async function verifyGateway() { +async function verifyGateway(): Promise { const url = new URL(origin); const args = ['--noproxy','*','--silent','--show-error','--fail']; if (url.hostname.endsWith('.localhost')) { @@ -90,7 +90,7 @@ async function verifyGateway() { const health = await run('curl',[...args,origin + '/healthz']); assert.equal(health.stdout,'ok\n'); } -async function launchServer() { +async function launchServer(): Promise { if (packaged) { await stack('up','--production'); let failure; @@ -101,17 +101,20 @@ async function launchServer() { throw failure; } server = spawn(resolve('target/debug/stack-chat-server'), ['--root',root,'--bind',new URL(origin).host], {stdio:['ignore','pipe','pipe']}); - server.stderr?.on('data', bytes => { serverErrors += bytes.toString(); }); + server.stderr?.on('data', (bytes: unknown) => { + if (!Buffer.isBuffer(bytes)) throw new Error('Expected server diagnostics as bytes'); + serverErrors += bytes.toString(); + }); await Promise.race([ - new Promise((ready, reject) => { server?.stdout?.once('data', () => ready()); server?.once('exit', () => reject(new Error(serverErrors || 'Server exited'))); }), - new Promise((_, reject) => setTimeout(() => reject(new Error('Server did not start')), 20000).unref()), + new Promise((ready, reject) => { server?.stdout?.once('data', () => { ready(); }); server?.once('exit', () => { reject(new Error((serverErrors.length > 0 ? serverErrors : 'Server exited'))); }); }), + new Promise((_, reject) => setTimeout(() => { reject(new Error('Server did not start')); }, 20000).unref()), ]); } -async function stopServer() { +async function stopServer(): Promise { if (packaged) { await stack('down'); return; } - if (!server) return; + if (server === undefined) return; if (server.exitCode !== null || server.signalCode !== null) return; - const stopped = new Promise(done => server?.once('exit', () => done())); + const stopped = new Promise(done => server?.once('exit', () => { done(); })); server.kill('SIGINT'); await stopped; } @@ -128,21 +131,21 @@ if (libre) { 'extensions.update.enabled':false,'app.update.auto':false,'datareporting.healthreport.uploadEnabled':false, 'browser.sessionstore.resume_from_crash':false, }}); - browser = { close: () => required(persistent, 'Missing Firefox context').close() }; + browser = { close: (): Promise => required(persistent, 'Missing Firefox context').close() }; } else { browser = await chromium.launch({headless:true,channel:'chromium'}); } -const deadline = setTimeout(() => browser.close(), 300000); +const deadline = setTimeout(() => { closeBrowser(browser); }, 300000); const contexts: BrowserContext[] = []; const external: string[] = []; const errors: string[] = []; const bodies: ReturnType[] = []; -async function page() { - const context = required(persistent || await ('newContext' in browser ? browser.newContext({acceptDownloads:true,ignoreHTTPSErrors:packaged}) : Promise.reject(new Error('Missing browser context'))), 'Missing browser context'); +async function page(): Promise { + const context = required(persistent ?? await ('newContext' in browser ? browser.newContext({acceptDownloads:true,ignoreHTTPSErrors:packaged}) : Promise.reject(new Error('Missing browser context'))), 'Missing browser context'); contexts.push(context); const page = await context.newPage(); page.setDefaultTimeout(90000); page.on('pageerror', error => errors.push(error.message)); const navigate = page.goto.bind(page); - page.goto = async (...args: Parameters) => { + page.goto = async (...args: Parameters): ReturnType => { for (let attempt = 0; ; attempt += 1) { try { return await navigate(...args); } catch (error) { if (attempt >= 2 || !(error instanceof Error && error.message.includes('ERR_NETWORK_CHANGED'))) throw error; } @@ -155,15 +158,15 @@ async function page() { }); return page; } -async function waitEntry(page: Page) { await page.locator('#create-choice').waitFor({state:'visible'}); } -async function waitStatus(page: Page, text: string) { await page.waitForFunction(value => document.getElementById('status')?.textContent?.includes(value), text); } -async function create(page: Page, name: string, owner = false) { +async function waitEntry(page: Page): Promise { await page.locator('#create-choice').waitFor({state:'visible'}); } +async function waitStatus(page: Page, text: string): Promise { await page.waitForFunction(value => document.getElementById('status')?.textContent.includes(value), text); } +async function create(page: Page, name: string, owner = false): Promise<{ name: string; mimeType: string; buffer: Buffer; }> { const previousAuthentications = bodies.filter(body => body.op === 'authenticate').length; if (owner) await page.fill('#setup-token', token); else await page.click('#create-choice'); await page.fill('#name', name); await page.fill('#new-password', password); await page.fill('#confirm-password', password); - const downloaded = Promise.race([page.waitForEvent('download'), page.waitForFunction(() => /failed|timed out/i.test(document.getElementById('status')?.textContent || '')).then(async () => { throw new Error(await page.locator('#status').textContent() || 'Missing status'); })]); + const downloaded = Promise.race([page.waitForEvent('download'), page.waitForFunction(() => /failed|timed out/i.test((document.getElementById('status')?.textContent ?? ''))).then(async () => { throw new Error((await page.locator('#status').textContent() ?? 'Missing status')); })]); await page.click('#create-form button'); const download = await downloaded; const bytes = await readFile(required(await download.path(), 'Missing download')); @@ -180,18 +183,18 @@ async function create(page: Page, name: string, owner = false) { assert.equal(bodies.filter(body => body.op === 'authenticate').length,previousAuthentications); return {name:'identity.chat',mimeType:'application/octet-stream',buffer:bytes}; } -async function unlock(page: Page, file: { name: string; mimeType: string; buffer: Buffer }, phrase = password) { +async function unlock(page: Page, file: { name: string; mimeType: string; buffer: Buffer }, phrase = password): Promise { await page.setInputFiles('#file', file); await page.fill('#password', phrase); await page.click('#unlock-form button'); } -async function storageEmpty(page: Page) { +async function storageEmpty(page: Page): Promise { assert.equal(await page.evaluate(() => localStorage.length + sessionStorage.length),0); assert.deepEqual(await page.evaluate(() => indexedDB.databases()),[]); assert.equal(await page.evaluate(async () => (await navigator.serviceWorker.getRegistrations()).length),0); assert.deepEqual(await page.context().cookies(),[]); } -async function verifyLibreJS() { +async function verifyLibreJS(): Promise { let executed = false; const controlServer = createServer((request,response) => { response.setHeader('Cache-Control','no-store'); @@ -221,12 +224,15 @@ try { await malformed.close(); const owner = await page(); const response = await owner.goto(origin); - await owner.locator('#owner-choice').waitFor({state:'visible'}).catch(async error => { throw new Error(`${error.message}; status=${await owner.locator('#status').textContent()}`); }); + await owner.locator('#owner-choice').waitFor({state:'visible'}).catch(async (error: unknown) => { + const currentStatus = await owner.locator('#status').textContent(); + throw new Error(`${String(error)}; status=${currentStatus ?? ''}`, { cause: error }); + }); await owner.click('#owner-choice'); - assert.match(required(response, 'Missing response').headers()['cache-control'] || '', /no-store/); - assert.match(required(response, 'Missing response').headers()['content-security-policy'] || '', /frame-ancestors 'none'/); + assert.match((required(response, 'Missing response').headers()['cache-control'] ?? ''), /no-store/); + assert.match((required(response, 'Missing response').headers()['content-security-policy'] ?? ''), /frame-ancestors 'none'/); await owner.locator('#setup-token').waitFor({state:'visible'}); - assert.match(await owner.locator('#create-title').textContent() || '',/owner only/); + assert.match((await owner.locator('#create-title').textContent() ?? ''),/owner only/); assert.equal(await owner.locator('.introduction h1').count(),0); assert.equal(await owner.locator('.libre-badge a[rel=jslicense]').count(),1); assert.equal(await owner.locator('.source-footer a[rel=jslicense]').count(),0); @@ -234,7 +240,7 @@ try { const ownerFile = await create(owner,'Doan',true); await unlock(owner,ownerFile); await waitStatus(owner,'Unlocked in memory only'); - assert.match(await owner.locator('#who').textContent() || '', /OWNER/); + assert.match((await owner.locator('#who').textContent() ?? ''), /OWNER/); const visitor = await page(); await visitor.goto(origin); await waitEntry(visitor); @@ -246,7 +252,7 @@ try { await unlock(visitor,visitorFile); await waitStatus(visitor,'Unlocked in memory only'); assert.equal(await visitor.locator('#messages li').count(),0); - assert.match(await visitor.locator('#counts').textContent() || '',/Total: 0/); + assert.match((await visitor.locator('#counts').textContent() ?? ''),/Total: 0/); const malicious = ' hello λ'; await visitor.fill('#text',malicious); await visitor.click('#send-form button'); @@ -254,7 +260,7 @@ try { await owner.click('#refresh-inbox'); await owner.locator('#conversations button').first().click(); await waitStatus(owner,'Conversation opened'); - assert.match(await owner.locator('#thread-name').textContent() || '',/A friend/); + assert.match((await owner.locator('#thread-name').textContent() ?? ''),/A friend/); assert.equal(await owner.locator('#messages li').count(),0); await owner.route('**/api', async route => { const body = requestBody(required(route.request().postData(), 'Missing API body')); @@ -280,7 +286,7 @@ try { await visitor.click('#history-form button'); await waitStatus(visitor,'Selected history decrypted'); assert.equal(await visitor.locator('#messages li').count(),2); - assert.match(await visitor.locator('#counts').textContent() || '',/Total: 2 \| Sent: 1 \| Received: 1/); + assert.match((await visitor.locator('#counts').textContent() ?? ''),/Total: 2 \| Sent: 1 \| Received: 1/); let interrupted = false; await visitor.route('**/api',async route => { const body = requestBody(required(route.request().postData(), 'Missing API request')); @@ -290,10 +296,10 @@ try { await visitor.fill('#text','Safe retry after a lost response'); await visitor.click('#send-form button'); await visitor.locator('#retry').waitFor({state:'visible'}); - await visitor.waitForFunction(() => !document.querySelector('#retry')?.disabled); + await visitor.waitForFunction(() => !(document.querySelector('#retry')?.disabled === true)); await visitor.click('#retry'); await waitStatus(visitor,'Encrypted message stored'); - assert.match(await visitor.locator('#counts').textContent() || '',/Total: 3/); + assert.match((await visitor.locator('#counts').textContent() ?? ''),/Total: 3/); await visitor.reload(); await waitEntry(visitor); assert.equal(await visitor.locator('#messages li').count(),0); @@ -302,12 +308,12 @@ try { await visitor.click('#open-choice'); await unlock(visitor,visitorFile); await waitStatus(visitor,'Unlocked in memory only'); - assert.match(await visitor.locator('#counts').textContent() || '',/Total: 3/); + assert.match((await visitor.locator('#counts').textContent() ?? ''),/Total: 3/); assert.equal(await visitor.locator('#messages li').count(),0); await visitor.fill('#history-count','1'); await visitor.click('#history-form button'); await waitStatus(visitor,'Selected history decrypted'); - assert.match(await visitor.locator('#messages p').textContent() || '',/Safe retry/); + assert.match((await visitor.locator('#messages p').textContent() ?? ''),/Safe retry/); await visitor.click('#older'); await waitStatus(visitor,'Selected history decrypted'); assert.equal(await visitor.locator('#messages li').count(),2); @@ -329,7 +335,7 @@ try { await visitor.click('#open-choice'); await unlock(visitor,visitorFile); await waitStatus(visitor,'Unlocked in memory only'); - assert.match(await visitor.locator('#counts').textContent() || '',/Total: 3/); + assert.match((await visitor.locator('#counts').textContent() ?? ''),/Total: 3/); await visitor.fill('#history-count','3'); await visitor.click('#history-form button'); await waitStatus(visitor,'Selected history decrypted'); @@ -342,7 +348,7 @@ try { await owner.click('#owner-open'); await unlock(owner,ownerFile); await waitStatus(owner,'Unlocked in memory only'); - assert.match(await owner.locator('#who').textContent() || '',/OWNER/); + assert.match((await owner.locator('#who').textContent() ?? ''),/OWNER/); await visitor.fill('#history-count','3'); await visitor.click('#history-form button'); await visitor.click('#lock'); @@ -358,7 +364,7 @@ try { await unlock(visitor,visitorFile); await waitStatus(visitor,'Unlocked in memory only'); for (let index = 1; index <= 51; index += 1) { - await visitor.fill('#text',`Batch fixture ${index}`); + await visitor.fill('#text',`Batch fixture ${String(index)}`); await visitor.click('#send-form button'); await waitStatus(visitor,'Encrypted message stored'); } @@ -392,7 +398,7 @@ try { } const blocked = await visitor.request.post(origin + '/api',{data:{op:'config'},headers:{Origin:'https://foreign.invalid'}}); assert.equal(blocked.status(),403); - const administrative = (...args: string[]) => packaged + const administrative = (...args: string[]): SpawnSyncReturns => packaged ? spawnSync(join(root,'bin/stack'),['chat',...args],{cwd:root,encoding:'utf8'}) : spawnSync(resolve('target/debug/stack-chat-server'),['--root',root,'--bind',new URL(origin).host,...args],{encoding:'utf8'}); const registered = administrative('--owner-fingerprint'); @@ -426,10 +432,10 @@ try { await waitStatus(visitor,'Pinned owner key differs'); assert.equal(await visitor.locator('#chat').isVisible(),false); assert.equal(serverErrors,''); - if (process.env['CHAT_SCREENSHOT']) await visitor.screenshot({path:resolve('.tools/chat-preview.png'),fullPage:true}); + if (((process.env['CHAT_SCREENSHOT'] ?? '').length > 0)) await visitor.screenshot({path:resolve('.tools/chat-preview.png'),fullPage:true}); if (libre) { const addon = libreJSAddon(await readFile(join(testHome,'firefox-profile','extensions.json'),'utf8')); - assert.ok(addon?.active,'LibreJS must actually be active'); + assert.ok(addon.active,'LibreJS must actually be active'); console.log(`LibreJS ${addon.version} is active in Firefox; complete chat workflow succeeded.`); } console.log('PASS: real browser/WASI owner setup, protected file recovery, wrong password, visitor/owner encrypted messages, safe text rendering, N=0, counts, dynamic 0-to-total selection and 50-row batching, pagination, retry deduplication, reload, live SQLite snapshot restoration and both identity recoveries, explicit lock, owner setup/access, backend-only reset guards and fresh registration, old key-pin rejection, origin rejection, malformed configuration and history rejection, no browser storage or external requests, no private uploads or database plaintext.'); -- cgit v1.2.3