// End-to-end tests for the desktop-side affiliate / referral handshake. // // We stand up an in-process HTTP server that implements the same contract // as openswarm-cloud's /api/install/{mint,bind,lookup} endpoints (in-memory // state, no SQLite). The Electron module's polling code talks to this // server over real fetch over real loopback TCP, which is as realistic as // it gets without booting the actual cloud Hono app. // // We then drive both halves of the flow: // * The "user clicks Download on the landing page" half: mint() to get an // install_token, stash it where the test's "welcome page" simulator can // find it. // * The "user installs the app" half: maybeRunFirstLaunchHandshake() with // a fake shell that captures the welcome URL, then we simulate the // welcome page by calling /api/install/bind from the test before the // poll loop times out. // // Polling cadence is squeezed via env vars (OPENSWARM_AFFILIATE_POLL_*) so // the suite finishes in milliseconds instead of seconds. const test = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const path = require("node:path"); const os = require("node:os"); const http = require("node:http"); const crypto = require("node:crypto"); // Force the tracking module to use tight polling well before requiring it, // because the constants are read at module-load time. process.env.OPENSWARM_AFFILIATE_POLL_INTERVAL_MS = "20"; process.env.OPENSWARM_AFFILIATE_POLL_MAX_ATTEMPTS = "30"; const affiliateTracking = require("./affiliateTracking"); // --- in-memory mock cloud -------------------------------------------------- function makeMockCloud() { // Mirrors the install_tokens table. const tokens = new Map(); const server = http.createServer((req, res) => { const url = new URL(req.url, `http://${req.headers.host}`); const send = (status, body) => { const json = JSON.stringify(body); res.writeHead(status, { "Content-Type": "application/json" }); res.end(json); }; const readBody = () => new Promise((resolve) => { let buf = ""; req.on("data", (c) => (buf += c)); req.on("end", () => { try { resolve(JSON.parse(buf || "{}")); } catch { resolve(null); } }); }); (async () => { if (req.method === "POST" && url.pathname === "/api/install/mint") { const body = await readBody(); if (!body || typeof body.ref !== "string" || !body.ref) { return send(400, { error: "ref required" }); } const token = crypto.randomBytes(32).toString("base64url"); tokens.set(token, { ref: body.ref, app_install_id: null, bound_at: null, expires_at: Date.now() + 24 * 60 * 60 * 1000, }); return send(200, { install_token: token, expires_at: Date.now() + 24 * 60 * 60 * 1000 }); } if (req.method === "POST" && url.pathname === "/api/install/bind") { const body = await readBody(); if (!body || typeof body.install_token !== "string" || typeof body.app_install_id !== "string") { return send(400, { error: "install_token + app_install_id required" }); } const row = tokens.get(body.install_token); if (!row) return send(404, { error: "not found" }); if (row.expires_at < Date.now()) return send(410, { error: "expired" }); if (row.app_install_id && row.app_install_id !== body.app_install_id) { return send(409, { error: "already bound" }); } row.app_install_id = body.app_install_id; row.bound_at = Date.now(); return send(200, { ok: true, ref: row.ref }); } if (req.method === "GET" && url.pathname === "/api/install/lookup") { const appInstallId = url.searchParams.get("app_install_id") || ""; if (!/^[A-Za-z0-9_-]{8,128}$/.test(appInstallId)) { return send(400, { error: "bad app_install_id" }); } for (const row of tokens.values()) { if (row.app_install_id === appInstallId) { return send(200, { ref: row.ref, bound_at: row.bound_at }); } } return send(200, { ref: null }); } send(404, { error: "no route" }); })().catch((err) => send(500, { error: err.message })); }); return new Promise((resolve) => { server.listen(0, "127.0.0.1", () => { const addr = server.address(); resolve({ url: `http://127.0.0.1:${addr.port}`, tokens, close: () => new Promise((r) => server.close(r)), }); }); }); } // --- fake shell ------------------------------------------------------------ function makeFakeShell() { const opened = []; return { opened, openExternal: async (url) => { opened.push(url); return true; }, }; } // --- temp-dir helper ------------------------------------------------------- function makeTempUserDataDir() { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openswarm-affiliate-test-")); return dir; } function readJson(p) { return JSON.parse(fs.readFileSync(p, "utf8")); } function delay(ms) { return new Promise((r) => setTimeout(r, ms)); } // Prefer the actual install_token = call the bind endpoint with it. async function simulateWelcomePageBind(cloudUrl, installToken, appInstallId) { const res = await fetch(`${cloudUrl}/api/install/bind`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ install_token: installToken, app_install_id: appInstallId }), }); return { status: res.status, body: await res.json() }; } async function mintTokenFromCloud(cloudUrl, ref) { const res = await fetch(`${cloudUrl}/api/install/mint`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ref }), }); const body = await res.json(); return body.install_token; } function appInstallIdFromWelcomeUrl(url) { const u = new URL(url); return u.searchParams.get("app_install_id"); } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- test("first launch: opens welcome URL and binds ref via poll loop", async () => { const cloud = await makeMockCloud(); try { const userDataDir = makeTempUserDataDir(); const shell = makeFakeShell(); process.env.OPENSWARM_AFFILIATE_LANDING_URL = "https://landing.test"; process.env.OPENSWARM_AFFILIATE_CLOUD_URL = cloud.url; // 1. Pre-mint a token at the cloud as if the user had clicked Download // on the landing page. const installToken = await mintTokenFromCloud(cloud.url, "haik-test"); // 2. Run the desktop's first-launch handshake. await affiliateTracking.maybeRunFirstLaunchHandshake({ shell, userDataDir, isDev: false, isPackaged: true, }); // 3. The shell should have been told to open the welcome URL with the // freshly generated app_install_id. assert.equal(shell.opened.length, 1, "exactly one browser open"); assert.ok( shell.opened[0].startsWith("https://landing.test/welcome?app_install_id="), "welcome URL has correct prefix", ); const appInstallId = appInstallIdFromWelcomeUrl(shell.opened[0]); assert.ok(appInstallId && appInstallId.length > 8, "app_install_id present in URL"); // 4. install.json on disk now has the app_install_id but no ref yet. const stateFile = path.join(userDataDir, "install.json"); let state = readJson(stateFile); assert.equal(state.app_install_id, appInstallId); assert.equal(state.ref, null); assert.ok(state.first_launch_at > 0); // 5. Simulate the welcome page completing the bind. const bindResult = await simulateWelcomePageBind(cloud.url, installToken, appInstallId); assert.equal(bindResult.status, 200); assert.equal(bindResult.body.ref, "haik-test"); // 6. Wait for the poll loop to pick up the bind. Poll cadence is // 20ms × 30 attempts = ~600ms upper bound; we wait up to 1s. let final = null; for (let i = 0; i < 50; i++) { await delay(50); final = readJson(stateFile); if (final.ref) break; } assert.equal(final.ref, "haik-test", "ref should have been written by poll loop"); assert.ok(final.ref_bound_at > 0, "ref_bound_at populated"); } finally { await cloud.close(); } }); test("returning launch: no-op when ref already bound", async () => { const cloud = await makeMockCloud(); try { const userDataDir = makeTempUserDataDir(); const shell = makeFakeShell(); process.env.OPENSWARM_AFFILIATE_CLOUD_URL = cloud.url; // Seed install.json as if first launch already happened and a ref // was bound a few minutes ago. fs.writeFileSync( path.join(userDataDir, "install.json"), JSON.stringify({ app_install_id: "preexisting-app-install-id-1234", first_launch_at: Date.now() - 5 * 60 * 1000, ref: "previously-bound-ref", ref_bound_at: Date.now() - 4 * 60 * 1000, attempts: 1, }), ); await affiliateTracking.maybeRunFirstLaunchHandshake({ shell, userDataDir, isDev: false, isPackaged: true, }); assert.equal(shell.opened.length, 0, "should NOT pop a browser tab"); const state = readJson(path.join(userDataDir, "install.json")); assert.equal(state.ref, "previously-bound-ref"); } finally { await cloud.close(); } }); test("returning launch within grace window: silent re-poll, no second browser pop-up", async () => { const cloud = await makeMockCloud(); try { const userDataDir = makeTempUserDataDir(); const shell = makeFakeShell(); process.env.OPENSWARM_AFFILIATE_CLOUD_URL = cloud.url; // Pre-mint a token + seed install.json as if first launch happened // but the user never completed the welcome page handshake yet. const appInstallId = "grace-app-install-id-abcdef0123"; fs.writeFileSync( path.join(userDataDir, "install.json"), JSON.stringify({ app_install_id: appInstallId, first_launch_at: Date.now() - 10 * 60 * 1000, ref: null, ref_bound_at: null, attempts: 5, }), ); const installToken = await mintTokenFromCloud(cloud.url, "grace-ref"); // Simulate a late welcome bind (user finally clicked through). await simulateWelcomePageBind(cloud.url, installToken, appInstallId); await affiliateTracking.maybeRunFirstLaunchHandshake({ shell, userDataDir, isDev: false, isPackaged: true, }); // Specifically NO browser pop-up the second time around. assert.equal(shell.opened.length, 0, "no second browser open"); // Wait for the silent re-poll to pick up the bind. const stateFile = path.join(userDataDir, "install.json"); let state = null; for (let i = 0; i < 50; i++) { await delay(50); state = readJson(stateFile); if (state.ref) break; } assert.equal(state.ref, "grace-ref", "silent re-poll should pick up the bind"); } finally { await cloud.close(); } }); test("returning launch outside grace window: skipped entirely", async () => { const cloud = await makeMockCloud(); try { const userDataDir = makeTempUserDataDir(); const shell = makeFakeShell(); process.env.OPENSWARM_AFFILIATE_CLOUD_URL = cloud.url; fs.writeFileSync( path.join(userDataDir, "install.json"), JSON.stringify({ app_install_id: "old-app-install-id-1234567890", first_launch_at: Date.now() - 7 * 24 * 60 * 60 * 1000, ref: null, ref_bound_at: null, attempts: 12, }), ); await affiliateTracking.maybeRunFirstLaunchHandshake({ shell, userDataDir, isDev: false, isPackaged: true, }); assert.equal(shell.opened.length, 0, "no browser open after grace window"); // Give the poll loop time to NOT run. await delay(200); const state = readJson(path.join(userDataDir, "install.json")); assert.equal(state.ref, null, "no ref bound"); } finally { await cloud.close(); } }); test("dev mode: skipped unless OPENSWARM_AFFILIATE_FORCE=1", async () => { const cloud = await makeMockCloud(); try { const userDataDir = makeTempUserDataDir(); const shell = makeFakeShell(); process.env.OPENSWARM_AFFILIATE_CLOUD_URL = cloud.url; delete process.env.OPENSWARM_AFFILIATE_FORCE; await affiliateTracking.maybeRunFirstLaunchHandshake({ shell, userDataDir, isDev: true, isPackaged: false, }); assert.equal(shell.opened.length, 0, "dev mode skips the handshake"); assert.ok(!fs.existsSync(path.join(userDataDir, "install.json")), "no state file written"); process.env.OPENSWARM_AFFILIATE_FORCE = "1"; try { await affiliateTracking.maybeRunFirstLaunchHandshake({ shell, userDataDir, isDev: true, isPackaged: false, }); assert.equal(shell.opened.length, 1, "force flag opts back in"); } finally { delete process.env.OPENSWARM_AFFILIATE_FORCE; } } finally { await cloud.close(); } }); test("install.json write is atomic-ish (temp + rename)", async () => { const userDataDir = makeTempUserDataDir(); affiliateTracking._writeState(userDataDir, { app_install_id: "atomic-test-1234567890", ref: "x" }); // After write, the temp file shouldn't be left behind. const files = fs.readdirSync(userDataDir); assert.ok(files.includes("install.json")); assert.ok(!files.some((f) => f.endsWith(".tmp")), "no leftover temp file"); }); test("readState returns {} when no install.json exists", () => { const userDataDir = makeTempUserDataDir(); const state = affiliateTracking._readState(userDataDir); assert.deepEqual(state, {}); }); test("readState returns {} when install.json is corrupt", () => { const userDataDir = makeTempUserDataDir(); fs.writeFileSync(path.join(userDataDir, "install.json"), "{ not json"); const state = affiliateTracking._readState(userDataDir); assert.deepEqual(state, {}); }); test("poll loop respects max attempts and gives up", async () => { // No cloud server at all — every poll attempt fails (ECONNREFUSED). const userDataDir = makeTempUserDataDir(); const shell = makeFakeShell(); // Point at a port nothing's listening on. process.env.OPENSWARM_AFFILIATE_CLOUD_URL = "http://127.0.0.1:1"; await affiliateTracking.maybeRunFirstLaunchHandshake({ shell, userDataDir, isDev: false, isPackaged: true, }); // Wait long enough for all attempts to fail. 20ms × 30 = 600ms. await delay(900); const state = readJson(path.join(userDataDir, "install.json")); assert.equal(state.ref, null, "no ref after exhausted polls"); assert.equal(state.attempts, 30, "all attempts recorded"); });