open-standards/openpay/crates/op-wasm/js/test.mjs
Transaction Science 70ecd6caa0 Initial import — three open standards under one shelf
Transaction Science open standards monorepo. Each subdirectory is a
self-contained reference protocol with its own README and licence.

- openpay/    Apache-2.0  payment-acceptance stack (Rust, 26 crates)
- smart-byte/ CC-BY-4.0   content-addressed value carrier (spec)
- eoc/        CC-BY-4.0   energy-optimised compute (spec)

The wire format and the right to fork are public. The optional hosted
services are the offer; the protocols are not.
2026-05-23 07:58:09 -04:00

234 lines
6.5 KiB
JavaScript

// JS-side smoke tests for the wasm-pack bundled output.
//
// Run after `bash scripts/build-wasm.sh --target nodejs`:
//
// node js/test.mjs
//
// Covers what the Rust-side wasm-bindgen-test cannot: the
// JS-visible OpenPayError shape, the explicit `.free()` ownership
// protocol, and the TypeScript-friendly camelCase property names.
import assert from "node:assert/strict";
import { test } from "node:test";
import {
CardData,
HeuristicScorer,
OpenPayError,
RustVault,
TokenFormat,
TokenLifetime,
TokenizationPolicy,
VaultRef,
} from "../pkg/op_wasm.js";
const VALID_VISA = "4242424242424242";
const VALID_MC = "5555555555554444";
test("CardData constructs and exposes camelCase getters", () => {
const card = new CardData(VALID_VISA, 12, 2030);
assert.equal(card.firstSix, "424242");
assert.equal(card.lastFour, "4242");
assert.equal(card.expMonth, 12);
assert.equal(card.expYear, 2030);
card.free();
});
test("CardData rejects invalid PAN with OpenPayError.InvalidInput", () => {
let thrown = null;
try {
new CardData("1111111111111111", 12, 2030);
} catch (e) {
thrown = e;
}
assert.ok(thrown !== null, "expected throw");
assert.equal(thrown.code, 1);
assert.equal(thrown.kind, "InvalidInput");
assert.ok(thrown.message.length > 0);
});
test("CardData rejects bad expiration", () => {
for (const [m, y] of [[13, 2030], [0, 2030]]) {
assert.throws(() => new CardData(VALID_VISA, m, y));
}
});
test("CardData methods throw after .free() (wasm-bindgen null-check)", () => {
const card = new CardData(VALID_VISA, 12, 2030);
card.free();
assert.throws(() => card.firstSix);
});
test("RustVault round-trip via classes", () => {
const vault = new RustVault("test");
assert.equal(vault.name, "test");
const card = new CardData(VALID_VISA, 12, 2030);
const token = vault.tokenize(card);
// card was consumed (moved by-value across the boundary); JS-side
// pointer is now null.
assert.throws(() => card.firstSix);
assert.ok(token.asString.startsWith("tok_v7_"));
const recovered = vault.detokenize(token);
assert.equal(recovered.lastFour, "4242");
recovered.free();
token.free();
vault.free();
});
test("Unknown token throws VaultLookupFailed", () => {
const vault = new RustVault("err");
const fake = VaultRef.fromString("tok_v7_doesnotexist");
try {
vault.detokenize(fake);
assert.fail("expected throw");
} catch (e) {
assert.equal(e.code, 2);
assert.equal(e.kind, "VaultLookupFailed");
}
fake.free();
vault.free();
});
test("Malformed token also collapses to VaultLookupFailed (oracle discipline)", () => {
const vault = new RustVault("err");
const bad = VaultRef.fromString("not-a-token");
try {
vault.detokenize(bad);
assert.fail("expected throw");
} catch (e) {
assert.equal(e.code, 2);
assert.equal(e.kind, "VaultLookupFailed");
}
bad.free();
vault.free();
});
test("Single-use token throws TokenAlreadyConsumed on second use", () => {
const vault = new RustVault("single");
const card = new CardData(VALID_VISA, 12, 2030);
const token = vault.tokenize(card, TokenizationPolicy.singleUse(120));
// First detokenize succeeds.
vault.detokenize(token).free();
// Second detokenize fails.
try {
vault.detokenize(token);
assert.fail("expected throw");
} catch (e) {
assert.equal(e.code, 4);
assert.equal(e.kind, "TokenAlreadyConsumed");
}
token.free();
vault.free();
});
test("Distinct tokens for same PAN (random format default)", () => {
const vault = new RustVault("uniq");
const t1 = vault.tokenize(new CardData(VALID_VISA, 12, 2030));
const t2 = vault.tokenize(new CardData(VALID_VISA, 12, 2030));
assert.notEqual(t1.asString, t2.asString);
t1.free();
t2.free();
vault.free();
});
test("Deterministic format produces same token for same PAN", () => {
const vault = new RustVault("det");
const policy = new TokenizationPolicy();
policy.format = TokenFormat.Deterministic;
const t1 = vault.tokenize(new CardData(VALID_VISA, 12, 2030), policy);
const t2 = vault.tokenize(new CardData(VALID_VISA, 12, 2030), policy);
assert.equal(t1.asString, t2.asString);
t1.free();
t2.free();
vault.free();
});
test("exists() and delete()", () => {
const vault = new RustVault("del");
const token = vault.tokenize(new CardData(VALID_VISA, 12, 2030));
assert.equal(vault.exists(token), true);
assert.equal(vault.delete(token), true);
assert.equal(vault.exists(token), false);
assert.equal(vault.delete(token), false); // idempotent
token.free();
vault.free();
});
test("HeuristicScorer.name is stable", () => {
const s = new HeuristicScorer();
assert.equal(s.name, "heuristic-v1");
s.free();
});
test("TokenizationPolicy factory helpers", () => {
const single = TokenizationPolicy.singleUse(60);
assert.equal(single.lifetime, TokenLifetime.SingleUse);
assert.equal(single.ttlSeconds, 60n); // u64 → BigInt on JS side
const cof = TokenizationPolicy.cardOnFile();
assert.equal(cof.lifetime, TokenLifetime.Reusable);
assert.equal(cof.ttlSeconds, 0n);
});
test("PAN never appears in token string (opacity invariant)", () => {
const vault = new RustVault("opacity");
const token = vault.tokenize(new CardData(VALID_VISA, 12, 2030));
const s = token.asString;
assert.ok(!s.includes(VALID_VISA));
assert.ok(!s.includes("424242"));
assert.ok(!s.includes("4242"));
token.free();
vault.free();
});
test("Multiple cards coexist", () => {
const vault = new RustVault("multi");
const visa = vault.tokenize(new CardData(VALID_VISA, 12, 2030));
const mc = vault.tokenize(new CardData(VALID_MC, 11, 2028));
const visaRecovered = vault.detokenize(visa);
assert.equal(visaRecovered.lastFour, "4242");
visaRecovered.free();
const mcRecovered = vault.detokenize(mc);
assert.equal(mcRecovered.lastFour, "4444");
assert.equal(mcRecovered.expMonth, 11);
mcRecovered.free();
visa.free();
mc.free();
vault.free();
});
test("Two vaults with the same name are isolated", () => {
const a = new RustVault("same");
const b = new RustVault("same");
const token = a.tokenize(new CardData(VALID_VISA, 12, 2030));
assert.throws(() => b.detokenize(token));
token.free();
a.free();
b.free();
});
test("OpenPayError toString includes code and kind", () => {
const vault = new RustVault("e");
const fake = VaultRef.fromString("tok_v7_no");
try {
vault.detokenize(fake);
} catch (e) {
const s = e.toString();
assert.ok(s.startsWith("OpenPayError ["));
assert.ok(s.includes("VaultLookupFailed"));
}
fake.free();
vault.free();
});