mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-07-22 20:31:03 +02:00
404 lines
13 KiB
JavaScript
404 lines
13 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
import { withApp } from "../lib/app.mjs";
|
|
|
|
async function jsonRequest(
|
|
url,
|
|
{ method = "GET", token, body, headers = {} } = {},
|
|
) {
|
|
const response = await fetch(url, {
|
|
method,
|
|
headers: {
|
|
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
...headers,
|
|
},
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
});
|
|
const text = await response.text();
|
|
let value = null;
|
|
if (text) {
|
|
try {
|
|
value = JSON.parse(text);
|
|
} catch {
|
|
value = text;
|
|
}
|
|
}
|
|
return { response, value };
|
|
}
|
|
|
|
async function seedTerms(app) {
|
|
const home = path.join(app.root, "home");
|
|
const directory =
|
|
process.platform === "darwin"
|
|
? path.join(home, "Library", "Application Support", "Wayfern")
|
|
: process.platform === "win32"
|
|
? path.join(app.root, "windows", "roaming", "Wayfern")
|
|
: path.join(app.root, "xdg", "config", "Wayfern");
|
|
await mkdir(directory, { recursive: true });
|
|
await writeFile(
|
|
path.join(directory, "license-accepted"),
|
|
String(Math.floor(Date.now() / 1000)),
|
|
);
|
|
}
|
|
|
|
async function invokeContract(app, command, args = {}) {
|
|
try {
|
|
return { ok: true, value: await app.invoke(command, args) };
|
|
} catch (error) {
|
|
return { ok: false, error: String(error) };
|
|
}
|
|
}
|
|
|
|
test("authenticated REST API serves its complete OpenAPI contract and CRUD lifecycle", async () => {
|
|
await withApp("integrations-rest", async (app) => {
|
|
await seedTerms(app);
|
|
const settings = await app.invoke("get_app_settings");
|
|
const saved = await app.invoke("save_app_settings", {
|
|
settings: {
|
|
...settings,
|
|
api_enabled: true,
|
|
api_port: 0,
|
|
api_token: null,
|
|
onboarding_completed: true,
|
|
},
|
|
});
|
|
assert.ok(saved.api_token?.length >= 32);
|
|
const port = await app.invoke("start_api_server", { port: 0 });
|
|
assert.equal(await app.invoke("get_api_server_status"), port);
|
|
const base = `http://127.0.0.1:${port}`;
|
|
|
|
const openapi = await jsonRequest(`${base}/openapi.json`);
|
|
assert.equal(openapi.response.status, 200);
|
|
assert.equal(openapi.value.openapi.startsWith("3."), true);
|
|
const paths = Object.keys(openapi.value.paths);
|
|
for (const required of [
|
|
"/v1/profiles",
|
|
"/v1/profiles/{id}/run",
|
|
"/v1/groups",
|
|
"/v1/proxies",
|
|
"/v1/vpns/{id}/export",
|
|
"/v1/extensions",
|
|
"/v1/browsers/{browser}/versions",
|
|
]) {
|
|
assert.ok(paths.includes(required), `OpenAPI is missing ${required}`);
|
|
}
|
|
|
|
const unauthorized = await jsonRequest(`${base}/v1/profiles`);
|
|
assert.equal(unauthorized.response.status, 401);
|
|
const wrongToken = await jsonRequest(`${base}/v1/profiles`, {
|
|
token: "wrong",
|
|
});
|
|
assert.equal(wrongToken.response.status, 401);
|
|
|
|
const groupsInitially = await jsonRequest(`${base}/v1/groups`, {
|
|
token: saved.api_token,
|
|
});
|
|
assert.equal(groupsInitially.response.status, 200);
|
|
assert.deepEqual(groupsInitially.value, []);
|
|
const createdGroup = await jsonRequest(`${base}/v1/groups`, {
|
|
method: "POST",
|
|
token: saved.api_token,
|
|
body: { name: "REST Group" },
|
|
});
|
|
assert.equal(createdGroup.response.status, 200);
|
|
assert.equal(createdGroup.value.name, "REST Group");
|
|
const groupId = createdGroup.value.id;
|
|
const updatedGroup = await jsonRequest(`${base}/v1/groups/${groupId}`, {
|
|
method: "PUT",
|
|
token: saved.api_token,
|
|
body: { name: "REST Group Updated" },
|
|
});
|
|
assert.equal(updatedGroup.value.name, "REST Group Updated");
|
|
|
|
const createdProxy = await jsonRequest(`${base}/v1/proxies`, {
|
|
method: "POST",
|
|
token: saved.api_token,
|
|
body: {
|
|
name: "REST Proxy",
|
|
proxy_settings: {
|
|
proxy_type: "http",
|
|
host: "127.0.0.1",
|
|
port: 8080,
|
|
username: null,
|
|
password: null,
|
|
},
|
|
},
|
|
});
|
|
assert.equal(createdProxy.response.status, 200);
|
|
assert.equal(createdProxy.value.proxy_settings.port, 8080);
|
|
const proxyId = createdProxy.value.id;
|
|
const fetchedProxy = await jsonRequest(`${base}/v1/proxies/${proxyId}`, {
|
|
token: saved.api_token,
|
|
});
|
|
assert.equal(fetchedProxy.value.name, "REST Proxy");
|
|
const imported = await jsonRequest(`${base}/v1/proxies/import`, {
|
|
method: "POST",
|
|
token: saved.api_token,
|
|
body: {
|
|
format: "txt",
|
|
content: "http://127.0.0.1:8081",
|
|
name_prefix: "API",
|
|
},
|
|
});
|
|
assert.equal(imported.response.status, 200);
|
|
assert.equal(imported.value.imported_count, 1);
|
|
|
|
const missing = await jsonRequest(`${base}/v1/groups/missing`, {
|
|
token: saved.api_token,
|
|
});
|
|
assert.equal(missing.response.status, 404);
|
|
const invalidProfile = await jsonRequest(`${base}/v1/profiles`, {
|
|
method: "POST",
|
|
token: saved.api_token,
|
|
body: { name: "Bad", browser: "unsupported", version: "latest" },
|
|
});
|
|
assert.equal(invalidProfile.response.status, 400);
|
|
|
|
assert.equal(
|
|
(
|
|
await jsonRequest(`${base}/v1/proxies/${proxyId}`, {
|
|
method: "DELETE",
|
|
token: saved.api_token,
|
|
})
|
|
).response.status,
|
|
204,
|
|
);
|
|
for (const importedProxy of imported.value.proxies) {
|
|
await jsonRequest(`${base}/v1/proxies/${importedProxy.id}`, {
|
|
method: "DELETE",
|
|
token: saved.api_token,
|
|
});
|
|
}
|
|
assert.equal(
|
|
(
|
|
await jsonRequest(`${base}/v1/groups/${groupId}`, {
|
|
method: "DELETE",
|
|
token: saved.api_token,
|
|
})
|
|
).response.status,
|
|
204,
|
|
);
|
|
await app.invoke("stop_api_server");
|
|
assert.equal(await app.invoke("get_api_server_status"), null);
|
|
});
|
|
});
|
|
|
|
test("MCP Streamable HTTP initialization, auth, discovery, calls, and isolated agent install", async () => {
|
|
await withApp("integrations-mcp", async (app) => {
|
|
await seedTerms(app);
|
|
const port = await app.invoke("start_mcp_server");
|
|
assert.equal(await app.invoke("get_mcp_server_status"), true);
|
|
const config = await app.invoke("get_mcp_config");
|
|
assert.equal(config.port, port);
|
|
assert.ok(config.token.length >= 32);
|
|
const base = `http://127.0.0.1:${port}`;
|
|
assert.equal((await fetch(`${base}/health`)).status, 200);
|
|
assert.equal(
|
|
(
|
|
await jsonRequest(`${base}/mcp`, {
|
|
method: "POST",
|
|
body: { jsonrpc: "2.0", id: 1, method: "initialize", params: {} },
|
|
})
|
|
).response.status,
|
|
401,
|
|
);
|
|
|
|
const initialized = await jsonRequest(`${base}/mcp/${config.token}`, {
|
|
method: "POST",
|
|
body: {
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "initialize",
|
|
params: {
|
|
protocolVersion: "2025-11-25",
|
|
capabilities: {},
|
|
clientInfo: { name: "donut-e2e", version: "1" },
|
|
},
|
|
},
|
|
});
|
|
assert.equal(initialized.response.status, 200);
|
|
assert.equal(initialized.value.result.serverInfo.name, "donut-browser");
|
|
const sessionId = initialized.response.headers.get("mcp-session-id");
|
|
assert.ok(sessionId);
|
|
const mcpHeaders = { "mcp-session-id": sessionId };
|
|
const notification = await jsonRequest(`${base}/mcp/${config.token}`, {
|
|
method: "POST",
|
|
headers: mcpHeaders,
|
|
body: { jsonrpc: "2.0", method: "notifications/initialized" },
|
|
});
|
|
assert.equal(notification.response.status, 202);
|
|
const tools = await jsonRequest(`${base}/mcp/${config.token}`, {
|
|
method: "POST",
|
|
headers: mcpHeaders,
|
|
body: { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} },
|
|
});
|
|
assert.equal(tools.response.status, 200);
|
|
const names = tools.value.result.tools.map((tool) => tool.name);
|
|
for (const name of [
|
|
"list_profiles",
|
|
"create_profile",
|
|
"run_profile",
|
|
"list_proxies",
|
|
"get_page_content",
|
|
"get_interactive_elements",
|
|
]) {
|
|
assert.ok(names.includes(name), `MCP is missing ${name}`);
|
|
}
|
|
const listed = await jsonRequest(`${base}/mcp/${config.token}`, {
|
|
method: "POST",
|
|
headers: mcpHeaders,
|
|
body: {
|
|
jsonrpc: "2.0",
|
|
id: 3,
|
|
method: "tools/call",
|
|
params: { name: "list_profiles", arguments: {} },
|
|
},
|
|
});
|
|
assert.equal(listed.response.status, 200);
|
|
assert.equal(listed.value.error, undefined);
|
|
assert.ok(listed.value.result);
|
|
|
|
const agents = await app.invoke("list_mcp_agents");
|
|
assert.ok(agents.some((agent) => agent.id === "cursor"));
|
|
await app.invoke("add_mcp_to_agent", { agentId: "cursor" });
|
|
assert.equal(
|
|
(await app.invoke("list_mcp_agents")).find(
|
|
(agent) => agent.id === "cursor",
|
|
).connected,
|
|
true,
|
|
);
|
|
await app.invoke("remove_mcp_from_agent", { agentId: "cursor" });
|
|
assert.equal(
|
|
(await app.invoke("list_mcp_agents")).find(
|
|
(agent) => agent.id === "cursor",
|
|
).connected,
|
|
false,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await jsonRequest(`${base}/mcp/${config.token}`, {
|
|
method: "DELETE",
|
|
headers: mcpHeaders,
|
|
})
|
|
).response.status,
|
|
200,
|
|
);
|
|
await app.invoke("stop_mcp_server");
|
|
assert.equal(await app.invoke("get_mcp_server_status"), false);
|
|
});
|
|
});
|
|
|
|
test("offline cloud, update, team-lock, trial, and synchronizer contracts are deterministic", async () => {
|
|
await withApp("integrations-contracts", async (app) => {
|
|
assert.equal(await app.invoke("cloud_get_user"), null);
|
|
assert.equal(await app.invoke("cloud_get_proxy_usage"), null);
|
|
assert.ok(await app.invoke("cloud_get_wayfern_token"));
|
|
assert.deepEqual(await app.invoke("get_team_locks"), []);
|
|
assert.equal(
|
|
await app.invoke("get_team_lock_status", {
|
|
profileId: "00000000-0000-0000-0000-000000000000",
|
|
}),
|
|
null,
|
|
);
|
|
assert.deepEqual(await app.invoke("get_sync_sessions"), []);
|
|
const startResult = await invokeContract(app, "start_sync_session", {
|
|
leaderProfileId: "00000000-0000-0000-0000-000000000001",
|
|
followerProfileIds: ["00000000-0000-0000-0000-000000000002"],
|
|
});
|
|
assert.equal(startResult.ok, false);
|
|
const stopError = await app.invokeError("stop_sync_session", {
|
|
sessionId: "missing",
|
|
});
|
|
assert.match(stopError, /not found|session/i);
|
|
const removeError = await app.invokeError("remove_sync_follower", {
|
|
sessionId: "missing",
|
|
followerProfileId: "missing",
|
|
});
|
|
assert.match(removeError, /not found|session/i);
|
|
|
|
assert.equal(await app.invoke("check_for_app_updates"), null);
|
|
assert.equal(await app.invoke("check_for_app_updates_manual"), null);
|
|
assert.ok(
|
|
await invokeContract(app, "cloud_exchange_device_code", {
|
|
code: "DONUT-E2E-INVALID-CODE",
|
|
}),
|
|
);
|
|
assert.ok(await invokeContract(app, "cloud_refresh_profile"));
|
|
assert.ok(await invokeContract(app, "cloud_get_countries"));
|
|
assert.ok(
|
|
await invokeContract(app, "cloud_get_regions", {
|
|
country: "ZZ",
|
|
}),
|
|
);
|
|
assert.ok(
|
|
await invokeContract(app, "cloud_get_cities", {
|
|
country: "ZZ",
|
|
region: null,
|
|
}),
|
|
);
|
|
assert.ok(
|
|
await invokeContract(app, "cloud_get_isps", {
|
|
country: "ZZ",
|
|
region: null,
|
|
city: null,
|
|
}),
|
|
);
|
|
assert.ok(
|
|
await invokeContract(app, "create_cloud_location_proxy", {
|
|
name: "E2E unavailable cloud proxy",
|
|
country: "ZZ",
|
|
region: null,
|
|
city: null,
|
|
isp: null,
|
|
}),
|
|
);
|
|
assert.ok(await invokeContract(app, "cloud_refresh_wayfern_token"));
|
|
|
|
assert.ok(await invokeContract(app, "trigger_manual_version_update"));
|
|
assert.ok(await invokeContract(app, "clear_all_version_cache_and_refetch"));
|
|
assert.ok(await invokeContract(app, "check_for_browser_updates"));
|
|
await app.invoke("dismiss_update_notification", {
|
|
notificationId: "missing-e2e-notification",
|
|
});
|
|
assert.deepEqual(
|
|
await app.invoke("complete_browser_update_with_auto_update", {
|
|
browser: "wayfern",
|
|
newVersion: "150.0.7871.100",
|
|
}),
|
|
[],
|
|
);
|
|
const prepareError = await app.invokeError(
|
|
"download_and_prepare_app_update",
|
|
{
|
|
updateInfo: {
|
|
current_version: "0.0.0",
|
|
new_version: "0.0.1-e2e",
|
|
release_notes: "E2E invalid update contract",
|
|
download_url: `${process.env.DONUT_E2E_FIXTURE_URL}/invalid-update.zip`,
|
|
is_nightly: false,
|
|
published_at: "2026-01-01T00:00:00Z",
|
|
manual_update_required: false,
|
|
release_page_url: null,
|
|
repo_update: false,
|
|
checksums_url: null,
|
|
asset_digest: null,
|
|
},
|
|
},
|
|
);
|
|
assert.match(prepareError, /checksum|verif|Failed to download/i);
|
|
const versionStatus = await app.invoke("get_version_update_status");
|
|
assert.ok(versionStatus && typeof versionStatus === "object");
|
|
assert.equal(typeof (await app.invoke("is_default_browser")), "boolean");
|
|
|
|
const trial = await app.invoke("get_commercial_trial_status");
|
|
assert.ok(trial && typeof trial === "object");
|
|
await app.invoke("acknowledge_trial_expiration");
|
|
assert.equal(await app.invoke("has_acknowledged_trial_expiration"), true);
|
|
await app.invoke("cloud_logout");
|
|
assert.equal(await app.invoke("cloud_get_user"), null);
|
|
});
|
|
});
|