PR C: wire real NotaryRegistry on Chain 138 (arch step 4) (#7)
Some checks failed
CI / Frontend Lint (push) Failing after 6s
CI / Frontend Type Check (push) Failing after 6s
CI / Frontend Build (push) Failing after 6s
CI / Frontend E2E Tests (push) Failing after 8s
CI / Contracts Compile (push) Has been cancelled
CI / Contracts Test (push) Has been cancelled
CI / Orchestrator Build (push) Has been cancelled
Security Scan / OWASP ZAP Scan (push) Has been cancelled
Security Scan / Dependency Vulnerability Scan (push) Has been cancelled
Some checks failed
CI / Frontend Lint (push) Failing after 6s
CI / Frontend Type Check (push) Failing after 6s
CI / Frontend Build (push) Failing after 6s
CI / Frontend E2E Tests (push) Failing after 8s
CI / Contracts Compile (push) Has been cancelled
CI / Contracts Test (push) Has been cancelled
CI / Orchestrator Build (push) Has been cancelled
Security Scan / OWASP ZAP Scan (push) Has been cancelled
Security Scan / Dependency Vulnerability Scan (push) Has been cancelled
This commit was merged in pull request #7.
This commit is contained in:
69
orchestrator/tests/unit/exceptionManager.test.ts
Normal file
69
orchestrator/tests/unit/exceptionManager.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { describe, it, expect } from "@jest/globals";
|
||||
import {
|
||||
Business,
|
||||
Control,
|
||||
Data,
|
||||
SettlementException,
|
||||
Timing,
|
||||
classify,
|
||||
route,
|
||||
} from "../../src/services/exceptionManager";
|
||||
|
||||
describe("ExceptionManager — architecture note §12", () => {
|
||||
describe("classification taxonomy", () => {
|
||||
it("builds the four §12 classes via factory functions", () => {
|
||||
expect(Timing.dispatch().exceptionClass).toBe("timing");
|
||||
expect(Timing.dispatch().code).toBe("dispatch_timeout");
|
||||
|
||||
expect(Data.valueMismatch().exceptionClass).toBe("data");
|
||||
expect(Data.documentHashMismatch().code).toBe("document_hash_mismatch");
|
||||
|
||||
expect(Control.unauthorized("nobody").exceptionClass).toBe("control");
|
||||
expect(Control.duplicate("ev-1").code).toBe("duplicate_event");
|
||||
|
||||
expect(Business.manualStop("operator halted").exceptionClass).toBe("business");
|
||||
expect(Business.policyViolation({ rule: "LTV" }).code).toBe("policy_rule_violation");
|
||||
});
|
||||
|
||||
it("classify() tags network/timeout errors as system/network_error", () => {
|
||||
const ex = classify(new Error("ETIMEDOUT connect"));
|
||||
expect(ex.exceptionClass).toBe("system");
|
||||
expect(ex.code).toBe("network_error");
|
||||
});
|
||||
|
||||
it("classify() tags postgres errors as system/database_error", () => {
|
||||
const ex = classify(new Error("postgres connection refused"));
|
||||
expect(ex.exceptionClass).toBe("system");
|
||||
expect(ex.code).toBe("database_error");
|
||||
});
|
||||
|
||||
it("classify() is idempotent for SettlementException inputs", () => {
|
||||
const original = Data.valueMismatch({ field: "amount" });
|
||||
expect(classify(original)).toBe(original);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deterministic routing", () => {
|
||||
const cases: Array<[SettlementException, string]> = [
|
||||
[Timing.dispatch(), "retry"],
|
||||
[Timing.settlement(), "retry"],
|
||||
[Data.valueMismatch(), "abort_transaction"],
|
||||
[Data.documentHashMismatch(), "abort_transaction"],
|
||||
[Control.missingApproval(), "escalate"],
|
||||
[Control.unauthorized("x"), "escalate"],
|
||||
[Control.duplicate("ev"), "dead_letter"],
|
||||
[Business.manualStop("halt"), "abort_transaction"],
|
||||
[Business.policyViolation({ rule: "LTV" }), "escalate"],
|
||||
];
|
||||
|
||||
it.each(cases)("routes %j to %s", (ex, expected) => {
|
||||
expect(route(ex)).toBe(expected);
|
||||
});
|
||||
|
||||
it("network errors retry; non-network system errors dead-letter", () => {
|
||||
expect(route(classify(new Error("ETIMEDOUT")))).toBe("retry");
|
||||
const dbErr = classify(new Error("postgres broken"));
|
||||
expect(route(dbErr)).toBe("dead_letter");
|
||||
});
|
||||
});
|
||||
});
|
||||
62
orchestrator/tests/unit/notaryChain.test.ts
Normal file
62
orchestrator/tests/unit/notaryChain.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect, beforeEach } from "@jest/globals";
|
||||
import {
|
||||
__resetForTests,
|
||||
anchorPlan,
|
||||
computePlanHash,
|
||||
finalizeAnchor,
|
||||
planIdToBytes32,
|
||||
} from "../../src/services/notaryChain";
|
||||
import type { Plan } from "../../src/types/plan";
|
||||
|
||||
const FIXTURE_PLAN: Plan = {
|
||||
plan_id: "11111111-2222-3333-4444-555555555555",
|
||||
creator: "0xabc",
|
||||
steps: [{ type: "pay", amount: 100, asset: "USD" }],
|
||||
};
|
||||
|
||||
describe("NotaryChain adapter", () => {
|
||||
beforeEach(() => __resetForTests());
|
||||
|
||||
describe("helpers", () => {
|
||||
it("planIdToBytes32 is deterministic and 32 bytes", () => {
|
||||
const a = planIdToBytes32("p-1");
|
||||
const b = planIdToBytes32("p-1");
|
||||
expect(a).toBe(b);
|
||||
expect(a).toMatch(/^0x[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
it("planIdToBytes32 collision-resistant across different ids", () => {
|
||||
expect(planIdToBytes32("a")).not.toBe(planIdToBytes32("b"));
|
||||
});
|
||||
|
||||
it("computePlanHash is deterministic and sha256", () => {
|
||||
const h1 = computePlanHash(FIXTURE_PLAN);
|
||||
const h2 = computePlanHash(FIXTURE_PLAN);
|
||||
expect(h1).toBe(h2);
|
||||
expect(h1).toMatch(/^0x[0-9a-f]{64}$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mock fallback (envs unset)", () => {
|
||||
it("anchorPlan returns mode=mock with planHash when unconfigured", async () => {
|
||||
const result = await anchorPlan(FIXTURE_PLAN, {});
|
||||
expect(result.mode).toBe("mock");
|
||||
expect(result.planHash).toMatch(/^0x[0-9a-f]{64}$/);
|
||||
expect(result.txHash).toBeUndefined();
|
||||
});
|
||||
|
||||
it("finalizeAnchor returns mode=mock when unconfigured", async () => {
|
||||
const result = await finalizeAnchor(FIXTURE_PLAN.plan_id!, true, {});
|
||||
expect(result.mode).toBe("mock");
|
||||
expect(result.txHash).toBeUndefined();
|
||||
});
|
||||
|
||||
it("anchorPlan stays on the mock path when only some envs are set", async () => {
|
||||
const result = await anchorPlan(FIXTURE_PLAN, {
|
||||
rpcUrl: "https://rpc.d-bis.org",
|
||||
// contractAddress + privateKey missing
|
||||
});
|
||||
expect(result.mode).toBe("mock");
|
||||
});
|
||||
});
|
||||
});
|
||||
82
orchestrator/tests/unit/planValidation.instrument.test.ts
Normal file
82
orchestrator/tests/unit/planValidation.instrument.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { describe, it, expect } from "@jest/globals";
|
||||
import { validatePlan } from "../../src/services/planValidation";
|
||||
import type { InstrumentTerms, Plan } from "../../src/types/plan";
|
||||
|
||||
const goodTerms: InstrumentTerms = {
|
||||
applicant: "Solace Bank Group PLC",
|
||||
issuingBankBIC: "SOLBAE22",
|
||||
beneficiaryBankBIC: "MEBLAEAD", // Emirates Islamic BIC prefix example
|
||||
beneficiaryName: "Acme Trading LLC",
|
||||
beneficiaryAccount: "AE070331234567890123456",
|
||||
amount: 1_000_000,
|
||||
currency: "USD",
|
||||
tenor: "90D",
|
||||
expiryDate: "2026-06-30",
|
||||
placeOfPresentation: "Dubai, UAE",
|
||||
governingLaw: "URDG 758",
|
||||
templateRef: "EIB-SBLC-v3.2",
|
||||
templateHash:
|
||||
"a".repeat(64), // dummy sha256
|
||||
};
|
||||
|
||||
function planWith(terms: Partial<InstrumentTerms> | null): Plan {
|
||||
return {
|
||||
creator: "solace-ops-01",
|
||||
steps: [
|
||||
{
|
||||
type: "issueInstrument",
|
||||
amount: terms?.amount ?? 1_000_000,
|
||||
instrument: terms === null ? undefined : ({ ...goodTerms, ...terms } as InstrumentTerms),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
describe("validatePlan — issueInstrument step", () => {
|
||||
it("accepts a well-formed SBLC step", () => {
|
||||
const result = validatePlan(planWith({}));
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("rejects a step missing the instrument object", () => {
|
||||
const result = validatePlan(planWith(null));
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]).toMatch(/missing instrument terms/);
|
||||
});
|
||||
|
||||
it("rejects an invalid BIC", () => {
|
||||
const result = validatePlan(planWith({ issuingBankBIC: "NOTABIC" }));
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.join("\n")).toMatch(/issuingBankBIC is not a valid BIC/);
|
||||
});
|
||||
|
||||
it("rejects a non-ISO-4217 currency", () => {
|
||||
const result = validatePlan(planWith({ currency: "usd" }));
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.join("\n")).toMatch(/currency must be ISO 4217/);
|
||||
});
|
||||
|
||||
it("rejects a non-ISO-8601 expiry date", () => {
|
||||
const result = validatePlan(planWith({ expiryDate: "30-06-2026" }));
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.join("\n")).toMatch(/expiryDate must be YYYY-MM-DD/);
|
||||
});
|
||||
|
||||
it("rejects a non-sha256 template hash", () => {
|
||||
const result = validatePlan(planWith({ templateHash: "deadbeef" }));
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.join("\n")).toMatch(/templateHash must be 64 hex chars/);
|
||||
});
|
||||
|
||||
it("rejects an instrument with non-positive amount", () => {
|
||||
const result = validatePlan(planWith({ amount: 0 }));
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.join("\n")).toMatch(/instrument.amount must be > 0/);
|
||||
});
|
||||
|
||||
it("accepts 11-char branched BIC", () => {
|
||||
const result = validatePlan(planWith({ issuingBankBIC: "SOLBAE22XXX" }));
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
85
orchestrator/tests/unit/transactionState.test.ts
Normal file
85
orchestrator/tests/unit/transactionState.test.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { describe, it, expect } from "@jest/globals";
|
||||
import {
|
||||
ALLOWED_TRANSITIONS,
|
||||
ROLE_FOR_TRANSITION,
|
||||
SOD_REQUIRED_TRANSITIONS,
|
||||
TRANSACTION_STATES,
|
||||
canTransition,
|
||||
} from "../../src/types/transactionState";
|
||||
|
||||
describe("Transaction state machine (architecture note §8–§9)", () => {
|
||||
it("declares the 12 states from §8.1", () => {
|
||||
expect(TRANSACTION_STATES).toEqual([
|
||||
"DRAFT",
|
||||
"INITIATED",
|
||||
"PRECONDITIONS_PENDING",
|
||||
"READY_FOR_PREPARE",
|
||||
"PREPARED",
|
||||
"EXECUTING",
|
||||
"PARTIALLY_EXECUTED",
|
||||
"VALIDATING",
|
||||
"COMMITTED",
|
||||
"ABORTED",
|
||||
"UNWIND_PENDING",
|
||||
"CLOSED",
|
||||
]);
|
||||
});
|
||||
|
||||
describe("§9.1 permitted high-level transitions", () => {
|
||||
// Each of these is listed in the note; canTransition must accept them.
|
||||
const legal: Array<[string, string]> = [
|
||||
["DRAFT", "INITIATED"],
|
||||
["INITIATED", "PRECONDITIONS_PENDING"],
|
||||
["PRECONDITIONS_PENDING", "READY_FOR_PREPARE"],
|
||||
["READY_FOR_PREPARE", "PREPARED"],
|
||||
["PREPARED", "EXECUTING"],
|
||||
["EXECUTING", "PARTIALLY_EXECUTED"],
|
||||
["EXECUTING", "VALIDATING"],
|
||||
["PARTIALLY_EXECUTED", "VALIDATING"],
|
||||
["VALIDATING", "COMMITTED"],
|
||||
["VALIDATING", "ABORTED"],
|
||||
["ABORTED", "UNWIND_PENDING"],
|
||||
["COMMITTED", "CLOSED"],
|
||||
["UNWIND_PENDING", "CLOSED"],
|
||||
];
|
||||
it.each(legal)("allows %s -> %s", (from, to) => {
|
||||
expect(canTransition(from as any, to as any)).toBe(true);
|
||||
});
|
||||
|
||||
// A few illegal edges — explicitly not in §9.1.
|
||||
const illegal: Array<[string, string]> = [
|
||||
["DRAFT", "COMMITTED"],
|
||||
["INITIATED", "EXECUTING"],
|
||||
["CLOSED", "INITIATED"],
|
||||
["PREPARED", "COMMITTED"],
|
||||
["COMMITTED", "ABORTED"],
|
||||
["ABORTED", "COMMITTED"],
|
||||
];
|
||||
it.each(illegal)("rejects %s -> %s", (from, to) => {
|
||||
expect(canTransition(from as any, to as any)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("CLOSED is a terminal state", () => {
|
||||
expect(ALLOWED_TRANSITIONS.CLOSED).toEqual([]);
|
||||
});
|
||||
|
||||
describe("segregation-of-duties checkpoints (§13)", () => {
|
||||
it("flags the four SoD-gated transitions", () => {
|
||||
expect([...SOD_REQUIRED_TRANSITIONS].sort()).toEqual(
|
||||
[
|
||||
"ABORTED->UNWIND_PENDING",
|
||||
"PREPARED->EXECUTING",
|
||||
"READY_FOR_PREPARE->PREPARED",
|
||||
"VALIDATING->COMMITTED",
|
||||
].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it("assigns a role to every SoD-gated transition", () => {
|
||||
for (const key of SOD_REQUIRED_TRANSITIONS) {
|
||||
expect(ROLE_FOR_TRANSITION[key]).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user