aweft

recipes/notify/main.ts

One file of recipes/notify, at commit 9a5bb24.

// The notify battery under the gate: two pages of one signed-in user hear a send live, mark it
// read for each other, and register a device; email and push go to two fake services on
// localhost that record what they were sent; a second server with no store does a contact form's
// job; and a restart over the same driver shows the same inbox.
//
// Run: AWEFT_DEFAULT_H=@aweftjs/ui node --import @aweftjs/build/loader recipes/notify/main.ts

import { generateKeyPairSync } from 'node:crypto';
import { createServer as createHttpServer } from 'node:http';
import type { IncomingMessage, Server as HttpServer, ServerResponse } from 'node:http';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';

import { chromium } from 'playwright';
import type { Page } from 'playwright';
import { createServer as createViteServer } from 'vite';

import { fromBundle } from '@aweftjs/modules';
import { notify } from '@aweftjs/notify';
import type { Send } from '@aweftjs/notify';
import { createServer, open } from '@aweftjs/server';
import { node } from '@aweftjs/server/node';
import type { Store } from '@aweftjs/store';

const here = fileURLToPath(new URL('.', import.meta.url));

let checks = 0;
let failed = 0;
const check = (ok: boolean, what: string): void => {
	checks += 1;
	if (!ok) failed += 1;
	console.log(`  ${ok ? 'ok  ' : 'FAIL'} ${what}`);
};

// --- two fake services on localhost ----------------------------------------------------------

interface Call { readonly path: string; readonly body: unknown }

const fake = async (route?: (call: Call, reply: (status: number, body: unknown) => void) => boolean): Promise<{ url: string; calls: Call[]; answer(status: number, body: unknown): void; http: HttpServer }> => {
	const calls: Call[] = [];
	let status = 200;
	let body: unknown = { id: 'fake-mail' };
	const http = createHttpServer((incoming: IncomingMessage, outgoing: ServerResponse) => {
		let text = '';
		incoming.on('data', (chunk: Buffer) => { text += chunk.toString(); });
		incoming.on('end', () => {
			let parsed: unknown = text;
			try { parsed = JSON.parse(text); } catch { /* a form body stays text */ }
			const call = { path: incoming.url ?? '', body: parsed };
			calls.push(call);
			const reply = (s: number, b: unknown): void => { outgoing.writeHead(s, { 'content-type': 'application/json' }); outgoing.end(JSON.stringify(b)); };
			if (route?.(call, reply) === true) return;
			reply(status, body);
		});
	});
	const url = await new Promise<string>((done) => { http.listen(0, '127.0.0.1', () => { done(`http://127.0.0.1:${String((http.address() as { port: number }).port)}`); }); });
	return { url, calls, answer: (s, b) => { status = s; body = b; }, http };
};

const resend = await fake();
const fcm = await fake((call, reply) => {
	if (call.path === '/token') { reply(200, { access_token: 'fake-access', expires_in: 3600 }); return true; }
	return false;
});
fcm.answer(200, { name: 'projects/recipe/messages/1' });
const sends = (): Call[] => fcm.calls.filter((c) => c.path !== '/token');

const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
const account = JSON.stringify({ client_email: 'push@recipe.iam.gserviceaccount.com', private_key: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(), project_id: 'recipe' });

// --- the backend, on a free port, configured through the environment ---------------------------

process.env.PORT = '0';
process.env.RESEND_URL = resend.url;
process.env.FCM_ACCOUNT = account;
process.env.FCM_URL = `${fcm.url}/v1/projects/{project}/messages:send`;
process.env.FCM_TOKEN_URL = `${fcm.url}/token`;
const backend = await import('./backend/main.ts');
let running = backend.running;
process.env.AWEFT_BACKEND_PORT = String(running.port);

// --- the dev server, with the backend behind it ----------------------------------------------

const dev = await createViteServer({
	configFile: join(here, 'page', 'vite.config.ts'),
	logLevel: 'warn',
	server: { port: 0 },
});
await dev.listen();
const origin = dev.resolvedUrls!.local[0]!;

// --- the pages --------------------------------------------------------------------------------

const browser = await chromium.launch();
const context = await browser.newContext({ viewport: { width: 900, height: 700 } });

