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
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). |
| 3 | import test from 'node:test'; |
| 4 | import assert from 'node:assert/strict'; |
| 5 | import { existsSync } from 'node:fs'; |
| 6 | |
| 7 | const distUrl = new URL('../dist/', import.meta.url); |
| 8 | const built = ['ckey.mjs', 'ckey-core.mjs', 'ckey.wasm'].every((f) => existsSync(new URL(f, distUrl))); |
| 9 | const skip = built ? false : 'dist/ is not built (run `npm run build` with Emscripten); skipping WebAssembly tests'; |
| 10 | if (skip && process.env.CKEY_REQUIRE_WASM) throw new Error(`CKEY_REQUIRE_WASM is set but ${skip}`); |
| 11 | if (skip) console.log(`# ${skip}`); |
| 12 | |
| 13 | const 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. */ |
| 16 | function 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 | |
| 25 | async 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 | |
| 34 | test('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 | |
| 42 | test('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 | |
| 75 | test('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 | |
| 86 | test('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 | |
| 100 | test('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 | |
| 120 | test('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 | |
| 135 | test('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 | |
| 183 | test('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 | |
| 202 | test('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 | |
| 208 | test('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 | }); |