Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/wallet-service/src/fullnode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const create = (baseURL = BASE_URL) => {
throw new Error(error.message);
}

return value as FullNodeApiVersionResponse;
return value;
};

const downloadTx = async (txId: string) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet-service/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export interface FullNodeApiVersionResponse {
genesis_block_hash?: string,
genesis_tx1_hash?: string,
genesis_tx2_hash?: string,
native_token?: { name: string, symbol: string };
native_token?: { name: string, symbol: string, version?: number };
}

export interface TxProposal {
Expand Down
56 changes: 56 additions & 0 deletions packages/wallet-service/tests/fullnode.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,60 @@
import fullnode from '@src/fullnode';
import { defaultTestVersionData } from '@tests/utils';

describe('version', () => {
afterEach(() => {
jest.restoreAllMocks();
});

test('returns parsed payload when native_token includes the version field', async () => {
expect.hasAssertions();

const payload = {
...defaultTestVersionData(),
native_token: { name: 'Hathor', symbol: 'HTR', version: 0 },
};
const apiGetSpy = jest.spyOn(fullnode.api, 'get');
apiGetSpy.mockImplementation(() => Promise.resolve({
status: 200,
data: payload,
}));

const response = await fullnode.version();
expect(response).toStrictEqual(payload);
});

test('returns parsed payload when native_token omits the version field', async () => {
expect.hasAssertions();

// defaultTestVersionData() returns a payload whose native_token has no `version`.
const payload = defaultTestVersionData();
const apiGetSpy = jest.spyOn(fullnode.api, 'get');
apiGetSpy.mockImplementation(() => Promise.resolve({
status: 200,
data: payload,
}));

const response = await fullnode.version();
expect(response).toStrictEqual(payload);
expect(response.native_token).not.toHaveProperty('version');
});

test('throws when the response fails schema validation', async () => {
expect.hasAssertions();

const invalidPayload = {
...defaultTestVersionData(),
native_token: { name: 'Hathor', symbol: 'HTR', version: 1.5 },
};
const apiGetSpy = jest.spyOn(fullnode.api, 'get');
apiGetSpy.mockImplementation(() => Promise.resolve({
status: 200,
data: invalidPayload,
}));

await expect(fullnode.version()).rejects.toThrow(/native_token\.version/);
});
});

test('downloadTx', async () => {
expect.hasAssertions();
Expand Down
45 changes: 45 additions & 0 deletions packages/wallet-service/tests/schemas.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { FullnodeVersionSchema } from '@src/schemas';
import { defaultTestVersionData } from '@tests/utils';

// Regression guard for the testnet outage that motivated PR 420: the
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not reference the PR solving the outage if possible, the comment should document the core issue this addresses instead.

I think a link to an issue describing the problem would be good if the core issue was too complex to describe in the comment.

// fullnode added `native_token.version` and the lambda crashed because the
// nested object did not allow it. Locking the modern payload in as a
// passing case prevents that fix from being reverted by accident.
test('FullnodeVersionSchema regression: native_token.version is accepted', () => {
const payload = {
...defaultTestVersionData(),
native_token: { name: 'Hathor', symbol: 'HTR', version: 0 },
};
const { error, value } = FullnodeVersionSchema.validate(payload);
expect(error).toBeUndefined();
expect(value.native_token).toEqual({ name: 'Hathor', symbol: 'HTR', version: 0 });
});

test('FullnodeVersionSchema accepts a legacy fullnode payload without native_token.version', () => {
// defaultTestVersionData() returns a payload whose native_token has no `version`.
const { error } = FullnodeVersionSchema.validate(defaultTestVersionData());
expect(error).toBeUndefined();
});

// Design-intent guard: native_token is intentionally strict — any unknown
// field there fails validation so a contract test (and not a production
// outage) surfaces fullnode schema drift.
test('FullnodeVersionSchema rejects unknown fields under native_token', () => {
const payload = {
...defaultTestVersionData(),
native_token: { name: 'Hathor', symbol: 'HTR', version: 0, icon: 'data:image/png;base64,...' },
};
const { error } = FullnodeVersionSchema.validate(payload);
expect(error).toBeDefined();
expect(error!.message).toMatch(/native_token\.icon/);
});

test('FullnodeVersionSchema rejects a non-integer native_token.version', () => {
const payload = {
...defaultTestVersionData(),
native_token: { name: 'Hathor', symbol: 'HTR', version: 1.5 },
};
const { error } = FullnodeVersionSchema.validate(payload);
expect(error).toBeDefined();
expect(error!.message).toMatch(/native_token\.version/);
});
Loading