Skip to content

Commit dc92041

Browse files
test: init unit tests for onAccountsRefresh
1 parent 688f95b commit dc92041

1 file changed

Lines changed: 348 additions & 0 deletions

File tree

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
2+
import type { Signature } from '@solana/kit';
3+
import { address } from '@solana/kit';
4+
5+
import * as snapContext from '../../../../snapContext';
6+
import { Network } from '../../../constants/solana';
7+
import type { AssetsService } from '../../../services/assets/AssetsService';
8+
import type { IStateManager } from '../../../services/state/IStateManager';
9+
import type { UnencryptedStateValue } from '../../../services/state/State';
10+
import type { TransactionsService } from '../../../services/transactions/TransactionsService';
11+
import {
12+
MOCK_SOLANA_KEYRING_ACCOUNT_0,
13+
MOCK_SOLANA_KEYRING_ACCOUNT_1,
14+
} from '../../../test/mocks/solana-keyring-accounts';
15+
import type { ILogger } from '../../../utils/logger';
16+
import logger from '../../../utils/logger';
17+
import type { SolanaKeyring } from '../../onKeyringRequest/Keyring';
18+
import { onAccountsRefresh } from './onAccountsRefresh';
19+
20+
// Mock all dependencies
21+
jest.mock('../../../../snapContext', () => ({
22+
keyring: {
23+
listAccounts: jest.fn(),
24+
},
25+
state: {
26+
getKey: jest.fn(),
27+
},
28+
assetsService: {
29+
refreshAssets: jest.fn(),
30+
},
31+
transactionsService: {
32+
fetchLatestSignatures: jest.fn(),
33+
refreshTransactions: jest.fn(),
34+
},
35+
}));
36+
37+
describe('onAccountsRefresh', () => {
38+
const mockKeyring = snapContext.keyring as jest.Mocked<SolanaKeyring>;
39+
const mockState = snapContext.state as unknown as jest.Mocked<
40+
IStateManager<UnencryptedStateValue>
41+
>;
42+
const mockAssetsService =
43+
snapContext.assetsService as jest.Mocked<AssetsService>;
44+
const mockTransactionsService =
45+
snapContext.transactionsService as jest.Mocked<TransactionsService>;
46+
const mockLogger = logger as jest.Mocked<ILogger>;
47+
48+
beforeEach(() => {
49+
jest.clearAllMocks();
50+
});
51+
52+
describe('when accounts have changes', () => {
53+
it('refreshes assets and transactions for accounts with new signatures', async () => {
54+
// Setup two accounts. One had changes since last refresh, the other didn't.
55+
const accounts = [
56+
MOCK_SOLANA_KEYRING_ACCOUNT_0,
57+
MOCK_SOLANA_KEYRING_ACCOUNT_1,
58+
];
59+
mockKeyring.listAccounts.mockResolvedValue(accounts);
60+
61+
const existingSignatures: Signature[] = ['signature1'] as Signature[];
62+
const newSignature = 'newSignature123' as Signature;
63+
64+
// Mock state.getKey to return existing signatures for both accounts
65+
mockState.getKey.mockImplementation(async (key: string) => {
66+
if (key.includes(accounts[0]!.address)) {
67+
return Promise.resolve(existingSignatures);
68+
}
69+
if (key.includes(accounts[1]!.address)) {
70+
return Promise.resolve(['oldSignature'] as Signature[]);
71+
}
72+
return Promise.resolve([]);
73+
});
74+
75+
// Mock fetchLatestSignatures - account 0 has new signature, account 1 doesn't
76+
mockTransactionsService.fetchLatestSignatures.mockImplementation(
77+
async (scope, addr) => {
78+
if (addr === address(accounts[0]!.address)) {
79+
return Promise.resolve([newSignature]); // New signature not in existing list
80+
}
81+
return Promise.resolve(['oldSignature'] as Signature[]); // Existing signature
82+
},
83+
);
84+
85+
mockAssetsService.refreshAssets.mockResolvedValue();
86+
mockTransactionsService.refreshTransactions.mockResolvedValue();
87+
88+
const request = {
89+
id: '1',
90+
jsonrpc: '2.0' as const,
91+
method: 'onCronjob',
92+
params: {},
93+
};
94+
95+
await onAccountsRefresh({ request });
96+
97+
expect(mockKeyring.listAccounts).toHaveBeenCalledTimes(1);
98+
expect(mockState.getKey).toHaveBeenCalledTimes(2);
99+
expect(mockState.getKey).toHaveBeenCalledWith(
100+
`signatures.${accounts[0]!.address}`,
101+
);
102+
expect(mockState.getKey).toHaveBeenCalledWith(
103+
`signatures.${accounts[1]!.address}`,
104+
);
105+
106+
expect(
107+
mockTransactionsService.fetchLatestSignatures,
108+
).toHaveBeenCalledTimes(2);
109+
expect(
110+
mockTransactionsService.fetchLatestSignatures,
111+
).toHaveBeenCalledWith(Network.Mainnet, address(accounts[0]!.address), 1);
112+
expect(
113+
mockTransactionsService.fetchLatestSignatures,
114+
).toHaveBeenCalledWith(Network.Mainnet, address(accounts[1]!.address), 1);
115+
116+
// Only account 0 should be refreshed (has changes)
117+
expect(mockAssetsService.refreshAssets).toHaveBeenCalledTimes(1);
118+
expect(mockAssetsService.refreshAssets).toHaveBeenCalledWith([
119+
accounts[0],
120+
]);
121+
122+
expect(mockTransactionsService.refreshTransactions).toHaveBeenCalledTimes(
123+
1,
124+
);
125+
expect(mockTransactionsService.refreshTransactions).toHaveBeenCalledWith([
126+
accounts[0],
127+
]);
128+
129+
expect(mockLogger.info).toHaveBeenCalledWith(
130+
'[onAccountsRefresh] Cronjob triggered',
131+
);
132+
expect(mockLogger.info).toHaveBeenCalledWith(
133+
'[onAccountsRefresh] Found 1 accounts with changes',
134+
);
135+
expect(mockLogger.info).toHaveBeenCalledWith(
136+
'[onAccountsRefresh] Successfully refreshed 1 accounts',
137+
);
138+
});
139+
140+
it('handles accounts with no existing signatures', async () => {
141+
const accounts = [MOCK_SOLANA_KEYRING_ACCOUNT_0];
142+
mockKeyring.listAccounts.mockResolvedValue(accounts);
143+
144+
mockState.getKey.mockResolvedValue(null); // No existing signatures
145+
const newSignature = 'firstSignature' as Signature;
146+
147+
mockTransactionsService.fetchLatestSignatures.mockResolvedValue([
148+
newSignature,
149+
]);
150+
151+
mockAssetsService.refreshAssets.mockResolvedValue();
152+
mockTransactionsService.refreshTransactions.mockResolvedValue();
153+
154+
await onAccountsRefresh({
155+
request: {
156+
id: '1',
157+
jsonrpc: '2.0',
158+
method: 'onCronjob',
159+
params: {},
160+
},
161+
});
162+
163+
expect(mockAssetsService.refreshAssets).toHaveBeenCalledWith([
164+
accounts[0],
165+
]);
166+
expect(mockTransactionsService.refreshTransactions).toHaveBeenCalledWith([
167+
accounts[0],
168+
]);
169+
});
170+
});
171+
172+
// describe('when no accounts have changes', () => {
173+
// it('should skip refresh when all accounts have no new signatures', async () => {
174+
// // Arrange
175+
// const accounts = [
176+
// MOCK_SOLANA_KEYRING_ACCOUNT_0,
177+
// MOCK_SOLANA_KEYRING_ACCOUNT_1,
178+
// ];
179+
// const existingSignatures: Signature[] = [
180+
// 'signature1',
181+
// 'signature2',
182+
// ] as Signature[];
183+
184+
// mockKeyring.listAccounts.mockResolvedValue(accounts);
185+
// mockState.getKey.mockResolvedValue(existingSignatures);
186+
187+
// // Return existing signatures (no changes)
188+
// mockTransactionsService.fetchLatestSignatures.mockResolvedValue([
189+
// 'signature1',
190+
// ] as Signature[]);
191+
192+
// const request = {
193+
// id: '1',
194+
// jsonrpc: '2.0' as const,
195+
// method: 'onCronjob',
196+
// params: {},
197+
// };
198+
199+
// await onAccountsRefresh({ request });
200+
201+
// // Assert
202+
// expect(mockAssetsService.refreshAssets).not.toHaveBeenCalled();
203+
// expect(
204+
// mockTransactionsService.refreshTransactions,
205+
// ).not.toHaveBeenCalled();
206+
// expect(mockLogger.info).toHaveBeenCalledWith(
207+
// '[onAccountsRefresh] No accounts with changes, skipping refresh',
208+
// );
209+
// });
210+
211+
// it('should skip refresh when no latest signatures are found', async () => {
212+
// // Arrange
213+
// const accounts = [MOCK_SOLANA_KEYRING_ACCOUNT_0];
214+
215+
// mockKeyring.listAccounts.mockResolvedValue(accounts);
216+
// mockState.getKey.mockResolvedValue([]);
217+
// mockTransactionsService.fetchLatestSignatures.mockResolvedValue([]); // No signatures found
218+
219+
// const request = {
220+
// id: '1',
221+
// jsonrpc: '2.0' as const,
222+
// method: 'onCronjob',
223+
// params: {},
224+
// };
225+
226+
// await onAccountsRefresh({ request });
227+
228+
// // Assert
229+
// expect(mockAssetsService.refreshAssets).not.toHaveBeenCalled();
230+
// expect(
231+
// mockTransactionsService.refreshTransactions,
232+
// ).not.toHaveBeenCalled();
233+
// expect(mockLogger.info).toHaveBeenCalledWith(
234+
// '[onAccountsRefresh] No accounts with changes, skipping refresh',
235+
// );
236+
// });
237+
// });
238+
239+
// describe('when no accounts exist', () => {
240+
// it('should handle empty accounts list gracefully', async () => {
241+
// // Arrange
242+
// mockKeyring.listAccounts.mockResolvedValue([]);
243+
244+
// // Act
245+
// await onAccountsRefresh();
246+
247+
// // Assert
248+
// expect(mockState.getKey).not.toHaveBeenCalled();
249+
// expect(
250+
// mockTransactionsService.fetchLatestSignatures,
251+
// ).not.toHaveBeenCalled();
252+
// expect(mockAssetsService.refreshAssets).not.toHaveBeenCalled();
253+
// expect(
254+
// mockTransactionsService.refreshTransactions,
255+
// ).not.toHaveBeenCalled();
256+
// expect(mockLogger.info).toHaveBeenCalledWith(
257+
// '[onAccountsRefresh] No accounts with changes, skipping refresh',
258+
// );
259+
// });
260+
// });
261+
262+
// describe('error handling', () => {
263+
// it('should handle errors during assets refresh gracefully', async () => {
264+
// // Arrange
265+
// const accounts = [MOCK_SOLANA_KEYRING_ACCOUNT_0];
266+
// const assetsError = new Error('Assets service failed');
267+
268+
// mockKeyring.listAccounts.mockResolvedValue(accounts);
269+
// mockState.getKey.mockResolvedValue([]);
270+
// mockTransactionsService.fetchLatestSignatures.mockResolvedValue([
271+
// 'newSig',
272+
// ] as Signature[]);
273+
// mockAssetsService.refreshAssets.mockRejectedValue(assetsError);
274+
// mockTransactionsService.refreshTransactions.mockResolvedValue();
275+
276+
// // Act
277+
// await onAccountsRefresh();
278+
279+
// // Assert
280+
// expect(mockLogger.warn).toHaveBeenCalledWith(
281+
// '[onAccountsRefresh] Caught error while refreshing assets',
282+
// assetsError,
283+
// );
284+
// expect(mockTransactionsService.refreshTransactions).toHaveBeenCalled(); // Should still continue
285+
// });
286+
287+
// it('should handle errors during transactions refresh gracefully', async () => {
288+
// // Arrange
289+
// const accounts = [MOCK_SOLANA_KEYRING_ACCOUNT_0];
290+
// const transactionsError = new Error('Transactions service failed');
291+
292+
// mockKeyring.listAccounts.mockResolvedValue(accounts);
293+
// mockState.getKey.mockResolvedValue([]);
294+
// mockTransactionsService.fetchLatestSignatures.mockResolvedValue([
295+
// 'newSig',
296+
// ] as Signature[]);
297+
// mockAssetsService.refreshAssets.mockResolvedValue();
298+
// mockTransactionsService.refreshTransactions.mockRejectedValue(
299+
// transactionsError,
300+
// );
301+
302+
// // Act
303+
// await onAccountsRefresh();
304+
305+
// // Assert
306+
// expect(mockLogger.warn).toHaveBeenCalledWith(
307+
// '[onAccountsRefresh] Caught error while refreshing transactions',
308+
// transactionsError,
309+
// );
310+
// });
311+
312+
// it('should handle critical errors and log them', async () => {
313+
// // Arrange
314+
// const criticalError = new Error('Critical failure');
315+
// mockKeyring.listAccounts.mockRejectedValue(criticalError);
316+
317+
// // Act
318+
// await onAccountsRefresh();
319+
320+
// // Assert
321+
// expect(mockLogger.error).toHaveBeenCalledWith(
322+
// '[onAccountsRefresh] Error',
323+
// criticalError,
324+
// );
325+
// });
326+
327+
// it('should handle errors during signature fetching', async () => {
328+
// // Arrange
329+
// const accounts = [MOCK_SOLANA_KEYRING_ACCOUNT_0];
330+
// const signatureError = new Error('Signature fetch failed');
331+
332+
// mockKeyring.listAccounts.mockResolvedValue(accounts);
333+
// mockState.getKey.mockResolvedValue([]);
334+
// mockTransactionsService.fetchLatestSignatures.mockRejectedValue(
335+
// signatureError,
336+
// );
337+
338+
// // Act
339+
// await onAccountsRefresh();
340+
341+
// // Assert
342+
// expect(mockLogger.error).toHaveBeenCalledWith(
343+
// '[onAccountsRefresh] Error',
344+
// signatureError,
345+
// );
346+
// });
347+
// });
348+
});

0 commit comments

Comments
 (0)