test: split the 1.5.2 suite into a harness and per-topic files
test/helpers/harness.js holds the aedes broker, the Alexa-side watcher, until() and the cleanups, plus directive(), connected() and setup(), which the old file spelled out in each test. test/alex2node.test.js (194 lines, 8 tests) becomes connection, discovery, directives, change-report and typings. The 50-line round-trip test is cut at its topic boundaries and the duplicate-endpointId test no longer carries the thermostat discovery check, so a failure names a case: 8 tests become 15, every 1.5.2 assertion kept. Four checks are stricter: the "directive" event is compared whole, the StateReport is checked for its token and properties, the Celsius conversion covers lowerSetpoint as well as temperature, and a duplicate endpointId is listed once. No change to src/ or dist/. npm test: 15 pass in 0.9 s. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
275f00f8a7
commit
58869d44c1
7 changed files with 447 additions and 194 deletions
105
test/helpers/harness.js
Normal file
105
test/helpers/harness.js
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
"use strict";
|
||||
// Shared by every test file: an in-process MQTT broker (aedes), a client that plays the Alexa side of the Alex2MQTT
|
||||
// topic contract, and until() for polling. Everything listens on 127.0.0.1, on a port the OS picks. node --test runs
|
||||
// each file in its own process, so the cleanups registered here close that file's brokers and clients when it ends.
|
||||
const { after } = require("node:test");
|
||||
const net = require("node:net");
|
||||
const aedes = require("aedes");
|
||||
const mqtt = require("mqtt");
|
||||
|
||||
const cleanups = [];
|
||||
// Newest first: a client closes before the broker it is connected to.
|
||||
after(async () => {
|
||||
for (const close of cleanups.reverse()) {
|
||||
try { await close(); } catch { /* already closed */ }
|
||||
}
|
||||
});
|
||||
|
||||
/** Run fn when the test file ends. */
|
||||
function cleanup(fn) {
|
||||
cleanups.push(fn);
|
||||
}
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
/** Poll fn every 15 ms until it returns something truthy. Throws "timeout waiting for <what>" after ms. */
|
||||
async function until(fn, ms = 3000, what = "condition") {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < ms) {
|
||||
if (await fn()) return true;
|
||||
await sleep(15);
|
||||
}
|
||||
throw new Error(`timeout waiting for ${what}`);
|
||||
}
|
||||
|
||||
/** An MQTT broker on 127.0.0.1. */
|
||||
async function broker() {
|
||||
const instance = aedes();
|
||||
const server = net.createServer(instance.handle);
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
cleanup(() => new Promise((resolve) => instance.close(() => server.close(() => resolve()))));
|
||||
const { port } = server.address();
|
||||
return { aedes: instance, port, url: `mqtt://127.0.0.1:${port}` };
|
||||
}
|
||||
|
||||
/**
|
||||
* The Alexa side (what Alex2MQTT does on the broker): a client subscribed to filter that keeps every message it
|
||||
* receives, in order, as { topic, payload } - the payload parsed when it is JSON, the text otherwise.
|
||||
*/
|
||||
async function watcher(url, filter) {
|
||||
const client = mqtt.connect(url);
|
||||
cleanup(() => new Promise((resolve) => client.end(true, {}, resolve)));
|
||||
await new Promise((resolve, reject) => { client.once("connect", resolve); client.once("error", reject); });
|
||||
const got = [];
|
||||
client.on("message", (topic, message) => {
|
||||
let payload = message.toString();
|
||||
try { payload = JSON.parse(payload); } catch { /* not JSON: kept as text */ }
|
||||
got.push({ topic, payload });
|
||||
});
|
||||
await new Promise((resolve, reject) => client.subscribe(filter, (err) => (err ? reject(err) : resolve())));
|
||||
const publish = (topic, payload) => client.publish(topic, typeof payload === "string" ? payload : JSON.stringify(payload));
|
||||
return {
|
||||
client,
|
||||
got,
|
||||
publish,
|
||||
/** The payloads received on one topic, oldest first. */
|
||||
on: (topic) => got.filter((m) => m.topic === topic).map((m) => m.payload),
|
||||
/** Publish a directive where Alex2MQTT does: <root>/<endpointId>/alexaDirective. */
|
||||
send: (root, message) => publish(`${root}/${message.endpoint.endpointId}/alexaDirective`, message),
|
||||
};
|
||||
}
|
||||
|
||||
/** A directive as Alex2MQTT publishes it: the Alexa directive with endpoint.scope (the user's token) removed. */
|
||||
function directive(namespace, name, endpointId, correlationToken, payload = {}) {
|
||||
return {
|
||||
header: { namespace, name, payloadVersion: "3", messageId: `message-${correlationToken}`, correlationToken },
|
||||
endpoint: { endpointId },
|
||||
payload,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* connect() the bridge and wait for its "connect" event, which fires once it is subscribed - a directive published
|
||||
* earlier would be lost. The bridge is disconnected when the test file ends.
|
||||
*/
|
||||
async function connected(bridge, ms = 3000) {
|
||||
let subscribed = false;
|
||||
bridge.once("connect", () => { subscribed = true; });
|
||||
bridge.connect();
|
||||
cleanup(() => bridge.disconnect());
|
||||
await until(() => subscribed, ms, "the bridge to connect and subscribe");
|
||||
return bridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* A broker, the Alexa side subscribed to <root>/#, and a connected bridge. The Alex2MQTT class is passed in: the test
|
||||
* file decides which build of the library it loads, not the harness.
|
||||
*/
|
||||
async function setup(Alex2MQTT, root, options = {}) {
|
||||
const { url } = await broker();
|
||||
const alexa = await watcher(url, `${root}/#`);
|
||||
const bridge = await connected(new Alex2MQTT("u", "p", root, false, { host: url, ...options }));
|
||||
return { url, alexa, bridge };
|
||||
}
|
||||
|
||||
module.exports = { broker, watcher, directive, connected, setup, until, sleep, cleanup };
|
||||
Loading…
Add table
Add a link
Reference in a new issue