const titles = (view: Page): Promise<string[]> => view.locator('#items .title').allTextContents();
const states = (view: Page): Promise<string[]> => view.locator('#items .state').allTextContents();
const inboxDoc = async (store: Store, user: string): Promise<{ title: string; delivery: string }[]> => {
	const handle = await store.open(`inbox:${user}`);
	const items = [...((handle.root as { items?: { title: string; delivery: string }[] }).items ?? [])].map((one) => ({ title: one.title, delivery: one.delivery }));
	await store.close(handle);
	return items;
};
const devicesDoc = async (store: Store, user: string): Promise<string[]> => {
	const handle = await store.open(`devices:${user}`);
	const devices = Object.keys((handle.root as { devices?: Record<string, unknown> }).devices ?? {});
	await store.close(handle);
	return devices;
};

try {
	const a = await context.newPage();
	await a.goto(origin);
	await a.waitForSelector('#page');
	await a.locator('#who', { hasText: 'nobody' }).waitFor();
	check((await a.evaluate(() => (globalThis as unknown as { __probe(): Promise<string> }).__probe())) === 'refused', 'an anonymous page\'s inbox refuses at once');

	await a.locator('input').first().fill('ada@example.com');
	await a.locator('input[type=password]').fill('correct horse battery staple');
	await a.click('#sign-in');
	await a.locator('#who', { hasText: 'signed in as' }).waitFor();
	const user = (await a.locator('#who').textContent())!.replace('signed in as ', '');
	await a.locator('#unread', { hasText: '0 unread' }).waitFor();

	// A second page of the same browser is the same user, and gets the same inbox.
	const b = await context.newPage();
	await b.goto(origin);
	await b.locator('#who', { hasText: 'signed in as' }).waitFor();
	await b.locator('#unread', { hasText: '0 unread' }).waitFor();

	await a.click('#register');
	await a.waitForTimeout(200);
	check((await devicesDoc(running.store, user)).includes('this-browser'), 'the page registered itself as a device');

	// A send from the application's own module lands on both pages live.
	await a.click('#ship');
	await a.locator('#unread', { hasText: '1 unread' }).waitFor();
	await b.locator('#unread', { hasText: '1 unread' }).waitFor();
	check((await titles(a)).length === 1 && (await titles(b)).length === 1, 'both pages show the item');
	check((await states(b))[0] === 'new', 'and it is new');

	await b.click('#read-all');
	await a.locator('#unread', { hasText: '0 unread' }).waitFor();
	check((await states(a))[0] === 'read', 'read on one page is read on the other');

	// A loud send goes to both services, and the record on the item says so.
	await a.click('#ship-loud');
	await a.locator('#unread', { hasText: '1 unread' }).waitFor();
	await a.waitForTimeout(300);
	const mail = resend.calls[0]?.body as { to: string; subject: string; html: string } | undefined;
	check(mail?.to === 'ada@example.com' && mail.subject === 'Order shipped' && mail.html.includes('<strong>Order shipped</strong>'), 'the mail went to the address off the user document');
	const push = sends()[0]?.body as { message: { data: Record<string, string>; android: { priority: string }; notification?: unknown } } | undefined;
	check(push?.message.android.priority === 'HIGH' && push.message.notification === undefined, 'the push is data only at HIGH priority');
	check(push?.message.data.t === 'Notification' && push.message.data.b === 'You have a new notification', 'and carries no text by default');
	const stored = await inboxDoc(running.store, user);
	const record = JSON.parse(stored[1]!.delivery) as Record<string, { ok?: boolean }>;
	check(push?.message.data.n !== undefined && stored[1] !== undefined && record.inbox?.ok === true && record.email?.ok === true && record.push?.ok === true, 'the item carries ok for all three channels');

	await a.click('#ship-open');
	await a.locator('#unread', { hasText: '2 unread' }).waitFor();
	await a.waitForTimeout(200);
	check((sends()[1]?.body as { message: { data: Record<string, string> } } | undefined)?.message.data.t === 'Order shipped', 'private false sends the text');

	// The service says the device is gone: so is the row.
	fcm.answer(404, { error: { status: 'NOT_FOUND', message: 'Requested entity was not found.', details: [{ '@type': 'type.googleapis.com/google.firebase.fcm.v1.FcmError', errorCode: 'UNREGISTERED' }] } });
	await a.click('#ship-loud');
	await a.locator('#unread', { hasText: '3 unread' }).waitFor();
	await a.waitForTimeout(200);
	check(!(await devicesDoc(running.store, user)).includes('this-browser'), 'a device the service answered unregistered for is forgotten');

	// The mailer fails: the item is kept and the record names the failure.
	resend.answer(500, { message: 'domain not verified' });
	await a.click('#ship-loud');
	await a.locator('#unread', { hasText: '4 unread' }).waitFor();
	await a.waitForTimeout(200);
	const after = await inboxDoc(running.store, user);
	check((JSON.parse(after[4]!.delivery) as { email: { error: string } }).email.error === 'resend answered 500: domain not verified', 'a failed mail is a line in the record and the item is kept');

	// A page's own write into the inbox is refused; the other page and the store never see it.
	await a.evaluate(() => { (globalThis as unknown as { __forge(): void }).__forge(); });
	await a.waitForTimeout(200);
	check((await titles(b))[0] === 'Order shipped' && (await inboxDoc(running.store, user))[0]!.title === 'Order shipped', 'a forged write never reaches the other page or the store');

	// Channels named on a send: email only writes nothing to the inbox.
	const send = running.server.loader.get('notify/Send') as Send;
	resend.answer(200, { id: 'ok' });
	await send.send({ to: { user }, title: 'mail only', channels: ['email'] });
	await a.waitForTimeout(200);
	check((await titles(a)).length === 5, 'channels: [email] wrote nothing to the inbox');

	// The cap: the hundredth send to this person is the last one this hour; somebody else is fine.
	let capped = false;
	for (let n = 0; n < 100; n += 1) {
		try { await send.send({ to: { user }, title: `bulk ${String(n)}` }); } catch (error) { capped = (error as { reason?: string }).reason === 'capped'; break; }
	}
	const other = await send.send({ to: { user: 'somebody-else' }, title: 'theirs' });
	check(capped && other.delivery.inbox !== undefined && 'ok' in other.delivery.inbox, 'the cap is per recipient, and another recipient is under it');

	// A restart over the same driver: the inbox is what it was.
	const before = (await inboxDoc(running.store, user)).length;
	const port = running.port;
	await running.stop(true);
	running = await backend.boot(running.driver, port);
	await a.reload();
	await a.locator('#who', { hasText: 'signed in as' }).waitFor();
	await a.waitForTimeout(300);
	check((await titles(a)).length === before && before > 5, `after a restart the page shows the same ${String(before)} items`);

	// A server with no store, which is a contact form's shape: the mail goes, nothing is refused.
	const bare = createServer({
		sources: [fromBundle({ './notify/Send.ts': { config: { email: { resend: { key: 'k', from: 'site@example.test', endpoint: resend.url } } } } }), notify],
		store: undefined, gate: open, listener: node({ port: 0, host: '127.0.0.1' }),
	});
	await bare.start();
	const contact = await (bare.loader.get('notify/Send') as Send).send({ to: { email: 'owner@example.test' }, title: 'From the form', body: 'hello', level: 'error', replyTo: 'visitor@example.test' });
	await bare.stop();
	check('ok' in (contact.delivery.email ?? {}) && contact.delivery.inbox !== undefined && 'skipped' in contact.delivery.inbox && contact.delivery.push !== undefined && 'skipped' in contact.delivery.push, 'with no store the mail goes and the rest is skipped');
	check((resend.calls.at(-1)?.body as { reply_to?: string }).reply_to === 'visitor@example.test', 'and the visitor is who a reply goes to');

	// Outward off: neither service is reached and the record says why.
	const quiet = createServer({
		sources: [fromBundle({ './notify/Send.ts': { config: { outward: false, email: { resend: { key: 'k', from: 'site@example.test', endpoint: resend.url } } } } }), notify],
		store: undefined, gate: open, listener: node({ port: 0, host: '127.0.0.1' }),
	});
	await quiet.start();
	const seen = resend.calls.length;
	const silent = await (quiet.loader.get('notify/Send') as Send).send({ to: { email: 'owner@example.test' }, title: 'never sent', channels: ['email'] });
	await quiet.stop();
	check(resend.calls.length === seen && 'error' in (silent.delivery.email ?? {}) && String((silent.delivery.email as { error: string }).error).includes('outward'), 'outward: false sends nothing and records the refusal');
} finally {
	await browser.close();
	await dev.close();
	await running.stop();
	resend.http.close();
	fcm.http.close();
}

console.log(`\n${checks - failed}/${checks} checks passed`);
process.exit(failed === 0 ? 0 : 1);