Skip to content

Commit fb28462

Browse files
authored
Merge pull request #15 from abtonmoy/test/node-sdk-unit-tests
test(sdk/node): add unit tests for the Node SDK
2 parents 5ee4125 + 1f168ab commit fb28462

3 files changed

Lines changed: 160 additions & 5 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ jobs:
5858
shellcheck --severity=error build/**/*.sh packaging/**/*.sh
5959
6060
node-sdk:
61-
name: node sdk syntax
61+
name: node sdk tests
6262
runs-on: ubuntu-latest
6363
steps:
6464
- uses: actions/checkout@v4
@@ -69,3 +69,5 @@ jobs:
6969
run: |
7070
node --check sdk/node/index.js
7171
node --check sdk/node/cli.js
72+
- name: Run unit tests
73+
run: node --test sdk/node/test/

sdk/node/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const CACHE = process.env.FORTRESS_BROWSERS_PATH || join(homedir(), ".cache", "t
1717
const HOST = process.env.FORTRESS_DOWNLOAD_HOST || `https://github.com/${REPO}/releases/download/${TAG}`;
1818

1919
// platform key -> { asset, kind, launcher }
20-
const ASSETS = {
20+
export const ASSETS = {
2121
"linux-x64": { asset: "tilion-fortress-linux-x64.tar.gz", kind: "tar", launcher: "tilion-fortress/tilion" },
2222
"win-x64": { asset: "tilion-fortress-win-x64.zip", kind: "zip", launcher: "tilion-fortress/tilion.cmd" },
2323
"mac-arm64": { asset: "tilion-fortress-mac-arm64.tar.gz", kind: "tar", launcher: "tilion-fortress/tilion" },
@@ -32,7 +32,7 @@ export function resolvePlatform() {
3232
return null;
3333
}
3434

35-
function personaArgs(persona) {
35+
export function personaArgs(persona) {
3636
if (!persona) return [];
3737
const map = { platform: "--uxr-platform", timezone: "--uxr-timezone", languages: "--uxr-languages",
3838
webglRenderer: "--uxr-webgl-renderer", webglVendor: "--uxr-webgl-vendor",
@@ -41,13 +41,13 @@ function personaArgs(persona) {
4141
return Object.entries(persona).map(([k, v]) => `${map[k] || `--uxr-${k}`}=${v}`);
4242
}
4343

44-
async function sha256(path) {
44+
export async function sha256(path) {
4545
const h = createHash("sha256");
4646
await pipeline(createReadStream(path), h);
4747
return h.digest("hex");
4848
}
4949

50-
async function expectedSha(asset) {
50+
export async function expectedSha(asset) {
5151
try {
5252
const r = await fetch(`${HOST}/SHA256SUMS`);
5353
if (!r.ok) return null;

sdk/node/test/sdk.test.js

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Unit tests for the tilion-fortress Node SDK.
2+
//
3+
// These cover the pure, release-critical logic that decides *which* bundle a user gets and
4+
// whether it is trusted — the platform resolver, the persona->flag mapping, the SHA256SUMS
5+
// parser, and the hasher — with no network and no browser launch. A regression here silently
6+
// ships the wrong binary or skips checksum verification, so it is worth gating in CI.
7+
//
8+
// Run: node --test sdk/node/test/
9+
//
10+
// Mirrors sdk/python/tests/test_sdk.py — same shape, Node side.
11+
import { test } from "node:test";
12+
import assert from "node:assert/strict";
13+
import { mkdtempSync, writeFileSync } from "node:fs";
14+
import { tmpdir } from "node:os";
15+
import { join } from "node:path";
16+
import { createHash } from "node:crypto";
17+
18+
import { resolvePlatform, personaArgs, sha256, expectedSha, ASSETS } from "../index.js";
19+
20+
// --- helpers ---------------------------------------------------------------
21+
22+
// process.platform / process.arch are read at call time by resolvePlatform(), so we can
23+
// swap them for a case and restore afterwards. They are configurable data properties.
24+
function withProcess(platform, arch, fn) {
25+
const desc = { platform: Object.getOwnPropertyDescriptor(process, "platform"),
26+
arch: Object.getOwnPropertyDescriptor(process, "arch") };
27+
Object.defineProperty(process, "platform", { value: platform, configurable: true });
28+
Object.defineProperty(process, "arch", { value: arch, configurable: true });
29+
try { return fn(); }
30+
finally {
31+
Object.defineProperty(process, "platform", desc.platform);
32+
Object.defineProperty(process, "arch", desc.arch);
33+
}
34+
}
35+
36+
// Swap globalThis.fetch for one call and restore, so expectedSha() can be tested offline.
37+
async function withFetch(impl, fn) {
38+
const orig = globalThis.fetch;
39+
globalThis.fetch = impl;
40+
try { return await fn(); }
41+
finally { globalThis.fetch = orig; }
42+
}
43+
44+
const okText = (body) => async () => ({ ok: true, text: async () => body });
45+
46+
// --- platform --------------------------------------------------------------
47+
48+
test("resolvePlatform maps supported platform/arch pairs", () => {
49+
const cases = [
50+
["linux", "x64", "linux-x64"],
51+
["win32", "x64", "win-x64"],
52+
["darwin", "arm64", "mac-arm64"],
53+
["darwin", "x64", "mac-x64"],
54+
];
55+
for (const [platform, arch, expected] of cases) {
56+
assert.equal(withProcess(platform, arch, resolvePlatform), expected, `${platform}/${arch}`);
57+
}
58+
});
59+
60+
test("resolvePlatform returns null for unsupported combos", () => {
61+
const cases = [
62+
["linux", "arm64"], // no arm64 Linux bundle yet
63+
["linux", "ia32"],
64+
["win32", "arm64"],
65+
["win32", "ia32"],
66+
["freebsd", "x64"],
67+
["android", "arm64"],
68+
];
69+
for (const [platform, arch] of cases) {
70+
assert.equal(withProcess(platform, arch, resolvePlatform), null, `${platform}/${arch}`);
71+
}
72+
});
73+
74+
// --- persona ---------------------------------------------------------------
75+
76+
test("personaArgs returns [] for null / empty", () => {
77+
assert.deepEqual(personaArgs(null), []);
78+
assert.deepEqual(personaArgs(undefined), []);
79+
assert.deepEqual(personaArgs({}), []);
80+
});
81+
82+
test("personaArgs maps known keys to the right --uxr-* flags", () => {
83+
const args = personaArgs({ timezone: "America/New_York", hwConcurrency: 16, webglRenderer: "ANGLE" });
84+
assert.ok(args.includes("--uxr-timezone=America/New_York"));
85+
assert.ok(args.includes("--uxr-hw-concurrency=16"));
86+
assert.ok(args.includes("--uxr-webgl-renderer=ANGLE"));
87+
});
88+
89+
test("personaArgs falls back to a --uxr- prefix for unknown keys (never a bare/branded flag)", () => {
90+
assert.deepEqual(personaArgs({ someNewSurface: "v" }), ["--uxr-someNewSurface=v"]);
91+
});
92+
93+
test("personaArgs output is always --uxr- prefixed", () => {
94+
const persona = { platform: "Win32", timezone: "UTC", webglRenderer: "ANGLE",
95+
deviceMemory: 8, screenWidth: 1920, canvasSeed: 42, weirdKey: "x" };
96+
for (const a of personaArgs(persona)) assert.ok(a.startsWith("--uxr-"), a);
97+
});
98+
99+
// --- checksums -------------------------------------------------------------
100+
101+
test("sha256 matches Node crypto for a known buffer", async () => {
102+
const dir = mkdtempSync(join(tmpdir(), "fortress-sdk-"));
103+
const file = join(dir, "blob.bin");
104+
const data = Buffer.from("fortress".repeat(4096));
105+
writeFileSync(file, data);
106+
const expected = createHash("sha256").update(data).digest("hex");
107+
assert.equal(await sha256(file), expected);
108+
});
109+
110+
test("expectedSha parses the matching asset from SHA256SUMS", async () => {
111+
const asset = ASSETS["linux-x64"].asset;
112+
const body = `aa11bb22 ${asset}\ndeadbeef tilion-fortress-win-x64.zip\n`;
113+
const got = await withFetch(okText(body), () => expectedSha(asset));
114+
assert.equal(got, "aa11bb22");
115+
});
116+
117+
test("expectedSha handles the sha256sum '*asset' binary marker", async () => {
118+
const asset = ASSETS["linux-x64"].asset;
119+
const got = await withFetch(okText(`CAFEF00D *${asset}\n`), () => expectedSha(asset));
120+
assert.equal(got, "cafef00d"); // lower-cased
121+
});
122+
123+
test("expectedSha returns null when the asset is absent", async () => {
124+
const body = "aa11bb22 some-other-asset.tar.gz\n";
125+
const got = await withFetch(okText(body), () => expectedSha(ASSETS["linux-x64"].asset));
126+
assert.equal(got, null);
127+
});
128+
129+
test("expectedSha returns null on a non-ok response", async () => {
130+
const got = await withFetch(async () => ({ ok: false, status: 404 }),
131+
() => expectedSha(ASSETS["linux-x64"].asset));
132+
assert.equal(got, null);
133+
});
134+
135+
test("expectedSha swallows a network error instead of throwing", async () => {
136+
const boom = async () => { throw new Error("network down"); };
137+
const got = await withFetch(boom, () => expectedSha("anything"));
138+
assert.equal(got, null);
139+
});
140+
141+
// --- assets table ----------------------------------------------------------
142+
143+
test("ASSETS stays consistent with resolvePlatform", () => {
144+
// Every key resolvePlatform() can return must exist in ASSETS, and each launcher path must
145+
// live under tilion-fortress/ so extraction lands where ensureNative expects.
146+
const resolvable = ["linux-x64", "win-x64", "mac-arm64", "mac-x64"];
147+
for (const key of resolvable) assert.ok(key in ASSETS, `missing asset for ${key}`);
148+
for (const [plat, { asset, kind, launcher }] of Object.entries(ASSETS)) {
149+
assert.ok(asset.startsWith("tilion-fortress-") && asset.includes(plat), `${plat}: ${asset}`);
150+
assert.ok(["tar", "zip"].includes(kind), `${plat}: kind ${kind}`);
151+
assert.ok(launcher.startsWith("tilion-fortress/"), `${plat}: launcher ${launcher}`);
152+
}
153+
});

0 commit comments

Comments
 (0)