Viewing is open to everyone. Downloading and starring need a GitHub sign-in. Sign in with GitHub

wasm.test.mjs

v0.1.0-build.85 Latest Commit ede38f5

Size
9.9 KB
Lines
222
Language
javascript
SHA-256
1004b84da6da…
View on GitHub

Sign in with GitHub to download. Each download is recorded with your GitHub username.

1// Tests against the built WebAssembly core (dist/). Skipped, with a message,
2// when dist/ has not been built (npm run build needs Emscripten).
3import test from 'node:test';
4import assert from 'node:assert/strict';
5import { existsSync } from 'node:fs';
6
7const distUrl = new URL('../dist/', import.meta.url);
8const built = ['ckey.mjs', 'ckey-core.mjs', 'ckey.wasm'].every((f) => existsSync(new URL(f, distUrl)));
9const skip = built ? false : 'dist/ is not built (run `npm run build` with Emscripten); skipping WebAssembly tests';
10if (skip && process.env.CKEY_REQUIRE_WASM) throw new Error(`CKEY_REQUIRE_WASM is set but ${skip}`);
11if (skip) console.log(`# ${skip}`);
12
13const mod = built ? await import(new URL('ckey.mjs', distUrl).href) : null;
14
15/** Places an RGBA image in the middle of a larger white frame, like a camera view. */
16function pad(img, margin) {
17 const side = img.width + 2 * margin;
18 const data = new Uint8ClampedArray(side * side * 4).fill(255);
19 for (let y = 0; y < img.height; y++) {
20 data.set(img.data.subarray(y * img.width * 4, (y + 1) * img.width * 4), ((y + margin) * side + margin) * 4);
21 }
22 return { width: side, height: side, data };
23}
24
25async function fresh(extra = {}) {
26 const sent = [];
27 const ckey = await mod.loadCKey({
28 fetch: async (url, init) => (sent.push({ url, init }), { status: 204 }),
29 ...extra,
30 });
31 return { ckey, sent };
32}
33
34test('loads, reports its version and the default telemetry mode', { skip }, async () => {
35 const { ckey } = await fresh();
36 assert.match(ckey.version, /^0\.\d+\.\d+(-(build|dev)\.\d+)?$/);
37 assert.equal(ckey.telemetry, 1);
38 const again = await mod.loadCKey();
39 assert.equal(again, await mod.loadCKey(), 'no-option loads share one instance');
40});
41
42test('encode -> renderPixels -> decodeImageData round trip', { skip }, async () => {
43 const { ckey } = await fresh();
44 for (const [text, opts] of [
45 ['https://openckey.org', { version: 0, level: 'M', layout: 'standard' }],
46 ['Grüße aus CKey ✓', { level: 'H', layout: 'print' }],
47 ]) {
48 const sym = ckey.encode(text, opts);
49 assert.equal(sym.level, opts.level);
50 assert.equal(sym.layout, opts.layout);
51 assert.equal(sym.payloadLength, new TextEncoder().encode(text).length);
52 assert.ok(sym.capacity >= sym.payloadLength);
53 assert.ok(sym.version >= 1 && sym.version <= 31);
54 const img = ckey.renderPixels(sym, { size: 512, transparent: false, policy: 'coverage' });
55 assert.equal(img.width, 512);
56 assert.equal(img.data.length, 512 * 512 * 4);
57 assert.ok(img.data instanceof Uint8ClampedArray);
58 const r = ckey.decodeImageData(pad(img, 64));
59 assert.ok(r, 'decoded');
60 assert.equal(r.text, text);
61 assert.equal(r.profile, opts.layout === 'print' ? '0.1' : '0');
62 assert.equal(r.version, sym.version);
63 assert.equal(r.layout, opts.layout);
64 sym.free();
65 sym.free();
66 assert.throws(() => ckey.renderPixels(sym), /freed/);
67 }
68 const bin = ckey.encode(Uint8Array.of(0, 1, 2, 255));
69 const r = ckey.decodeImageData(pad(ckey.renderPixels(bin, { size: 400, preset: 'concept' }), 40));
70 assert.deepEqual([...r.bytes], [0, 1, 2, 255]);
71 assert.equal(r.text, null);
72 assert.equal(ckey.decodeImageData({ width: 64, height: 64, data: new Uint8ClampedArray(64 * 64 * 4).fill(255) }), null);
73});
74
75test('transparent output: corner alpha 0, still decodes over white', { skip }, async () => {
76 const { ckey } = await fresh();
77 const sym = ckey.encode('transparent');
78 const img = ckey.renderPixels(sym, { size: 256, transparent: true });
79 assert.equal(img.data[3], 0);
80 assert.equal(img.data[img.data.length - 1], 0);
81 assert.ok(ckey.decodeImageData(pad(img, 32)));
82 const opaque = ckey.renderPixels(sym, { size: 256 });
83 assert.equal(opaque.data[3], 255);
84});
85
86test('PNG, SVG and PDF exports', { skip }, async () => {
87 const { ckey } = await fresh();
88 const sym = ckey.encode('exports');
89 const png = ckey.renderPNG(sym, { size: 128, transparent: true, policy: 'binary' });
90 assert.ok(png instanceof Uint8Array);
91 assert.deepEqual([...png.subarray(0, 8)], [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
92 const svg = ckey.renderSVG(sym);
93 assert.equal(typeof svg, 'string');
94 assert.match(svg, /^<svg[\s\S]*<\/svg>\s*$/);
95 assert.match(ckey.renderSVG(sym, { preset: 'concept' }), /<defs>/);
96 const pdf = ckey.renderPDF(sym, { widthPt: 144, transparent: false });
97 assert.equal(new TextDecoder().decode(pdf.subarray(0, 5)), '%PDF-');
98});
99
100test('errors carry the core diagnostics', { skip }, async () => {
101 const { ckey } = await fresh();
102 assert.throws(() => ckey.encode('x'.repeat(10000)), (e) => e instanceof Error && /exceeds|capacity|payload/i.test(e.message));
103 assert.throws(() => ckey.encode('x', { level: 'Z' }), RangeError);
104 const sym = ckey.encode('style');
105 assert.throws(() => ckey.renderSVG(sym, { style: { ink: '#fafafa' } }), /contrast|:1/);
106 assert.equal(ckey.checkStyle(sym, { style: { ink: '#fafafa' } }).ok, false);
107 assert.equal(ckey.checkStyle(sym).ok, true);
108 if (ckey.abiVersion >= 4) {
109 const over = ckey.checkStyle(sym, { style: { ink: '#fafafa', policy: 'override' } });
110 assert.equal(over.ok, true);
111 assert.match(over.message, /override/);
112 assert.ok(ckey.renderSVG(sym, { style: { ink: '#fafafa', policy: 'override' } }).startsWith('<svg'));
113 const cert = ckey.certify(sym);
114 assert.equal(cert.certified, true);
115 const unknown = ckey.certify(sym, { transparent: true });
116 assert.equal(unknown.certified, false);
117 }
118});
119
120test('telemetry mode validation', { skip }, async () => {
121 const { ckey } = await fresh();
122 for (const bad of [3, -1, '1', 1.5, null, undefined]) {
123 assert.throws(() => (ckey.telemetry = bad), RangeError);
124 assert.equal(ckey.telemetry, 1, 'never silently changed');
125 }
126 ckey.telemetry = 2;
127 assert.equal(ckey.telemetry, 2);
128 ckey.telemetry = 0;
129 assert.equal(ckey.telemetry, 0);
130 const off = await mod.loadCKey({ telemetry: 0, fetch: null });
131 assert.equal(off.telemetry, 0);
132 await assert.rejects(mod.loadCKey({ telemetry: 7 }), RangeError);
133});
134
135test('scanner drives a session and a mocked send', { skip }, async () => {
136 let clock = 10_000;
137 const { ckey, sent } = await fresh({ now: () => clock, day: () => '2026-11-05' });
138 const sym = ckey.encode('https://openckey.org/scan');
139 const frame = pad(ckey.renderPixels(sym, { size: 320 }), 40);
140 const blank = { width: 64, height: 64, data: new Uint8ClampedArray(64 * 64 * 4).fill(255) };
141 const results = [];
142 const scanner = ckey.createScanner({ onResult: (r) => results.push(r) });
143 assert.equal(scanner.processFrame(blank), null);
144 clock += 100;
145 const r = scanner.processFrame(frame);
146 assert.equal(r.text, 'https://openckey.org/scan');
147 assert.equal(results.length, 1);
148 assert.equal(scanner.processFrame(frame), null, 'one result per session');
149 scanner.stop();
150 const c = ckey.telemetryCounters();
151 assert.deepEqual(c.sessions, { started: 1, succeeded: 1, failed: 0 });
152 assert.equal(c.frames, 2);
153 assert.equal(c.decoded.p0, 1);
154
155 assert.equal(await ckey.flushTelemetry(), null, 'delta younger than a minute');
156 clock += 61_000;
157 assert.equal(await ckey.flushTelemetry(), 204);
158 assert.equal(sent.length, 1);
159 assert.equal(sent[0].url, mod.CKEY_TELEMETRY_ENDPOINT);
160 assert.equal(sent[0].init.method, 'POST');
161 assert.equal(sent[0].init.keepalive, true);
162 assert.equal(sent[0].init.headers['content-type'], 'application/json');
163 const body = JSON.parse(sent[0].init.body);
164 assert.equal(body.sdk, 'web');
165 assert.equal(body.platform, 'web');
166 assert.equal(body.sdk_version, ckey.version);
167 assert.equal(body.day, '2026-11-05');
168 assert.deepEqual(body.sessions, { started: 1, succeeded: 1, failed: 0 });
169 assert.ok(!sent[0].init.body.includes('openckey.org/scan'), 'no payload content');
170 assert.equal(await ckey.flushTelemetry(), null, 'delta empty, hourly cadence');
171
172 // Mode 2 counts locally and never sends.
173 ckey.telemetry = 2;
174 const s2 = ckey.createScanner();
175 s2.processFrame(frame);
176 s2.stop();
177 clock += 4_000_000;
178 assert.equal(await ckey.flushTelemetry(), null);
179 assert.equal(sent.length, 1);
180 assert.ok(ckey.telemetryCounters().sessions.started >= 2);
181});
182
183test('renderToCanvas uses ImageData + putImageData (OffscreenCanvas-like)', { skip }, async () => {
184 const { ckey } = await fresh();
185 const sym = ckey.encode('canvas');
186 let put = null;
187 const canvas = {
188 width: 10,
189 height: 10,
190 getContext: (kind) => (kind === '2d' ? {
191 createImageData: (w, h) => ({ width: w, height: h, data: new Uint8ClampedArray(w * h * 4) }),
192 putImageData: (img, x, y) => (put = { img, x, y }),
193 } : null),
194 };
195 assert.equal(ckey.renderToCanvas(sym, canvas, { size: 64, transparent: false, policy: 'coverage' }), canvas);
196 assert.equal(canvas.width, 64);
197 assert.equal(put.img.width, 64);
198 assert.equal(put.x, 0);
199 assert.throws(() => ckey.renderToCanvas(sym, {}), TypeError);
200});
201
202test('classifyPayload is exposed on the instance and the module', { skip }, async () => {
203 const { ckey } = await fresh();
204 assert.equal(ckey.classifyPayload('tel:+15551234567').kind, 'tel');
205 assert.equal(mod.classifyPayload('javascript:alert(1)').kind, 'text');
206});
207
208test('Safe Actions verify through the wasm core and agree with the conformance vectors', { skip }, async () => {
209 const { readFileSync } = await import('node:fs');
210 const { vectors } = JSON.parse(readFileSync(new URL('../../../tests/vectors/safe-actions-v1.json', import.meta.url), 'utf8'));
211 const { ckey } = await fresh();
212 for (const v of vectors.filter((x) => x.fields || x.expect === 'integrity')) {
213 const app = v.fields?.app ?? 'com.viadere';
214 let ran = 0;
215 const sa = ckey.safeActions(app, { now: () => v.now * 1000, maxLifetime: v.max_lifetime })
216 .trust(v.fields?.issuer ?? 'citeory.com', v.fields?.key_id ?? '2026-11', v.public_key)
217 .register(v.fields?.action ?? 'open.library', { issuer: v.fields?.issuer ?? 'citeory.com' }, () => ++ran);
218 const out = await sa.handle(v.payload);
219 if (v.expect === 'ok' && v.fields.scope === 0) assert.equal(out.kind, 'ran', v.name);
220 if (v.expect !== 'ok') assert.equal(out.kind, 'rejected', v.name);
221 }
222});