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.
234 lines
6.5 KiB
JavaScript
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();
|
|
});
|