test: POST /pair smoke test (T-1.3 regression guard)
This commit is contained in:
parent
547df01c21
commit
8eb8360387
|
|
@ -22,7 +22,8 @@
|
|||
"typecheck": "tsc --noEmit",
|
||||
"smoke": "node --test scripts/smoke/smoke.mjs",
|
||||
"smoke:stream": "node --test scripts/smoke/stream.test.mjs",
|
||||
"smoke:all": "node --test scripts/smoke/smoke.mjs scripts/smoke/stream.test.mjs"
|
||||
"smoke:pair": "node --test scripts/smoke/pair.test.mjs",
|
||||
"smoke:all": "node --test scripts/smoke/smoke.mjs scripts/smoke/stream.test.mjs scripts/smoke/pair.test.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.12",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,197 @@
|
|||
/**
|
||||
* T-1.3 Integration smoke: POST /pair pairing flow.
|
||||
*
|
||||
* Requires a running pi process with remote-control (spawned by the before() hook).
|
||||
* Re-uses the helpers from helpers.mjs.
|
||||
*
|
||||
* Tests:
|
||||
* 1. GET /pair-qr → 200 text/plain containing a pi-remote:// URL
|
||||
* 2. POST /pair → 200 { bearerToken, sidecarId } with valid pairingToken
|
||||
* 3. GET /sessions?token=<bearerToken> → 200 (bearer token authenticates)
|
||||
* 4. POST /pair (invalid) → 403 invalid_pairing_token
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import path from "node:path";
|
||||
import { after, before, describe, it } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
baseUrl,
|
||||
createSmokeHome,
|
||||
fetchText,
|
||||
killPi,
|
||||
removeSmokeHome,
|
||||
spawnPi,
|
||||
waitForPort,
|
||||
} from "./helpers.mjs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const EXTENSION_PATH = path.resolve(
|
||||
__dirname,
|
||||
"../../extensions/remote-control",
|
||||
);
|
||||
|
||||
const SMOKE_PORT = Number(process.env.SMOKE_PORT_PAIR ?? 19878);
|
||||
const SMOKE_TOKEN = "pair-test-token-deterministic-789";
|
||||
const BASE = baseUrl(SMOKE_PORT);
|
||||
const AUTH = `?token=${SMOKE_TOKEN}`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: fetch JSON from a URL
|
||||
// ---------------------------------------------------------------------------
|
||||
async function fetchJson(url, init) {
|
||||
const res = await fetch(url, init);
|
||||
let body;
|
||||
try {
|
||||
body = await res.json();
|
||||
} catch {
|
||||
body = null;
|
||||
}
|
||||
return { res, body };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: extract a query-param value from a URL string
|
||||
// ---------------------------------------------------------------------------
|
||||
function extractParam(urlStr, param) {
|
||||
// Find the pi-remote:// URL in a potentially multi-line body
|
||||
const match = urlStr.match(/pi-remote:\/\/[^\s]+/);
|
||||
if (!match) return null;
|
||||
// URLSearchParams doesn't parse custom schemes, so strip the scheme+host
|
||||
const raw = match[0];
|
||||
const qIdx = raw.indexOf("?");
|
||||
if (qIdx === -1) return null;
|
||||
const params = new URLSearchParams(raw.slice(qIdx + 1));
|
||||
return params.get(param);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let piProc = null;
|
||||
let tmpHome = null;
|
||||
|
||||
describe("T-1.3 pair smoke", () => {
|
||||
before(async () => {
|
||||
tmpHome = await createSmokeHome({ port: SMOKE_PORT, token: SMOKE_TOKEN });
|
||||
const { proc } = spawnPi({
|
||||
extensionPath: EXTENSION_PATH,
|
||||
fakeHome: tmpHome,
|
||||
});
|
||||
piProc = proc;
|
||||
await waitForPort({ port: SMOKE_PORT });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
if (piProc) await killPi(piProc);
|
||||
if (tmpHome) await removeSmokeHome(tmpHome);
|
||||
});
|
||||
|
||||
// ── Test 1: GET /pair-qr ─────────────────────────────────────────────────
|
||||
|
||||
it("GET /pair-qr returns a pairing URL", async () => {
|
||||
const { res, body } = await fetchText(`${BASE}/pair-qr${AUTH}`);
|
||||
assert.equal(res.status, 200, `Expected 200, got ${res.status}: ${body}`);
|
||||
assert.ok(
|
||||
body.includes("pi-remote://"),
|
||||
`Response should contain pi-remote:// URL. Got:\n${body}`,
|
||||
);
|
||||
const token = extractParam(body, "pair");
|
||||
assert.ok(
|
||||
token && token.length > 0,
|
||||
`Should be able to extract pair token from URL. Body:\n${body}`,
|
||||
);
|
||||
});
|
||||
|
||||
// ── Test 2: POST /pair with valid pairingToken ────────────────────────────
|
||||
|
||||
it("POST /pair with valid pairingToken returns bearerToken", async () => {
|
||||
// Step 1: obtain a fresh pairing token via GET /pair-qr
|
||||
const { res: qrRes, body: qrBody } = await fetchText(
|
||||
`${BASE}/pair-qr${AUTH}`,
|
||||
);
|
||||
assert.equal(
|
||||
qrRes.status,
|
||||
200,
|
||||
`GET /pair-qr failed: ${qrRes.status} ${qrBody}`,
|
||||
);
|
||||
const pairingToken = extractParam(qrBody, "pair");
|
||||
assert.ok(pairingToken, "Could not extract pairingToken from /pair-qr");
|
||||
|
||||
// Step 2: exchange pairingToken for bearerToken
|
||||
const { res, body } = await fetchJson(`${BASE}/pair`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
pairingToken,
|
||||
deviceToken: "fake-device-token-smoke",
|
||||
environment: "sandbox",
|
||||
}),
|
||||
});
|
||||
assert.equal(res.status, 200, `Expected 200, got ${res.status}: ${JSON.stringify(body)}`);
|
||||
assert.ok(body, "Response body should be JSON");
|
||||
assert.ok(
|
||||
typeof body.bearerToken === "string" && body.bearerToken.length > 0,
|
||||
`Response should have bearerToken string. Got: ${JSON.stringify(body)}`,
|
||||
);
|
||||
assert.ok(
|
||||
typeof body.sidecarId === "string" && body.sidecarId.length > 0,
|
||||
`Response should have sidecarId string. Got: ${JSON.stringify(body)}`,
|
||||
);
|
||||
});
|
||||
|
||||
// ── Test 3: bearerToken authenticates GET /sessions ──────────────────────
|
||||
|
||||
it("bearerToken from pair can authenticate GET /sessions", async () => {
|
||||
// Obtain pairing token
|
||||
const { res: qrRes, body: qrBody } = await fetchText(
|
||||
`${BASE}/pair-qr${AUTH}`,
|
||||
);
|
||||
assert.equal(qrRes.status, 200, `GET /pair-qr failed: ${qrRes.status}`);
|
||||
const pairingToken = extractParam(qrBody, "pair");
|
||||
assert.ok(pairingToken, "Could not extract pairingToken");
|
||||
|
||||
// Exchange for bearer token
|
||||
const { res: pairRes, body: pairBody } = await fetchJson(`${BASE}/pair`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ pairingToken }),
|
||||
});
|
||||
assert.equal(
|
||||
pairRes.status,
|
||||
200,
|
||||
`POST /pair failed: ${pairRes.status} ${JSON.stringify(pairBody)}`,
|
||||
);
|
||||
const { bearerToken } = pairBody;
|
||||
assert.ok(bearerToken, "Must have bearerToken to test auth");
|
||||
|
||||
// Use bearerToken to call GET /sessions
|
||||
const { res: sessRes, body: sessBody } = await fetchJson(
|
||||
`${BASE}/sessions?token=${encodeURIComponent(bearerToken)}`,
|
||||
);
|
||||
assert.equal(
|
||||
sessRes.status,
|
||||
200,
|
||||
`GET /sessions with bearerToken should return 200, got ${sessRes.status}: ${JSON.stringify(sessBody)}`,
|
||||
);
|
||||
assert.ok(Array.isArray(sessBody), "GET /sessions should return an array");
|
||||
});
|
||||
|
||||
// ── Test 4: POST /pair with invalid pairingToken → 403 ───────────────────
|
||||
|
||||
it("POST /pair with invalid pairingToken returns 403", async () => {
|
||||
const { res, body } = await fetchJson(`${BASE}/pair`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ pairingToken: "invalid-xxx" }),
|
||||
});
|
||||
assert.equal(
|
||||
res.status,
|
||||
403,
|
||||
`Expected 403 for invalid token, got ${res.status}: ${JSON.stringify(body)}`,
|
||||
);
|
||||
assert.ok(body, "Should return JSON error body");
|
||||
assert.equal(body.error, "invalid_pairing_token");
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue