mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 20:27:44 +02:00
384 lines
12 KiB
JavaScript
384 lines
12 KiB
JavaScript
// E2E tests for the desktop affiliate handshake against an in-process mock cloud.
|
|
|
|
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");
|
|
|
|
// Polling envs must be set before require: constants read at module load.
|
|
process.env.OPENSWARM_AFFILIATE_POLL_INTERVAL_MS = "20";
|
|
process.env.OPENSWARM_AFFILIATE_POLL_MAX_ATTEMPTS = "30";
|
|
|
|
const affiliateTracking = require("./affiliateTracking");
|
|
|
|
function makeMockCloud() {
|
|
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)),
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function makeFakeShell() {
|
|
const opened = [];
|
|
return {
|
|
opened,
|
|
openExternal: async (url) => {
|
|
opened.push(url);
|
|
return true;
|
|
},
|
|
};
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
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;
|
|
|
|
const installToken = await mintTokenFromCloud(cloud.url, "haik-test");
|
|
|
|
await affiliateTracking.maybeRunFirstLaunchHandshake({
|
|
shell,
|
|
userDataDir,
|
|
isDev: false,
|
|
isPackaged: true,
|
|
});
|
|
|
|
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");
|
|
|
|
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);
|
|
|
|
const bindResult = await simulateWelcomePageBind(cloud.url, installToken, appInstallId);
|
|
assert.equal(bindResult.status, 200);
|
|
assert.equal(bindResult.body.ref, "haik-test");
|
|
|
|
// Poll budget: 20ms * 30 attempts ~= 600ms; 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;
|
|
|
|
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;
|
|
|
|
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");
|
|
await simulateWelcomePageBind(cloud.url, installToken, appInstallId);
|
|
|
|
await affiliateTracking.maybeRunFirstLaunchHandshake({
|
|
shell,
|
|
userDataDir,
|
|
isDev: false,
|
|
isPackaged: true,
|
|
});
|
|
|
|
assert.equal(shell.opened.length, 0, "no second browser open");
|
|
|
|
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");
|
|
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" });
|
|
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 () => {
|
|
const userDataDir = makeTempUserDataDir();
|
|
const shell = makeFakeShell();
|
|
process.env.OPENSWARM_AFFILIATE_CLOUD_URL = "http://127.0.0.1:1";
|
|
|
|
await affiliateTracking.maybeRunFirstLaunchHandshake({
|
|
shell,
|
|
userDataDir,
|
|
isDev: false,
|
|
isPackaged: true,
|
|
});
|
|
|
|
// 20ms * 30 = 600ms upper bound; wait 900ms.
|
|
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");
|
|
});
|