summaryrefslogtreecommitdiff
path: root/crates/chat-server/tests
diff options
context:
space:
mode:
Diffstat (limited to 'crates/chat-server/tests')
-rw-r--r--crates/chat-server/tests/browser.ts (renamed from crates/chat-server/tests/browser.mjs)147
-rw-r--r--crates/chat-server/tests/progress.ts (renamed from crates/chat-server/tests/progress.mjs)58
2 files changed, 114 insertions, 91 deletions
diff --git a/crates/chat-server/tests/browser.mjs b/crates/chat-server/tests/browser.ts
index b0a9041..74a9df2 100644
--- a/crates/chat-server/tests/browser.mjs
+++ b/crates/chat-server/tests/browser.ts
@@ -21,16 +21,18 @@ import { resolve, join } from 'node:path';
import { randomBytes } from 'node:crypto';
import { spawn, execFileSync, spawnSync, execFile } from 'node:child_process';
import { promisify } from 'node:util';
-import { chromium, firefox } from '../../../.tools/browser/node_modules/playwright/index.mjs';
+import { chromium, firefox, type Browser, type BrowserContext, type Page } from 'playwright';
+import { libreJSAddon, list, record, requestBody, required, serverOrigin, string } from '../../../tests/browser-support.js';
const run = promisify(execFile);
-const packaged = Boolean(process.env.CHAT_TEST_ROOT);
-let root = packaged ? resolve(process.env.CHAT_TEST_ROOT) : await mkdtemp(join(tmpdir(), 'stack-chat-e2e-'));
+const testRoot = process.env['CHAT_TEST_ROOT'];
+const packaged = Boolean(testRoot);
+let root = testRoot ? 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);
@@ -48,14 +50,15 @@ if (packaged) {
} else {
await writeFile(join(root, '.env'), `CHAT_ORIGIN=${origin}\nCHAT_SETUP_TOKEN=${token}\n`, {mode:0o600});
for (const path of ['crates','target','docs','web','LICENSE']) await symlink(resolve(path), join(root,path));
- await mkdir(join(root,'var/build'), {recursive:true});
+ await mkdir(join(root,'var'), {recursive:true});
+ await symlink(resolve('var/build'),join(root,'var/build'));
}
-async function stack(...args) {
+async function stack(...args: string[]) {
return run(join(root,'bin/stack'),args,{cwd:root,env:{...process.env,GNUPGHOME:gpgHome},maxBuffer:8*1024*1024,timeout:300000});
}
async function restorePackage() {
const listed = await run('gpg',['--no-options','--homedir',gpgHome,'--with-colons','--list-keys']);
- const fingerprint = listed.stdout.split('\n').find(line => line.startsWith('fpr:')).split(':')[9];
+ 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);
@@ -73,15 +76,15 @@ async function restorePackage() {
await launchServer();
console.log(`PASS: compiler-free HTTPS stack recovered from encrypted export ${restorations}.`);
}
-let server;
+let server: ReturnType<typeof spawn> | undefined;
let serverErrors = '';
async function verifyGateway() {
const url = new URL(origin);
const args = ['--noproxy','*','--silent','--show-error','--fail'];
if (url.hostname.endsWith('.localhost')) {
- const state = JSON.parse(await readFile(join(root,'var/state.json'),'utf8'));
+ const state = record(JSON.parse(await readFile(join(root,'var/state.json'),'utf8')));
const certificate = join(testHome,'gateway-ca.crt');
- await run('podman',['--root',join(root,'var/podman/storage'),'--runroot',join(root,'var/podman/run'),'cp',`${state.project}-caddy:/data/caddy/pki/authorities/local/root.crt`,certificate]);
+ await run('podman',['--root',join(root,'var/podman/storage'),'--runroot',join(root,'var/podman/run'),'cp',`${string(state['project'])}-caddy:/data/caddy/pki/authorities/local/root.crt`,certificate]);
args.push('--cacert',certificate,'--resolve',`${url.hostname}:${url.port}:127.0.0.1`);
}
const health = await run('curl',[...args,origin + '/healthz']);
@@ -98,23 +101,24 @@ 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 => { 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<void>((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()),
]);
}
async function stopServer() {
if (packaged) { await stack('down'); return; }
+ if (!server) return;
if (server.exitCode !== null || server.signalCode !== null) return;
- const stopped = new Promise(done => server.once('exit',done));
+ const stopped = new Promise<void>(done => server?.once('exit', () => done()));
server.kill('SIGINT');
await stopped;
}
await launchServer();
-const libre = process.env.CHAT_LIBREJS === '1';
-let persistent;
-let browser;
+const libre = process.env['CHAT_LIBREJS'] === '1';
+let persistent: BrowserContext | undefined;
+let browser: Browser | { close(): Promise<void> };
if (libre) {
const profile = join(testHome,'firefox-profile');
await mkdir(join(profile,'extensions'),{recursive:true});
@@ -124,63 +128,64 @@ if (libre) {
'extensions.update.enabled':false,'app.update.auto':false,'datareporting.healthreport.uploadEnabled':false,
'browser.sessionstore.resume_from_crash':false,
}});
- browser = { close: () => persistent.close() };
+ browser = { close: () => required(persistent, 'Missing Firefox context').close() };
} else { browser = await chromium.launch({headless:true,channel:'chromium'}); }
const deadline = setTimeout(() => browser.close(), 300000);
-const contexts = [];
-const external = [];
-const errors = [];
-const bodies = [];
+const contexts: BrowserContext[] = [];
+const external: string[] = [];
+const errors: string[] = [];
+const bodies: ReturnType<typeof requestBody>[] = [];
async function page() {
- const context = persistent || await browser.newContext({acceptDownloads:true,ignoreHTTPSErrors:packaged});
+ 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) => {
+ page.goto = async (...args: Parameters<Page['goto']>) => {
for (let attempt = 0; ; attempt += 1) {
try { return await navigate(...args); }
- catch (error) { if (attempt >= 2 || !error.message.includes('ERR_NETWORK_CHANGED')) throw error; }
+ catch (error) { if (attempt >= 2 || !(error instanceof Error && error.message.includes('ERR_NETWORK_CHANGED'))) throw error; }
}
};
page.on('request', request => {
if (!request.url().startsWith(origin + '/')) external.push(request.url());
- if (request.url() === origin + '/api') bodies.push(JSON.parse(request.postData()));
+ if (request.url() === origin + '/api') bodies.push(requestBody(required(request.postData(), 'Missing API body')));
});
return page;
}
-async function waitEntry(page) { await page.locator('#create-choice').waitFor({state:'visible'}); }
-async function waitStatus(page, text) { await page.waitForFunction(value => document.getElementById('status').textContent.includes(value), text); }
-async function create(page, name, owner = false) {
+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) {
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()); })]);
+ 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(await download.path());
- const envelope = JSON.parse(bytes.toString());
- assert.ok(envelope.format.startsWith('stack-chat-file/v2;'));
+ const bytes = await readFile(required(await download.path(), 'Missing download'));
+ const envelope = record(JSON.parse(bytes.toString()));
+ const ciphertext = list(envelope['ciphertext']).map(string);
+ assert.ok(string(envelope['format']).startsWith('stack-chat-file/v2;'));
assert.ok(bytes.toString().startsWith('{\n "format"'));
- assert.ok(Array.isArray(envelope.ciphertext));
- assert.ok(envelope.ciphertext.length > 1);
- assert.ok(envelope.ciphertext.slice(0,-1).every(chunk => chunk.length === 80));
- assert.ok(envelope.ciphertext.at(-1).length > 0 && envelope.ciphertext.at(-1).length <= 80);
+ assert.ok(Array.isArray(ciphertext));
+ assert.ok(ciphertext.length > 1);
+ assert.ok(ciphertext.slice(0,-1).every(chunk => chunk.length === 80));
+ assert.ok(required(ciphertext.at(-1), 'Missing final chunk').length > 0 && required(ciphertext.at(-1), 'Missing final chunk').length <= 80);
await waitStatus(page,'Reopen your downloaded');
assert.equal(await page.locator('#chat').isVisible(), 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, file, phrase = password) {
+async function unlock(page: Page, file: { name: string; mimeType: string; buffer: Buffer }, phrase = password) {
await page.setInputFiles('#file', file);
await page.fill('#password', phrase);
await page.click('#unlock-form button');
}
-async function storageEmpty(page) {
+async function storageEmpty(page: Page) {
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);
@@ -196,10 +201,10 @@ async function verifyLibreJS() {
} else if (request.url === '/executed') { executed = true; response.end('unexpected'); }
else { response.setHeader('Content-Type','text/html'); response.end('<!doctype html><script src="/control.js"></script><p>Unlicensed external-script rejection control.</p>'); }
});
- await new Promise(done => controlServer.listen(0,'127.0.0.1',done));
- const controlPage = await persistent.newPage();
+ await new Promise<void>(done => controlServer.listen(0,'127.0.0.1',done));
+ const controlPage = await required(persistent, 'Missing Firefox context').newPage();
try {
- await controlPage.goto(`http://127.0.0.1:${controlServer.address().port}/`);
+ await controlPage.goto(serverOrigin(controlServer) + '/');
await controlPage.waitForTimeout(1500);
assert.equal(await controlPage.evaluate(() => window.unlicensedControl),undefined,'LibreJS must reject the unlicensed control');
assert.equal(executed,false);
@@ -207,14 +212,21 @@ async function verifyLibreJS() {
}
try {
if (libre) await verifyLibreJS();
+ const malformed = await page();
+ await malformed.route('**/api', route => route.fulfill({ json: { owner: 42, origin, protocol: 'stack-chat/v1' } }));
+ await malformed.goto(origin);
+ await waitStatus(malformed, 'Invalid owner configuration');
+ assert.equal(await malformed.locator('#create-choice').isVisible(), false);
+ await storageEmpty(malformed);
+ 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.click('#owner-choice');
- assert.match(response.headers()['cache-control'], /no-store/);
- assert.match(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);
@@ -222,7 +234,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);
@@ -234,7 +246,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 = '<script>window.chatInjection = true</script> hello λ';
await visitor.fill('#text',malicious);
await visitor.click('#send-form button');
@@ -242,8 +254,20 @@ 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'));
+ if (body.op === 'history') {
+ const response = record(await (await route.fetch()).json());
+ await route.fulfill({ json: { ...response, conversation: { ...record(response['conversation']), total: '1' } } });
+ } else await route.continue();
+ });
+ await owner.fill('#history-count','1');
+ await owner.click('#history-form button');
+ await waitStatus(owner, 'Invalid response count: total');
+ assert.equal(await owner.locator('#messages li').count(),0);
+ await owner.unroute('**/api');
await owner.fill('#history-count','1');
await owner.click('#history-form button');
await waitStatus(owner,'Selected history decrypted');
@@ -256,20 +280,20 @@ 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 = route.request().postDataJSON();
+ const body = requestBody(required(route.request().postData(), 'Missing API request'));
if (body.op === 'send' && !interrupted) { interrupted = true; await route.fetch(); await route.abort(); }
else await route.continue();
});
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.getElementById('retry').disabled);
+ await visitor.waitForFunction(() => !document.querySelector<HTMLButtonElement>('#retry')?.disabled);
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);
@@ -278,12 +302,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);
@@ -305,7 +329,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');
@@ -318,7 +342,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');
@@ -326,7 +350,7 @@ try {
assert.equal(await visitor.locator('#messages li').count(),0);
await visitor.goto(origin + '/LIMITATIONS.txt');
await visitor.goBack();
- await visitor.waitForFunction(() => !document.getElementById('entry').hidden);
+ await visitor.waitForFunction(() => document.getElementById('entry')?.hidden === false);
assert.equal(await visitor.locator('#messages li').count(),0);
assert.equal(await visitor.locator('#password').inputValue(),'');
assert.equal(await visitor.locator('#chat').isVisible(),false);
@@ -368,7 +392,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) => packaged
+ const administrative = (...args: string[]) => 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');
@@ -402,14 +426,13 @@ 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']) await visitor.screenshot({path:resolve('.tools/chat-preview.png'),fullPage:true});
if (libre) {
- const extensions = JSON.parse(await readFile(join(testHome,'firefox-profile','extensions.json'),'utf8'));
- const addon = extensions.addons.find(addon => addon.id === 'jid1-KtlZuoiikVfFew@jetpack');
+ const addon = libreJSAddon(await readFile(join(testHome,'firefox-profile','extensions.json'),'utf8'));
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, no browser storage or external requests, no private uploads or database plaintext.');
+ 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.');
} finally {
clearTimeout(deadline);
await browser.close();
diff --git a/crates/chat-server/tests/progress.mjs b/crates/chat-server/tests/progress.ts
index 0ee5614..59c939d 100644
--- a/crates/chat-server/tests/progress.mjs
+++ b/crates/chat-server/tests/progress.ts
@@ -14,22 +14,23 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import assert from 'node:assert/strict';
-import { createServer } from 'node:http';
+import { createServer, type ServerResponse } from 'node:http';
import { readFile, writeFile, mkdir, mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
-import { chromium, firefox } from '../../../.tools/browser/node_modules/playwright/index.mjs';
+import { chromium, firefox, type BrowserContext, type Page } from 'playwright';
+import { libreJSAddon, required, serverOrigin } from '../../../tests/browser-support.js';
-const libre = process.env.CHAT_LIBREJS === '1';
+const libre = process.env['CHAT_LIBREJS'] === '1';
const profile = await mkdtemp(join(tmpdir(),'stack-chat-progress-'));
const bytes = await readFile(resolve('target/wasm32-wasip1/release/stack-chat-crypto.wasm'));
let mode = 'known';
-let transfer;
-let configuration;
-let configReady;
-let signalConfig;
-let origin;
-const errors = [];
+let transfer: ServerResponse | undefined;
+let configuration: ServerResponse | undefined;
+let configReady: Promise<void> | undefined;
+let signalConfig: (() => void) | undefined;
+let origin = '';
+const errors: string[] = [];
const server = createServer(async (request,response) => {
response.setHeader('Cache-Control','no-store');
if (request.url === '/crypto.wasm') {
@@ -40,20 +41,20 @@ const server = createServer(async (request,response) => {
response.write(bytes.subarray(0,1048576));
return;
}
- if (request.url === '/api') { configuration = response; signalConfig(); return; }
- const assets = { '/':'crates/chat-server/web/index.html','/app.js':'crates/chat-server/web/app.js','/style.css':'crates/chat-server/web/style.css' };
- const path = assets[request.url];
+ if (request.url === '/api') { configuration = response; required(signalConfig, 'Missing configuration signal')(); return; }
+ const assets: Record<string, string> = { '/':'crates/chat-server/web/index.html','/app.js':'var/build/typescript/crates/chat-server/web/app.js','/style.css':'crates/chat-server/web/style.css' };
+ const path = assets[request.url || ''];
if (!path) { response.writeHead(404); response.end(); return; }
- const mime = request.url === '/' ? 'text/html' : request.url.endsWith('.js') ? 'text/javascript' : 'text/css';
+ const mime = request.url === '/' ? 'text/html' : request.url?.endsWith('.js') ? 'text/javascript' : 'text/css';
response.setHeader('Content-Type',mime);
let source = await readFile(resolve(path),'utf8');
if (request.url === '/') source = source.replaceAll('{{SITE_ORIGIN}}',origin);
response.end(source);
});
-await new Promise(done => server.listen(0,'127.0.0.1',done));
-origin = `http://127.0.0.1:${server.address().port}`;
+await new Promise<void>(done => server.listen(0,'127.0.0.1',done));
+origin = serverOrigin(server);
let browser;
-let context;
+let context: BrowserContext;
if (libre) {
await mkdir(join(profile,'extensions'));
const addon = await readFile(resolve('.tools/librejs.xpi'));
@@ -64,7 +65,7 @@ if (libre) {
browser = await chromium.launch({headless:true,channel:'chromium'});
context = await browser.newContext();
}
-async function open(selected) {
+async function open(selected: 'known' | 'unknown' | 'failure') {
mode = selected;
configuration = undefined;
configReady = new Promise(done => { signalConfig = done; });
@@ -74,16 +75,16 @@ async function open(selected) {
await page.goto(origin);
return page;
}
-async function waitStatus(page,part) {
- await page.waitForFunction(value => document.getElementById('status').textContent.includes(value),part);
- return page.locator('#status').textContent();
+async function waitStatus(page: Page, part: string) {
+ await page.waitForFunction(value => document.getElementById('status')?.textContent?.includes(value),part);
+ return await page.locator('#status').textContent() || '';
}
-async function complete(page,offset) {
- transfer.end(bytes.subarray(offset));
+async function complete(page: Page, offset: number) {
+ required(transfer, 'Missing transfer').end(bytes.subarray(offset));
await waitStatus(page,'Connecting to chat');
await configReady;
- configuration.setHeader('Content-Type','application/json');
- configuration.end(JSON.stringify({origin,owner:''}));
+ required(configuration, 'Missing configuration').setHeader('Content-Type','application/json');
+ required(configuration, 'Missing configuration').end(JSON.stringify({origin,owner:'',protocol:'stack-chat/v1'}));
await page.locator('#owner-choice').waitFor({state:'visible'});
assert.equal(await page.locator('#status').textContent(),'');
assert.equal(await page.evaluate(async () => (await wasm).byteLength),bytes.length);
@@ -93,11 +94,11 @@ try {
const known = await open('known');
assert.match(await waitStatus(known,'1.0 /'),/\[#{3}-{17}\] 19%/);
await known.clock.fastForward(44000);
- transfer.write(bytes.subarray(1048576,2097152));
+ required(transfer, 'Missing transfer').write(bytes.subarray(1048576,2097152));
assert.match(await waitStatus(known,'2.0 /'),/39%/);
await known.screenshot({path:resolve('.tools/chat-download-progress.png'),fullPage:true});
await known.clock.fastForward(44000);
- assert.ok(!(await known.locator('#status').textContent()).includes('stalled'));
+ assert.ok(!(await known.locator('#status').textContent() || '').includes('stalled'));
await complete(known,2097152);
const unknown = await open('unknown');
assert.equal(await waitStatus(unknown,'1.0 MiB received'),'Downloading cryptographic module... 1.0 MiB received');
@@ -110,13 +111,12 @@ try {
await waitStatus(stalled,'1.0 /');
await stalled.clock.fastForward(45001);
await waitStatus(stalled,'stalled for 45 seconds');
- assert.match(await stalled.locator('#status').textContent(),/reload/);
+ assert.match(await stalled.locator('#status').textContent() || '',/reload/);
assert.equal(await stalled.locator('#owner-choice').isVisible(),false);
await stalled.close();
assert.deepEqual(errors,[]);
if (libre) {
- const state = JSON.parse(await readFile(join(profile,'extensions.json'),'utf8'));
- assert.ok(state.addons.find(addon => addon.id === 'jid1-KtlZuoiikVfFew@jetpack')?.active);
+ assert.ok(libreJSAddon(await readFile(join(profile,'extensions.json'),'utf8')).active);
}
console.log(`PASS: ${libre ? 'Firefox/LibreJS' : 'Chromium'} streamed percentage/bar/MiB, unknown length, exact byte recovery, slow continuous transfer, idle timeout, HTTP failure, and connection phase.`);
} finally {