Refactor, Added documentation, moved from avj-cli to avj, update script and added tests
This commit is contained in:
234
test/parse-template.test.ts
Normal file
234
test/parse-template.test.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
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
|
||||
const templateWithoutScripts = { ...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();
|
||||
Reference in New Issue
Block a user