Skip to content

Commit 13a31fb

Browse files
authored
Merge pull request #263 from hack-a-chain-software/unit-tests
tests: more unit tests
2 parents e04e69f + 7d5a4d7 commit 13a31fb

File tree

8 files changed

+603
-3
lines changed

8 files changed

+603
-3
lines changed

indexer/src/utils/chainweb-node.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@ import { PactQueryResponse } from '../kadena-server/config/graphql-types';
2222
* @returns A normalized numeric balance value or 0 if no valid balance is found
2323
*/
2424
export const formatBalance_NODE = (queryResult: PactQueryResponse) => {
25-
const resultParsed = JSON.parse(queryResult.result ?? '{}');
25+
let resultParsed;
26+
try {
27+
resultParsed = JSON.parse(queryResult.result ?? '{}');
28+
} catch (error) {
29+
return 0;
30+
}
31+
2632
if (resultParsed?.balance?.decimal) {
2733
return Number(resultParsed.balance.decimal);
2834
} else if (resultParsed?.balance) {
@@ -48,7 +54,12 @@ export const formatBalance_NODE = (queryResult: PactQueryResponse) => {
4854
* @returns A normalized guard object with standardized properties including the raw representation
4955
*/
5056
export const formatGuard_NODE = (queryResult: PactQueryResponse) => {
51-
const resultParsed = JSON.parse(queryResult.result ?? '{}');
57+
let resultParsed;
58+
try {
59+
resultParsed = JSON.parse(queryResult.result ?? '{}');
60+
} catch (error) {
61+
return { raw: 'null', keys: [], predicate: '' };
62+
}
5263

5364
// Handle function-based guards (fun format)
5465
if (resultParsed.guard?.fun) {
@@ -71,5 +82,9 @@ export const formatGuard_NODE = (queryResult: PactQueryResponse) => {
7182
}
7283

7384
// Default case for other guard formats
74-
return { raw: JSON.stringify(resultParsed.guard), keys: [], predicate: '' };
85+
return {
86+
raw: resultParsed.guard ? JSON.stringify(resultParsed.guard) : 'null',
87+
keys: [],
88+
predicate: '',
89+
};
7590
};
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { formatBalance_NODE, formatGuard_NODE } from '../../src/utils/chainweb-node';
2+
import { PactQueryResponse } from '../../src/kadena-server/config/graphql-types';
3+
4+
describe('chainweb-node utility functions', () => {
5+
describe('formatBalance_NODE', () => {
6+
it('should handle balance with decimal format', () => {
7+
const queryResult: PactQueryResponse = {
8+
chainId: '0',
9+
code: '0',
10+
status: 'success',
11+
result: JSON.stringify({
12+
balance: {
13+
decimal: '123.45',
14+
},
15+
}),
16+
};
17+
expect(formatBalance_NODE(queryResult)).toBe(123.45);
18+
});
19+
20+
it('should handle direct balance value', () => {
21+
const queryResult: PactQueryResponse = {
22+
chainId: '0',
23+
code: '0',
24+
status: 'success',
25+
result: JSON.stringify({
26+
balance: 100,
27+
}),
28+
};
29+
expect(formatBalance_NODE(queryResult)).toBe(100);
30+
});
31+
32+
it('should return 0 for missing balance', () => {
33+
const queryResult: PactQueryResponse = {
34+
chainId: '0',
35+
code: '0',
36+
status: 'success',
37+
result: JSON.stringify({}),
38+
};
39+
expect(formatBalance_NODE(queryResult)).toBe(0);
40+
});
41+
42+
it('should return 0 for invalid JSON', () => {
43+
const queryResult: PactQueryResponse = {
44+
chainId: '0',
45+
code: '0',
46+
status: 'success',
47+
result: 'invalid json',
48+
};
49+
expect(formatBalance_NODE(queryResult)).toBe(0);
50+
});
51+
});
52+
53+
describe('formatGuard_NODE', () => {
54+
it('should handle function-based guards', () => {
55+
const queryResult: PactQueryResponse = {
56+
chainId: '0',
57+
code: '0',
58+
status: 'success',
59+
result: JSON.stringify({
60+
guard: {
61+
fun: 'keys-all',
62+
args: [{ keys: ['key1', 'key2'] }],
63+
},
64+
}),
65+
};
66+
const expected = {
67+
args: ['{"keys":["key1","key2"]}'],
68+
fun: 'keys-all',
69+
raw: JSON.stringify({ fun: 'keys-all', args: [{ keys: ['key1', 'key2'] }] }),
70+
keys: [],
71+
predicate: '',
72+
};
73+
expect(formatGuard_NODE(queryResult)).toEqual(expected);
74+
});
75+
76+
it('should handle predicate-based guards', () => {
77+
const queryResult: PactQueryResponse = {
78+
chainId: '0',
79+
code: '0',
80+
status: 'success',
81+
result: JSON.stringify({
82+
guard: {
83+
pred: 'keys-any',
84+
keys: ['key1', 'key2'],
85+
},
86+
}),
87+
};
88+
const expected = {
89+
keys: ['key1', 'key2'],
90+
predicate: 'keys-any',
91+
raw: JSON.stringify({ pred: 'keys-any', keys: ['key1', 'key2'] }),
92+
};
93+
expect(formatGuard_NODE(queryResult)).toEqual(expected);
94+
});
95+
96+
it('should handle other guard formats', () => {
97+
const queryResult: PactQueryResponse = {
98+
chainId: '0',
99+
code: '0',
100+
status: 'success',
101+
result: JSON.stringify({
102+
guard: {
103+
custom: 'format',
104+
},
105+
}),
106+
};
107+
const expected = {
108+
raw: JSON.stringify({ custom: 'format' }),
109+
keys: [],
110+
predicate: '',
111+
};
112+
expect(formatGuard_NODE(queryResult)).toEqual(expected);
113+
});
114+
115+
it('should handle missing guard', () => {
116+
const queryResult: PactQueryResponse = {
117+
chainId: '0',
118+
code: '0',
119+
status: 'success',
120+
result: JSON.stringify({}),
121+
};
122+
const expected = {
123+
raw: 'null',
124+
keys: [],
125+
predicate: '',
126+
};
127+
expect(formatGuard_NODE(queryResult)).toEqual(expected);
128+
});
129+
130+
it('should handle invalid JSON', () => {
131+
const queryResult: PactQueryResponse = {
132+
chainId: '0',
133+
code: '0',
134+
status: 'success',
135+
result: 'invalid json',
136+
};
137+
const expected = {
138+
raw: 'null',
139+
keys: [],
140+
predicate: '',
141+
};
142+
expect(formatGuard_NODE(queryResult)).toEqual(expected);
143+
});
144+
});
145+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { getCirculationNumber } from '../../src/kadena-server/utils/coin-circulation';
2+
import fs from 'fs';
3+
4+
// Mock the file system module
5+
jest.mock('fs', () => ({
6+
readFile: jest.fn(),
7+
}));
8+
9+
const mockedFs = fs as jest.Mocked<typeof fs>;
10+
11+
describe('coin circulation utilities', () => {
12+
const mockMinerRewardsCsv = `
13+
100,1.0
14+
200,0.5
15+
300,0.25
16+
`;
17+
18+
const mockTokenPaymentsCsv = `
19+
chain1,2023-01-01T00:00:00Z,key1,1000,0
20+
chain2,2023-01-02T00:00:00Z,key2,2000,0
21+
chain3,2023-01-03T00:00:00Z,key3,3000,0
22+
`;
23+
24+
beforeEach(() => {
25+
jest.clearAllMocks();
26+
});
27+
28+
describe('getCirculationNumber', () => {
29+
it('should calculate total circulation correctly', async () => {
30+
// Mock the fs.readFile function to return different CSV content based on the file path
31+
mockedFs.readFile.mockImplementation((...args: any[]) => {
32+
const [filePath, , callback] = args;
33+
const pathStr = filePath.toString();
34+
if (pathStr.includes('miner_rewards.csv')) {
35+
callback(null, mockMinerRewardsCsv);
36+
} else if (pathStr.includes('token_payments.csv')) {
37+
callback(null, mockTokenPaymentsCsv);
38+
} else {
39+
callback(new Error('Unknown file path'), '');
40+
}
41+
});
42+
43+
// Test with a height that falls between the second and third reward periods
44+
const height = 5000; // Average chain height will be 250 (5000/20)
45+
// Use a timestamp that's in the middle of the second day (2023-01-02)
46+
// This should include the first two token payments (1000 + 2000 = 3000)
47+
const timestamp = new Date('2023-01-02T12:00:00Z').getTime() * 1000; // Convert to microseconds
48+
49+
const result = await getCirculationNumber(height, timestamp);
50+
51+
// Expected calculation:
52+
// Miner rewards:
53+
// - First period (0-100): 100 blocks * 1.0 = 100
54+
// - Second period (100-200): 100 blocks * 0.5 = 50
55+
// - Third period (200-250): 50 blocks * 0.25 = 12.5
56+
// Total miner rewards: 162.5
57+
// Token payments up to 2023-01-02T12:00:00Z: 1000 + 2000 = 3000
58+
// Total circulation: 3162.5
59+
expect(result).toBe(3162.5);
60+
});
61+
62+
it('should handle file reading errors gracefully', async () => {
63+
mockedFs.readFile.mockImplementation((...args: any[]) => {
64+
const [, , callback] = args;
65+
callback(new Error('File not found'), '');
66+
});
67+
68+
const height = 5000;
69+
const timestamp = new Date('2023-01-02T12:00:00Z').getTime() * 1000; // Convert to microseconds
70+
71+
await expect(getCirculationNumber(height, timestamp)).rejects.toThrow('File not found');
72+
});
73+
});
74+
});

indexer/tests/unit/date.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { convertStringToDate } from '../../src/kadena-server/utils/date';
2+
3+
describe('date utilities', () => {
4+
describe('convertStringToDate', () => {
5+
it('should convert a microsecond timestamp string to a Date object', () => {
6+
// Test with a timestamp in microseconds (2023-01-01T00:00:00.000Z)
7+
const timestamp = '1672531200000000'; // 2023-01-01T00:00:00.000Z in microseconds
8+
const result = convertStringToDate(timestamp);
9+
10+
// The result should be a Date object representing the same time
11+
expect(result).toBeInstanceOf(Date);
12+
expect(result.toISOString()).toBe('2023-01-01T00:00:00.000Z');
13+
});
14+
15+
it('should handle timestamps with different precisions', () => {
16+
// Test with a timestamp that's not exactly on a second boundary
17+
const timestamp = '1672531200123456'; // 2023-01-01T00:00:00.123Z in microseconds
18+
const result = convertStringToDate(timestamp);
19+
20+
expect(result.toISOString()).toBe('2023-01-01T00:00:00.123Z');
21+
});
22+
23+
it('should throw an error for non-string input', () => {
24+
// Test with a number instead of a string
25+
const timestamp = 1672531200000000;
26+
27+
expect(() => convertStringToDate(timestamp as any)).toThrow(
28+
'The input timestamp must be a string.',
29+
);
30+
});
31+
32+
it('should throw an error for timestamps too large to convert', () => {
33+
// Test with a timestamp that's way beyond MAX_SAFE_INTEGER when converted to milliseconds
34+
// Using 2^64 as a very large number that will definitely exceed the safe integer range
35+
const timestamp = (2n ** 64n).toString();
36+
37+
expect(() => convertStringToDate(timestamp)).toThrow(
38+
'The timestamp is too large to safely convert to a JavaScript number.',
39+
);
40+
});
41+
42+
it('should handle timestamps at the edge of safe integer range', () => {
43+
// Test with a timestamp that's exactly at MAX_SAFE_INTEGER when converted to milliseconds
44+
const timestamp = (BigInt(Number.MAX_SAFE_INTEGER) * 1000n).toString();
45+
46+
const result = convertStringToDate(timestamp);
47+
expect(result).toBeInstanceOf(Date);
48+
});
49+
});
50+
});

0 commit comments

Comments
 (0)