git » sdk » main » tree

[main] / test / fast-auth.spec.ts

import { expect, test } from "@playwright/test";
import fs from "fs";
import xml from "@xmpp/xml";
import { parse } from "ltx";

const BIND2 = "urn:xmpp:bind:0";
const SASL2 = "urn:xmpp:sasl:2";
const FRAMING = "urn:ietf:params:xml:ns:xmpp-framing";
const SASL = "urn:ietf:params:xml:ns:xmpp-sasl";
const STREAM = "http://etherx.jabber.org/streams";
const BOROGOVE_CODE = fs.readFileSync("playwright/.cache/borogove.js", "utf8");

const HOST_META = xml(
	"XRD",
	{ xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0" },
	xml("Link", {
		rel: "urn:xmpp:alt-connections:websocket",
		href: "ws://127.0.0.1/xmpp",
	}),
).toString();
const OPEN = xml("open", {
	from: "127.0.0.1",
	id: "stream",
	version: "1.0",
	xmlns: FRAMING,
}).toString();
const FEATURES = xml(
	"stream:features",
	{ xmlns: "jabber:client", "xmlns:stream": STREAM },
	xml(
		"authentication",
		{ xmlns: SASL2 },
		xml("mechanism", {}, "PLAIN"),
		xml("mechanism", {}, "SCRAM-SHA-1"),
		xml(
			"inline",
			{},
			xml(
				"fast",
				{ xmlns: "urn:xmpp:fast:0" },
				xml("mechanism", {}, "HT-SHA-256-NONE"),
			),
			xml("bind", { xmlns: BIND2 }),
		),
	),
).toString();
const NOT_AUTHORIZED = xml(
	"failure",
	{ xmlns: SASL2 },
	xml("not-authorized", { xmlns: SASL }),
).toString();
const CLOSE = xml("close", { xmlns: FRAMING }).toString();

test.beforeEach(async ({ page }) => {
	await page.route("https://localhost/", (route) =>
		route.fulfill({ body: "<html></html>" }),
	);
	await page.route("https://127.0.0.1/.well-known/host-meta", (route) =>
		route.fulfill({
			body: HOST_META,
			contentType: "application/xrd+xml",
		}),
	);
});

test("a rejected unexpired FAST token uses a non-empty password before reconnecting", async ({
	page,
}) => {
	let connectionCount = 0;
	let resolveSocketClosed: () => void;
	const socketClosed = new Promise<void>((resolve) => {
		resolveSocketClosed = resolve;
	});
	await page.routeWebSocket("ws://127.0.0.1/xmpp", (socket) => {
		connectionCount++;
		socket.onMessage((message: string) => {
			const stanza = parse(message);
			expect(stanza.is("open", FRAMING)).toBe(true);
			if (connectionCount === 2) {
				socket.onMessage((message: string) => {
					const stanza = parse(message);
					expect(stanza.is("close", FRAMING)).toBe(true);
					socket.send(CLOSE);
				});
				socket.send(OPEN);
				socket.send(FEATURES);
				return;
			}
			socket.onMessage((message: string) => {
				const stanza = parse(message);
				expect(stanza.is("authenticate", SASL2)).toBe(true);
				expect(stanza.attrs.mechanism).toBe("HT-SHA-256-NONE");
				expect(stanza.getChildText("initial-response")).toBe(
					"dGVzdGVyANlEKT65Z6grrZHbeSjhb05VNRnDs6X1T3Wh0pHQg7cv",
				);
				socket.onMessage((message: string) => {
					const stanza = parse(message);
					expect(stanza.is("authenticate", SASL2)).toBe(true);
					expect(stanza.attrs.mechanism).toBe("PLAIN");
					const [, username, password] = atob(
						stanza.getChildText("initial-response"),
					).split("\0");
					expect(username).toBe("tester");
					expect(password).not.toBe("");
					expect(password).not.toBe("persisted secret");
					socket.send(NOT_AUTHORIZED);
				});
				socket.send(NOT_AUTHORIZED);
			});
			socket.send(OPEN);
			socket.send(FEATURES);
		});
		socket.onClose(() => resolveSocketClosed());
	});
	await page.goto("https://localhost/");

	await page.evaluate(
		async ({ code }) => {
			const moduleUrl = URL.createObjectURL(
				new Blob([code], { type: "text/javascript" }),
			);
			const borogove = await import(moduleUrl);
			const persistence = new borogove.persistence.Dummy();
			persistence.getLogin = async () => ({
				clientId: "test-client",
				displayName: "Test",
				fastCount: 0,
				token: JSON.stringify({
					token: "persisted secret",
					expiry: "2099-01-01T00:00:00Z",
					mechanism: "HT-SHA-256-NONE",
				}),
			});
			persistence.getStreamManagement = async () => ({
				sortId: "a ",
				sm: null,
			});
			persistence.getChats = async () => [];
			persistence.getChatsUnreadDetails = async () => [];
			const client = new borogove.Client(
				"tester@127.0.0.1",
				persistence,
			);
			client.stream.debug = false;
			const passwordRequested = Promise.withResolvers<void>();
			client.addPasswordNeededListener(() => {
				passwordRequested.resolve();
			});
			client.start();
			await passwordRequested.promise;
			client.stream.disconnect();
		},
		{ code: BOROGOVE_CODE },
	);
	await socketClosed;
	expect(connectionCount).toBe(2);
});