// test/app.test.mjs import assert from 'node:assert/strict'; import { after, before, beforeEach, test } from 'node:test'; import { fileURLToPath } from 'node:url'; import { PGlite } from '@electric-sql/pglite'; import { createAccountDouble } from '@turnzero/account/testing'; import { createAiAllowanceDouble } from '@turnzero/ai_allowance/testing'; import { createDatabaseDouble, migrationsFrom } from '@turnzero/database/testing'; import { createAsker } from '../src/ai.mjs'; import { createDataLayer } from '../src/db.mjs'; import { createAppServer } from '../src/server.mjs'; import { createUserReader } from '../src/session.mjs'; const MIGRATIONS = fileURLToPath(new URL('../migrations/', import.meta.url)); // The packages' three test doubles for the file: a PostgreSQL engine inside // this process, with the migrations applied; the realm's sign-in; and the AI // allowance. The server runs over them on a port of this machine. let double; let accounts; let ai; let db; let server; let base; // Two signed-in users, and the headers a browser of each sends. let sam; let kim; const signedIn = (user) => ({ cookie: `__Host-turnzero_cloud_realm=${user.session}`, 'x-turnzero-cloud-session': user.header }); before(async () => { double = await createDatabaseDouble({ engine: new PGlite(), files: migrationsFrom(MIGRATIONS) }); accounts = await createAccountDouble(); ai = createAiAllowanceDouble(); db = createDataLayer(double.pool); server = createAppServer({ db, userIdOf: createUserReader(accounts.client()), ask: createAsker(ai.transport) }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); base = `http://127.0.0.1:${server.address().port}`; sam = accounts.signIn({ id: 'user-sam' }); kim = accounts.signIn({ id: 'user-kim' }); }); // Every table emptied and the allowance's answers cleared between tests, so each test starts from empty. beforeEach(() => { ai.reset(); return double.reset(); }); after(async () => { await new Promise((resolve) => server.close(resolve)); await double.close(); }); test('the migration applies', () => { assert.deepEqual(double.applied, ['0001_notes.sql']); }); test('the data layer adds a row for its owner and lists it to that owner alone', async () => { assert.deepEqual(await db.addNote('user-sam', 'first'), { id: '1', title: 'first' }); assert.deepEqual(await db.listNotes('user-sam'), [{ id: '1', title: 'first' }]); assert.deepEqual(await db.listNotes('user-kim'), []); }); test('GET /health reads the database and returns 200', async () => { const response = await fetch(`${base}/health`); assert.equal(response.status, 200); assert.deepEqual(await response.json(), { ok: true }); }); test('a local run serves the page, its script, and its stylesheet from client/', async () => { for (const [path, type] of [['/', 'text/html'], ['/app.js', 'text/javascript'], ['/styles.css', 'text/css']]) { const response = await fetch(`${base}${path}`); assert.equal(response.status, 200, path); assert.ok(response.headers.get('content-type').startsWith(type), path); assert.ok((await response.text()).length > 0, path); } }); test('the API answers 401 to a request with no session, and reads a session sent as a Bearer', async () => { for (const path of ['/api/notes', '/api/ask']) { const response = await fetch(`${base}${path}`, { method: path === '/api/ask' ? 'POST' : 'GET' }); assert.equal(response.status, 401, path); assert.deepEqual(await response.json(), { error: 'sign_in_required' }); } const bearer = await fetch(`${base}/api/notes`, { headers: { authorization: `Bearer ${sam.session}`, 'x-turnzero-cloud-session': sam.header } }); assert.equal(bearer.status, 200); }); test('POST /api/notes keeps a title with characters outside ASCII for the signed-in user, and GET /api/notes returns it to that user alone', async () => { const title = 'café ☕ 東京'; const response = await fetch(`${base}/api/notes`, { method: 'POST', headers: { 'content-type': 'application/json', ...signedIn(sam) }, body: JSON.stringify({ title }), }); assert.equal(response.status, 201); assert.equal((await response.json()).title, title); const listed = await fetch(`${base}/api/notes`, { headers: signedIn(sam) }); assert.equal(listed.status, 200); assert.deepEqual((await listed.json()).map((row) => row.title), [title]); assert.deepEqual(await (await fetch(`${base}/api/notes`, { headers: signedIn(kim) })).json(), []); }); test('POST /api/notes refuses an empty title, and GET /api/notes returns no row', async () => { const response = await fetch(`${base}/api/notes`, { method: 'POST', headers: { 'content-type': 'application/json', ...signedIn(sam) }, body: JSON.stringify({ title: '' }), }); assert.equal(response.status, 400); assert.deepEqual(await response.json(), { error: 'invalid_note' }); assert.deepEqual(await (await fetch(`${base}/api/notes`, { headers: signedIn(sam) })).json(), []); }); test('POST /api/notes answers 400 invalid_json for a body that is not JSON, and GET /api/notes returns no row', async () => { const response = await fetch(`${base}/api/notes`, { method: 'POST', headers: { 'content-type': 'application/json', ...signedIn(sam) }, body: '{', }); assert.equal(response.status, 400); assert.deepEqual(await response.json(), { error: 'invalid_json' }); assert.deepEqual(await (await fetch(`${base}/api/notes`, { headers: signedIn(sam) })).json(), []); }); test('POST /api/ask answers under the AI allowance, and answers 503 naming why the model gave no answer: the allowance drawn, no key in place, or the provider out of capacity', async () => { ai.prime({ text: 'Lunch is at noon.', usage: { input: 40, output: 12 } }); const ask = (question) => fetch(`${base}/api/ask`, { method: 'POST', headers: { 'content-type': 'application/json', ...signedIn(sam) }, body: JSON.stringify({ question }) }); const answered = await ask('When is lunch?'); assert.equal(answered.status, 200); assert.deepEqual(await answered.json(), { answer: 'Lunch is at noon.' }); const unanswered = async (error) => { const response = await ask('When is lunch?'); assert.equal(response.status, 503, error); assert.deepEqual(await response.json(), { error }); }; ai.refuseNext('allowance_exhausted'); await unanswered('allowance_drawn'); ai.refuseNext('allowance_unavailable'); await unanswered('allowance_unavailable'); ai.prime({ status: 429, json: { error: { status: 'RESOURCE_EXHAUSTED' } } }); await unanswered('upstream_quota'); const empty = await ask(''); assert.equal(empty.status, 400); });