Files
brain/tools/receipt-sign.test.mjs
T

105 lines
4.8 KiB
JavaScript

// tools/receipt-sign.test.mjs
import { describe, it, expect } from 'vitest';
import { canonicalJson, signPayload, verifyReceipt, RECEIPT_DOMAINS } from './receipt-sign.mjs';
describe('доменное разделение подписи (R-31)', () => {
const KEY = 'test-secret-key-123';
it('подпись одного домена НЕ проходит проверку другого домена', () => {
const rec = { a: 1 };
const sig = signPayload(rec, KEY, RECEIPT_DOMAINS.JOURNAL_HEAD);
expect(verifyReceipt({ ...rec, sig }, KEY, RECEIPT_DOMAINS.JOURNAL_HEAD)).toBe(true);
expect(verifyReceipt({ ...rec, sig }, KEY, RECEIPT_DOMAINS.FROZEN_PLAN)).toBe(false);
});
it('RECEIPT_DOMAINS экспортирует различимые домены', () => {
expect(typeof RECEIPT_DOMAINS.FROZEN_PLAN).toBe('string');
expect(RECEIPT_DOMAINS.FROZEN_ARTIFACT).not.toBe(RECEIPT_DOMAINS.FROZEN_PLAN);
});
it('без домена (default) — обратная совместимость сохранена', () => {
const sig = signPayload({ a: 1 }, KEY);
expect(verifyReceipt({ a: 1, sig }, KEY)).toBe(true);
});
});
describe('receipt-sign: домен M5_GREEN (Пакет 3.1)', () => {
const KEY = 'test-secret-key-123';
it('RECEIPT_DOMAINS.M5_GREEN — отдельный строковый домен m5-green', () => {
expect(typeof RECEIPT_DOMAINS.M5_GREEN).toBe('string');
expect(RECEIPT_DOMAINS.M5_GREEN).toBe('m5-green');
});
it('M5_GREEN отличается от всех прочих доменов', () => {
expect(RECEIPT_DOMAINS.M5_GREEN).not.toBe(RECEIPT_DOMAINS.JOURNAL_HEAD);
expect(RECEIPT_DOMAINS.M5_GREEN).not.toBe(RECEIPT_DOMAINS.FROZEN_PLAN);
expect(RECEIPT_DOMAINS.M5_GREEN).not.toBe(RECEIPT_DOMAINS.FROZEN_ARTIFACT);
expect(RECEIPT_DOMAINS.M5_GREEN).not.toBe(RECEIPT_DOMAINS.APPROVAL);
expect(RECEIPT_DOMAINS.M5_GREEN).not.toBe(RECEIPT_DOMAINS.STEP_PTR);
});
it('подпись M5_GREEN НЕ принимается за другой домен (cross-domain)', () => {
const rec = { criterion_id: 'c1', occurrence: 1 };
const sig = signPayload(rec, KEY, RECEIPT_DOMAINS.M5_GREEN);
expect(verifyReceipt({ ...rec, sig }, KEY, RECEIPT_DOMAINS.M5_GREEN)).toBe(true);
expect(verifyReceipt({ ...rec, sig }, KEY, RECEIPT_DOMAINS.JOURNAL_HEAD)).toBe(false);
expect(verifyReceipt({ ...rec, sig }, KEY, RECEIPT_DOMAINS.FROZEN_PLAN)).toBe(false);
});
});
describe('VERIFY_PASS domain (G1)', () => {
const KEY = 'test-secret-key-123';
it('VERIFY_PASS определён как verify-pass и отделён от M5_GREEN (R-31)', () => {
expect(RECEIPT_DOMAINS.VERIFY_PASS).toBe('verify-pass');
const sig = signPayload({ a: 1 }, KEY, RECEIPT_DOMAINS.VERIFY_PASS);
expect(verifyReceipt({ a: 1, sig }, KEY, RECEIPT_DOMAINS.VERIFY_PASS)).toBe(true);
expect(verifyReceipt({ a: 1, sig }, KEY, RECEIPT_DOMAINS.M5_GREEN)).toBe(false);
});
});
describe('canonicalJson', () => {
it('sorts object keys deterministically', () => {
expect(canonicalJson({ b: 1, a: 2 })).toBe('{"a":2,"b":1}');
});
it('is stable regardless of insertion order', () => {
expect(canonicalJson({ a: 1, b: 2 })).toBe(canonicalJson({ b: 2, a: 1 }));
});
it('recurses into nested objects and arrays', () => {
expect(canonicalJson({ z: [3, { y: 1, x: 2 }], a: 0 }))
.toBe('{"a":0,"z":[3,{"x":2,"y":1}]}');
});
it('no whitespace', () => {
expect(canonicalJson({ a: 1 })).not.toMatch(/\s/);
});
});
describe('signPayload / verifyReceipt', () => {
const KEY = 'test-secret-key-123';
it('signPayload returns a 64-char hex HMAC', () => {
const sig = signPayload({ a: 1 }, KEY);
expect(sig).toMatch(/^[0-9a-f]{64}$/);
});
it('signPayload returns null without a key', () => {
expect(signPayload({ a: 1 }, null)).toBe(null);
expect(signPayload({ a: 1 }, '')).toBe(null);
});
it('verifyReceipt true for a correctly signed record', () => {
const rec = { kind: 'x', n: 5 };
const sig = signPayload(rec, KEY);
expect(verifyReceipt({ ...rec, sig }, KEY)).toBe(true);
});
it('verifyReceipt false when payload tampered after signing', () => {
const rec = { kind: 'x', n: 5 };
const sig = signPayload(rec, KEY);
expect(verifyReceipt({ ...rec, n: 6, sig }, KEY)).toBe(false);
});
it('verifyReceipt false for a wrong key', () => {
const rec = { kind: 'x', n: 5 };
const sig = signPayload(rec, KEY);
expect(verifyReceipt({ ...rec, sig }, 'other-key')).toBe(false);
});
it('verifyReceipt false for an unsigned record (no key → no trust)', () => {
expect(verifyReceipt({ kind: 'x', n: 5 }, KEY)).toBe(false);
});
it('verifyReceipt false when verifying key is null', () => {
const sig = signPayload({ a: 1 }, KEY);
expect(verifyReceipt({ a: 1, sig }, null)).toBe(false);
});
});