addition of new dashboard
This commit is contained in:
225
backend/modules/dashboard/verify.test.js
Normal file
225
backend/modules/dashboard/verify.test.js
Normal file
@@ -0,0 +1,225 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import { summarizeVerification, verifyMany } from "./verify.js";
|
||||
|
||||
// Build the record shape fetchAuditRecords produces.
|
||||
const record = (auditId, sigCount = 1) => ({
|
||||
auditId,
|
||||
audit: { version: 1, hashType: "sha256", digest: `d${auditId}`, created: 100, eventId: "e1" },
|
||||
signatures: Array.from({ length: sigCount }, (_, i) => ({ id: `alice@a`, jws: `x${i}` })),
|
||||
});
|
||||
|
||||
// A verifier verdict as the canonical verifier returns it: the record we passed
|
||||
// in, echoed back on `.audit`, plus its per-check results.
|
||||
const verdict = (rec, results) => ({ audit: rec, results });
|
||||
|
||||
describe("summarizeVerification", () => {
|
||||
it("reports no audit record when nothing was found", () => {
|
||||
expect(summarizeVerification({ eventUuid: "e1", auditId: null }, [], null)).toEqual({
|
||||
eventUuid: "e1",
|
||||
auditId: null,
|
||||
status: "no-audit",
|
||||
verified: false,
|
||||
signatureCount: 0,
|
||||
auditCount: 0,
|
||||
checks: null,
|
||||
reason: "no audit record",
|
||||
});
|
||||
});
|
||||
|
||||
it("marks verified when the record's verdict is in the valid bucket", () => {
|
||||
const r = record(9, 2);
|
||||
const result = { data: { valid: [verdict(r, { idMatch: true, auditSig: true })], invalid: [] } };
|
||||
expect(summarizeVerification({ eventUuid: null, auditId: 9 }, [r], result)).toEqual({
|
||||
eventUuid: null,
|
||||
auditId: 9,
|
||||
status: "verified",
|
||||
verified: true,
|
||||
signatureCount: 2,
|
||||
auditCount: 1,
|
||||
checks: { idMatch: true, auditSig: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("marks invalid and surfaces the failed checks", () => {
|
||||
const r = record(9, 2);
|
||||
const result = { data: { valid: [], invalid: [verdict(r, { auditSig: false })] } };
|
||||
expect(summarizeVerification({ eventUuid: null, auditId: 9 }, [r], result)).toMatchObject({
|
||||
status: "invalid",
|
||||
verified: false,
|
||||
checks: { auditSig: false },
|
||||
});
|
||||
});
|
||||
|
||||
// The regression this whole change is about: one event, two audit records.
|
||||
describe("an event with several audit records", () => {
|
||||
const produce = record(9);
|
||||
const process = record(10);
|
||||
const target = { eventUuid: "e1", auditId: null };
|
||||
|
||||
it("matches each verdict to its own record by audit id, not by position", () => {
|
||||
// Deliberately out of order: `process` is valid, `produce` is not.
|
||||
const result = {
|
||||
data: {
|
||||
valid: [verdict(process, { auditSig: true })],
|
||||
invalid: [verdict(produce, { auditSig: false })],
|
||||
},
|
||||
};
|
||||
const out = summarizeVerification(target, [produce, process], result);
|
||||
expect(out).toMatchObject({ status: "invalid", verified: false, auditCount: 2 });
|
||||
// The failing record's checks are what surface, not the first bucket's.
|
||||
expect(out.checks).toEqual({ auditSig: false });
|
||||
});
|
||||
|
||||
it("only reports verified when EVERY audit record passed", () => {
|
||||
const allGood = {
|
||||
data: { valid: [verdict(produce, { auditSig: true }), verdict(process, { auditSig: true })], invalid: [] },
|
||||
};
|
||||
expect(summarizeVerification(target, [produce, process], allGood)).toMatchObject({
|
||||
status: "verified",
|
||||
verified: true,
|
||||
auditCount: 2,
|
||||
signatureCount: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// A fake pooled client that routes by SQL so we can exercise verifyMany's
|
||||
// orchestration without a DB or the crypto engine. `entity` is matched first so
|
||||
// the audit query's audit_signature JOIN can't shadow it.
|
||||
function routingClient(routes) {
|
||||
const query = jest.fn(async (sql, params) => {
|
||||
if (/FROM entity/.test(sql)) return { rows: routes.entity ?? [] };
|
||||
if (/FROM audit/.test(sql)) return { rows: routes.audit?.(sql, params) ?? [] };
|
||||
return { rows: [] };
|
||||
});
|
||||
return { query, release: jest.fn(async () => {}) };
|
||||
}
|
||||
|
||||
// One audit row as it comes back from Postgres (snake_case + aggregated JSON).
|
||||
const auditRow = (auditId, eventId = "e1") => ({
|
||||
audit_id: auditId,
|
||||
version: 1,
|
||||
event_id: eventId,
|
||||
hash_type: "sha256",
|
||||
digest: `d${auditId}`,
|
||||
created: "100",
|
||||
sender_id: "did:alice",
|
||||
recipient_id: "did:bob",
|
||||
signatures: [{ version: 1, id: "alice@a", signedon: "101", type: "audit", jws: "x" }],
|
||||
});
|
||||
|
||||
describe("verifyMany", () => {
|
||||
it("returns [] when there are no targets", async () => {
|
||||
expect(await verifyMany(1, {})).toEqual([]);
|
||||
expect(await verifyMany(1, { eventUuids: [], auditIds: [] })).toEqual([]);
|
||||
});
|
||||
|
||||
it("verifies an event by assembling its audit records and calling the verifier", async () => {
|
||||
const client = routingClient({
|
||||
entity: [{ did_id: "did:alice", did_doc: { id: "did:alice" } }],
|
||||
audit: () => [auditRow(9)],
|
||||
});
|
||||
const verify = jest.fn(async (input) => ({
|
||||
data: { valid: [{ audit: input.audits[0], results: { ok: true } }], invalid: [] },
|
||||
}));
|
||||
|
||||
const out = await verifyMany(1, { eventUuids: ["e1"] }, { acquire: async () => client, verify });
|
||||
|
||||
expect(out).toEqual([{
|
||||
eventUuid: "e1",
|
||||
auditId: null,
|
||||
status: "verified",
|
||||
verified: true,
|
||||
signatureCount: 1,
|
||||
auditCount: 1,
|
||||
checks: { ok: true },
|
||||
}]);
|
||||
expect(verify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
didDocs: [{ id: "did:alice" }],
|
||||
audits: [expect.objectContaining({ auditId: 9, audit: expect.objectContaining({ eventId: "e1" }) })],
|
||||
}),
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves only the DIDs the batch references, once, and never all entities", async () => {
|
||||
const entityCalls = [];
|
||||
const client = routingClient({
|
||||
entity: [{ did_id: "did:alice", did_doc: { id: "did:alice" } }],
|
||||
audit: (_sql, params) => [auditRow(params[1])],
|
||||
});
|
||||
const inner = client.query;
|
||||
client.query = jest.fn(async (sql, params) => {
|
||||
if (/FROM entity/.test(sql)) entityCalls.push(params);
|
||||
return inner(sql, params);
|
||||
});
|
||||
const verify = jest.fn(async (input) => ({
|
||||
data: { valid: [{ audit: input.audits[0], results: { ok: true } }], invalid: [] },
|
||||
}));
|
||||
|
||||
await verifyMany(1, { auditIds: [9, 10] }, { acquire: async () => client, verify });
|
||||
|
||||
// One lookup for the batch, scoped to the two DIDs on those events.
|
||||
expect(entityCalls).toHaveLength(1);
|
||||
expect(entityCalls[0]).toEqual([1, ["did:alice", "did:bob"]]);
|
||||
// Second target reused the cache rather than re-querying.
|
||||
expect(verify.mock.calls[1][0].didDocs).toEqual([{ id: "did:alice" }]);
|
||||
});
|
||||
|
||||
it("looks an audit-tab target up by audit_id, scoped to the user", async () => {
|
||||
let seen;
|
||||
const client = routingClient({
|
||||
entity: [{ did_id: "did:alice", did_doc: { id: "did:alice" } }],
|
||||
audit: (sql, params) => { seen = { sql, params }; return [auditRow(42)]; },
|
||||
});
|
||||
const verify = jest.fn(async (input) => ({
|
||||
data: { valid: [{ audit: input.audits[0], results: { ok: true } }], invalid: [] },
|
||||
}));
|
||||
|
||||
const out = await verifyMany(1, { auditIds: [42] }, { acquire: async () => client, verify });
|
||||
|
||||
expect(out[0]).toMatchObject({ auditId: 42, eventUuid: null, status: "verified" });
|
||||
expect(seen.sql).toMatch(/a\.audit_id = \$2/);
|
||||
expect(seen.sql).not.toMatch(/a\.event_id = \$2/);
|
||||
// user_id is always $1 — an audit id alone never reaches another tenant's row.
|
||||
expect(seen.params.slice(0, 2)).toEqual([1, 42]);
|
||||
});
|
||||
|
||||
it("gives each audit record of one event its own independent result", async () => {
|
||||
// Both rows belong to event e1; only audit 9 verifies.
|
||||
const client = routingClient({
|
||||
entity: [{ did_id: "did:alice", did_doc: { id: "did:alice" } }],
|
||||
audit: (_sql, params) => [auditRow(params[1])],
|
||||
});
|
||||
const verify = jest.fn(async (input) => {
|
||||
const rec = input.audits[0];
|
||||
return rec.auditId === 9
|
||||
? { data: { valid: [{ audit: rec, results: { ok: true } }], invalid: [] } }
|
||||
: { data: { valid: [], invalid: [{ audit: rec, results: { ok: false } }] } };
|
||||
});
|
||||
|
||||
const out = await verifyMany(1, { auditIds: [9, 10] }, { acquire: async () => client, verify });
|
||||
|
||||
expect(out).toHaveLength(2);
|
||||
expect(out[0]).toMatchObject({ auditId: 9, verified: true });
|
||||
expect(out[1]).toMatchObject({ auditId: 10, verified: false, status: "invalid" });
|
||||
});
|
||||
|
||||
it("reports no audit record when the target has none", async () => {
|
||||
const client = routingClient({ entity: [{ short_name: "alice@a" }], audit: () => [] });
|
||||
const verify = jest.fn();
|
||||
const out = await verifyMany(1, { eventUuids: ["missing"] }, { acquire: async () => client, verify });
|
||||
expect(out[0]).toMatchObject({ status: "no-audit", verified: false, signatureCount: 0, reason: "no audit record" });
|
||||
expect(verify).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("caps the batch at 200 targets regardless of input length", async () => {
|
||||
const client = routingClient({ entity: [{ short_name: "a" }], audit: () => [] });
|
||||
const eventUuids = Array.from({ length: 150 }, (_, i) => `id-${i}`);
|
||||
const auditIds = Array.from({ length: 150 }, (_, i) => i + 1);
|
||||
const out = await verifyMany(1, { eventUuids, auditIds }, { acquire: async () => client, verify: jest.fn() });
|
||||
expect(out).toHaveLength(200);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user