236 lines
9.1 KiB
TypeScript
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();
|