Files
xo-cash-utils/test/parse-template.test.ts
2026-07-18 17:03:39 +00:00

236 lines
9.1 KiB
TypeScript

import { expect, test } from 'vitest';
import type { XOTemplate } from '@xo-cash/types';
import { p2pkhTemplate } from '@xo-cash/templates';
import { TemplateInvalidError, parseTemplate, serializeTemplate } from '../source/index.ts';
/**
* Tests that parseTemplate accepts a valid XOTemplate object and returns it unchanged.
*/
const testParseTemplateAcceptsValidTemplate = (): void => {
// Parse the template
const parsedObjectTemplate: XOTemplate = parseTemplate(p2pkhTemplate);
// Parse the template from a string
const parsedStringTemplate = parseTemplate(serializeTemplate(p2pkhTemplate));
// The parsed template should be equal to the original template
expect(parsedObjectTemplate).toEqual(p2pkhTemplate);
expect(parsedStringTemplate).toEqual(p2pkhTemplate);
};
/**
* Tests that parseTemplate accepts a simple bigint value in a satoshi field and preserves it
* through the serialize/deserialize round-trip.
*/
const testParseTemplateAcceptsBigintSatoshis = (): void => {
// Get the first output key
const firstOutputKey = Object.keys(p2pkhTemplate.outputs)[0];
if (firstOutputKey === undefined) {
throw new Error('p2pkhTemplate has no outputs, test fixture is invalid');
}
const firstOutput = p2pkhTemplate.outputs[firstOutputKey];
if (firstOutput === undefined) {
throw new Error('p2pkhTemplate first output is undefined, test fixture is invalid');
}
const templateWithBigint = {
...p2pkhTemplate,
outputs: {
...p2pkhTemplate.outputs,
[firstOutputKey]: { ...firstOutput, valueSatoshis: 1000n },
},
};
// Parse the template
const parsedTemplate = parseTemplate(templateWithBigint);
// The parsed template should have the bigint value
expect(parsedTemplate.outputs[firstOutputKey]!.valueSatoshis).toBe(1000n);
};
/**
* Tests that parseTemplate preserves bigint precision for values exceeding Number.MAX_SAFE_INTEGER.
* Uses 2^54 + 1, so any naive Number conversion in the serialize/deserialize round-trip would produce the wrong value
* but BigInt conversion would preserve the exact value.
*/
const testParseTemplatePreservesBigintPrecisionBeyondMaxSafeInteger = (): void => {
// Get the first output key
const firstOutputKey = Object.keys(p2pkhTemplate.outputs)[0];
if (firstOutputKey === undefined) {
throw new Error('p2pkhTemplate has no outputs, test fixture is invalid');
}
const firstOutput = p2pkhTemplate.outputs[firstOutputKey];
if (firstOutput === undefined) {
throw new Error('p2pkhTemplate first output is undefined, test fixture is invalid');
}
const templateWithBigint = {
...p2pkhTemplate,
outputs: {
...p2pkhTemplate.outputs,
[firstOutputKey]: { ...firstOutput, valueSatoshis: 18014398509481985n },
},
};
// Parse the template
const parsedTemplate = parseTemplate(templateWithBigint);
// The parsed template should have the exact bigint value
expect(parsedTemplate.outputs[firstOutputKey]!.valueSatoshis).toBe(18014398509481985n);
};
/**
* Tests that parseTemplate throws TemplateInvalidError listing all missing required fields
*/
const testParseTemplateThrowsOnMissingRequiredFields = (): void => {
let thrownError: unknown;
try {
parseTemplate({} as XOTemplate);
} catch (caughtError) {
thrownError = caughtError;
}
const isTemplateInvalidError = thrownError instanceof TemplateInvalidError;
expect(isTemplateInvalidError).toBe(true);
const expectedMessage =
'Template invalid: \n'
+ '- name: expected string, received undefined\n'
+ '- description: expected string, received undefined\n'
+ '- $schema: expected string, received undefined\n'
+ '- supported: expected array, received undefined\n'
+ '- roles: expected record, received undefined\n'
+ '- start: expected array, received undefined\n'
+ '- actions: expected record, received undefined\n'
+ '- inputs: expected record, received undefined\n'
+ '- outputs: expected record, received undefined\n'
+ '- lockingScripts: expected record, received undefined\n'
+ '- scripts: expected record, received undefined';
expect((thrownError as TemplateInvalidError).message).toBe(expectedMessage);
};
/**
* Tests that parseTemplate throws TemplateInvalidError naming the exact field path when a
* single required field is removed from a valid template.
*/
const testParseTemplateThrowsOnMissingScriptsField = (): void => {
// Set scripts to undefined to test the field path for a missing record
// @ts-expect-error - scripts is intentionally undefined for this test case
const templateWithoutScripts: XOTemplate = { ...p2pkhTemplate, scripts: undefined };
let thrownError: unknown;
try {
parseTemplate(templateWithoutScripts);
} catch (caughtError) {
thrownError = caughtError;
}
const isTemplateInvalidError = thrownError instanceof TemplateInvalidError;
expect(isTemplateInvalidError).toBe(true);
const expectedMessage = 'Template invalid: \n' + '- scripts: expected record, received undefined';
expect((thrownError as TemplateInvalidError).message).toBe(expectedMessage);
};
/**
* Tests that parseTemplate throws TemplateInvalidError reporting both field paths for type violations
*/
const testParseTemplateThrowsOnFieldTypeViolations = (): void => {
// Replace version with a number and name with a number to produce two simultaneous type errors.
// @ts-expect-error - version and name are intentionally wrong types for this test case
const templateWithWrongFieldTypes: XOTemplate = { ...p2pkhTemplate, version: 42, name: 42 };
let thrownError: unknown;
try {
parseTemplate(templateWithWrongFieldTypes);
} catch (caughtError) {
thrownError = caughtError;
}
const isTemplateInvalidError = thrownError instanceof TemplateInvalidError;
expect(isTemplateInvalidError).toBe(true);
const expectedMessage = 'Template invalid: \n' + '- name: expected string, received number\n' + '- version: expected string, received number';
expect((thrownError as TemplateInvalidError).message).toBe(expectedMessage);
};
/**
* Tests that parseTemplate throws TemplateInvalidError with the unknown key path for an unrecognized property at the top level
*/
const testParseTemplateThrowsOnUnknownProperties = (): void => {
const templateWithUnknownProperty = { ...p2pkhTemplate, unknownProperty: 'unexpected' };
let thrownError: unknown;
try {
parseTemplate(templateWithUnknownProperty);
} catch (caughtError) {
thrownError = caughtError;
}
const isTemplateInvalidError = thrownError instanceof TemplateInvalidError;
expect(isTemplateInvalidError).toBe(true);
const expectedMessage = 'Template invalid: \n' + '- (root): Unrecognized key: "unknownProperty"';
expect((thrownError as TemplateInvalidError).message).toBe(expectedMessage);
};
/**
* Tests that parseTemplate throws TemplateInvalidError reporting both unrecognized keys for unknown properties nested inside an action definition
*/
const testParseTemplateThrowsOnDeepUnknownProperties = (): void => {
if (p2pkhTemplate.actions.receive === undefined) {
throw new Error('p2pkhTemplate has no "receive" action, test fixture is invalid');
}
const templateWithDeepUnknown = {
...p2pkhTemplate,
actions: {
...p2pkhTemplate.actions,
receive: { ...p2pkhTemplate.actions.receive, new: 42, new2: 'test' },
},
};
let thrownError: unknown;
try {
parseTemplate(templateWithDeepUnknown);
} catch (caughtError) {
thrownError = caughtError;
}
const isTemplateInvalidError = thrownError instanceof TemplateInvalidError;
expect(isTemplateInvalidError).toBe(true);
const expectedMessage = 'Template invalid: \n' + '- actions.receive: Unrecognized keys: "new", "new2"';
expect((thrownError as TemplateInvalidError).message).toBe(expectedMessage);
};
const runTests = async (): Promise<void> => {
test('parseTemplate: accepts a valid template', testParseTemplateAcceptsValidTemplate);
test('parseTemplate: accepts native bigint in satoshi fields', testParseTemplateAcceptsBigintSatoshis);
test('parseTemplate: preserves bigint precision beyond Number.MAX_SAFE_INTEGER', testParseTemplatePreservesBigintPrecisionBeyondMaxSafeInteger);
test('parseTemplate: throws TemplateInvalidError on missing required fields', testParseTemplateThrowsOnMissingRequiredFields);
test('parseTemplate: throws TemplateInvalidError on missing scripts field', testParseTemplateThrowsOnMissingScriptsField);
test('parseTemplate: throws TemplateInvalidError on field type violations', testParseTemplateThrowsOnFieldTypeViolations);
test('parseTemplate: throws TemplateInvalidError on unknown top-level property', testParseTemplateThrowsOnUnknownProperties);
test('parseTemplate: throws TemplateInvalidError on deep unknown properties', testParseTemplateThrowsOnDeepUnknownProperties);
};
await runTests();