// node --test modules/user-data/test.mjs (after `cargo wasm` here and in ../atlibs-iroh; ../atlibs: its hosts) // Byte-equal to what web/webrtc/user-data.js wrote (snapshots/), on both engines; ssh through the // irohUserData hook's default, atlibs-iroh's iroh-user-data. import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { test } from "node:test"; import { runModuleRust } from "../../../atlibs/rust/host/conformance.mjs"; import { runModule } from "../../../atlibs/javascript/src/run-module/index.js"; const lib = (manifest, wasm) => ({ ...JSON.parse(readFileSync(new URL(manifest, import.meta.url), "utf8")), module: readFileSync(new URL(wasm, import.meta.url)) }); const mod = lib("module.json", "../../target/wasm32-unknown-unknown/release/user_data.wasm"); const iroh = lib("../../../atlibs-iroh/modules/iroh-user-data/module.json", "../../../atlibs-iroh/target/wasm32-unknown-unknown/release/iroh_user_data.wasm"); const snapshot = (name) => readFileSync(new URL(`snapshots/${name}.cloud-config`, import.meta.url), "utf8"); const KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIExample me@host"; const ctx = { wasm: { run: async ({ module, entry, input }) => { const x = new WebAssembly.Instance(new WebAssembly.Module(module), {}).exports; const data = new TextEncoder().encode(JSON.stringify(input)); const ptr = x.alloc(data.length); new Uint8Array(x.memory.buffer, ptr, data.length).set(data); const packed = BigInt.asUintN(64, x[entry](ptr, data.length)); return JSON.parse(new TextDecoder().decode(new Uint8Array(x.memory.buffer, Number(packed >> 32n), Number(packed & 0xffffffffn)))); } } }; const noCall = async (nsid) => assert.fail(`no call: ${nsid}`); for (const [engine, run] of [["js", runModule], ["rust", runModuleRust]]) { const loaded = []; const load = async (key) => (loaded.push(key), assert.equal(key, "iroh-user-data"), iroh); const ud = async (input, m = mod) => { const ran = await run(ctx, m, "user_data", input, noCall, { wasm: m.module, load, callFor: () => noCall }); if (ran.error) throw new Error(ran.error); return ran.result.user_data; }; test(`user_data (${engine}): shell, shell and ssh, iroh, ssh only (snapshots); errors`, async () => { assert.equal(await ud({}), snapshot("shell")); assert.deepEqual(loaded, [], "shell alone needs no ssh"); assert.equal(await ud({ shell: true, ssh_key: KEY, webrtc_ssh: true }), snapshot("shell-ssh")); assert.equal(await ud({ ssh_key: KEY, iroh: true }), snapshot("shell-iroh")); assert.equal(await ud({ shell: false, webrtc_ssh: true, ssh_key: KEY }), snapshot("ssh")); assert.equal(await ud({ ssh_key: KEY, webrtc_ssh: true, iroh: true }), snapshot("all")); assert.equal(await ud({ tunnels: [{ tag: "atirc", port: 6680, transport: "webrtc" }] }), snapshot("shell-tunnels")); const alone = JSON.parse((await ud({ shell: false, tunnels: [{ tag: "atirc", port: 6680 }] })).replace("#cloud-config\n", "")); assert.deepEqual(alone.write_files.map((f) => [f.path, f.content]), [["/etc/socialweb-guest-agent/tunnels.json", '{"tunnels":[{"tag":"atirc","port":6680}]}\n']], "a tunnel alone is enough"); for (const bad of [[{ tag: "shell", port: 1 }], [{ tag: "a b", port: 1 }], [{ tag: "x", port: 0 }], [{ tag: "x", port: 1, transport: "iroh" }], [{ tag: "x", port: 1 }, { tag: "x", port: 2 }], "x"]) await assert.rejects(ud({ tunnels: bad }), /tunnels/, JSON.stringify(bad)); await assert.rejects(ud({ shell: false, iroh: true, ssh_key: KEY }), /needs shell or webrtc_ssh/); await assert.rejects(ud({ iroh: true }), /ssh_key/); await assert.rejects(ud({ webrtc_ssh: true, ssh_key: "nope" }), /ssh_key/); await assert.rejects(ud({ shell: "yes" }), /shell must be a boolean/); assert.equal(await ud({ shell: "true", webrtc_ssh: "true", ssh_key: KEY }), snapshot("shell-ssh"), "a form's booleans"); await assert.rejects(ud({ iroh: true, ssh_key: KEY }, { ...mod, hooks: [{ nsid: "computer.socialweb.vm.irohUserData", reason: "r" }] }), /need a computer\.socialweb\.vm\.irohUserData hook/); }); }