Skip to content

Commit 93afa1c

Browse files
committed
test: wip escrow completion tests
1 parent d77e7b0 commit 93afa1c

File tree

3 files changed

+381
-1
lines changed

3 files changed

+381
-1
lines changed
Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
jest.mock('@human-protocol/sdk');
2+
3+
import { faker } from '@faker-js/faker';
4+
import { createMock } from '@golevelup/ts-jest';
5+
import { EscrowClient, EscrowStatus, EscrowUtils } from '@human-protocol/sdk';
6+
import { Test } from '@nestjs/testing';
7+
8+
import { ServerConfigService } from '../../config/server-config.service';
9+
10+
import { ReputationService } from '../reputation/reputation.service';
11+
import { StorageService } from '../storage/storage.service';
12+
import { OutgoingWebhookService } from '../webhook/webhook-outgoing.service';
13+
import { Web3Service } from '../web3/web3.service';
14+
import { generateTestnetChainId } from '../web3/fixtures';
15+
16+
import { EscrowCompletionStatus } from './constants';
17+
import { generateEscrowCompletion } from './fixtures/escrow-completion';
18+
import { EscrowCompletionService } from './escrow-completion.service';
19+
import { EscrowCompletionRepository } from './escrow-completion.repository';
20+
import { EscrowPayoutsBatchRepository } from './escrow-payouts-batch.repository';
21+
import {
22+
AudinoResultsProcessor,
23+
CvatResultsProcessor,
24+
FortuneResultsProcessor,
25+
} from './results-processing';
26+
import {
27+
AudinoPayoutsCalculator,
28+
CvatPayoutsCalculator,
29+
FortunePayoutsCalculator,
30+
} from './payouts-calculation';
31+
import { generateFortuneManifest } from './fixtures';
32+
33+
const mockServerConfigService = {
34+
maxRetryCount: faker.number.int({ min: 2, max: 5 }),
35+
};
36+
37+
const mockEscrowCompletionRepository = createMock<EscrowCompletionRepository>();
38+
const mockEscrowPayoutsBatchRepository =
39+
createMock<EscrowPayoutsBatchRepository>();
40+
const mockWeb3Service = createMock<Web3Service>();
41+
const mockStorageService = createMock<StorageService>();
42+
const mockOutgoingWebhookService = createMock<OutgoingWebhookService>();
43+
const mockReputationService = createMock<ReputationService>();
44+
const mockFortuneResultsProcessor = createMock<FortuneResultsProcessor>();
45+
const mockCvatResultsProcessor = createMock<CvatResultsProcessor>();
46+
const mockFortunePayoutsCalculator = createMock<FortunePayoutsCalculator>();
47+
const mockCvatPayoutsCalculator = createMock<CvatPayoutsCalculator>();
48+
49+
const mockedEscrowClient = jest.mocked(EscrowClient);
50+
const mockedEscrowUtils = jest.mocked(EscrowUtils);
51+
52+
describe('EscrowCompletionService', () => {
53+
let service: EscrowCompletionService;
54+
55+
beforeAll(async () => {
56+
const moduleRef = await Test.createTestingModule({
57+
providers: [
58+
EscrowCompletionService,
59+
{
60+
provide: ServerConfigService,
61+
useValue: mockServerConfigService,
62+
},
63+
{
64+
provide: EscrowCompletionRepository,
65+
useValue: mockEscrowCompletionRepository,
66+
},
67+
{
68+
provide: EscrowPayoutsBatchRepository,
69+
useValue: mockEscrowPayoutsBatchRepository,
70+
},
71+
{
72+
provide: Web3Service,
73+
useValue: mockWeb3Service,
74+
},
75+
{
76+
provide: StorageService,
77+
useValue: mockStorageService,
78+
},
79+
{
80+
provide: OutgoingWebhookService,
81+
useValue: mockOutgoingWebhookService,
82+
},
83+
{
84+
provide: ReputationService,
85+
useValue: mockReputationService,
86+
},
87+
{
88+
provide: FortuneResultsProcessor,
89+
useValue: mockFortuneResultsProcessor,
90+
},
91+
{
92+
provide: CvatResultsProcessor,
93+
useValue: mockCvatResultsProcessor,
94+
},
95+
{
96+
provide: FortunePayoutsCalculator,
97+
useValue: mockFortunePayoutsCalculator,
98+
},
99+
{
100+
provide: CvatPayoutsCalculator,
101+
useValue: mockCvatPayoutsCalculator,
102+
},
103+
{
104+
provide: AudinoResultsProcessor,
105+
useValue: createMock(),
106+
},
107+
{
108+
provide: AudinoPayoutsCalculator,
109+
useValue: createMock(),
110+
},
111+
],
112+
}).compile();
113+
114+
service = moduleRef.get<EscrowCompletionService>(EscrowCompletionService);
115+
});
116+
117+
afterEach(() => {
118+
jest.resetAllMocks();
119+
});
120+
121+
describe('createEscrowCompletion', () => {
122+
it('creates escrow completion tracking record with proper defaults', async () => {
123+
const chainId = generateTestnetChainId();
124+
const escrowAddress = faker.finance.ethereumAddress();
125+
126+
const now = Date.now();
127+
jest.useFakeTimers({ now });
128+
129+
await service.createEscrowCompletion(chainId, escrowAddress);
130+
131+
jest.useRealTimers();
132+
133+
expect(mockEscrowCompletionRepository.createUnique).toHaveBeenCalledTimes(
134+
1,
135+
);
136+
expect(mockEscrowCompletionRepository.createUnique).toHaveBeenCalledWith(
137+
expect.objectContaining({
138+
chainId,
139+
escrowAddress,
140+
status: 'pending',
141+
retriesCount: 0,
142+
waitUntil: new Date(now),
143+
}),
144+
);
145+
});
146+
});
147+
148+
describe('processPendingRecords', () => {
149+
const mockGetEscrowStatus = jest.fn();
150+
let spyOnCreateEscrowPayoutsBatch: jest.SpyInstance;
151+
152+
beforeAll(() => {
153+
spyOnCreateEscrowPayoutsBatch = jest
154+
.spyOn(service as any, 'createEscrowPayoutsBatch')
155+
.mockImplementation();
156+
});
157+
158+
afterAll(() => {
159+
spyOnCreateEscrowPayoutsBatch.mockRestore();
160+
});
161+
162+
beforeEach(() => {
163+
mockedEscrowClient.build.mockResolvedValue({
164+
getStatus: mockGetEscrowStatus,
165+
} as unknown as EscrowClient);
166+
});
167+
168+
describe('handle failures', () => {
169+
const testError = new Error(faker.lorem.sentence());
170+
171+
beforeEach(() => {
172+
mockGetEscrowStatus.mockRejectedValue(testError);
173+
});
174+
175+
it('should process multiple items and handle failure for each', async () => {
176+
const pendingRecords = [
177+
generateEscrowCompletion(EscrowCompletionStatus.PENDING),
178+
generateEscrowCompletion(EscrowCompletionStatus.PENDING),
179+
];
180+
mockEscrowCompletionRepository.findByStatus.mockResolvedValueOnce(
181+
pendingRecords,
182+
);
183+
184+
await service.processPendingRecords();
185+
186+
expect(mockEscrowCompletionRepository.updateOne).toHaveBeenCalledTimes(
187+
2,
188+
);
189+
expect(mockEscrowCompletionRepository.updateOne).toHaveBeenCalledWith(
190+
expect.objectContaining({
191+
id: pendingRecords[0].id,
192+
}),
193+
);
194+
expect(mockEscrowCompletionRepository.updateOne).toHaveBeenCalledWith(
195+
expect.objectContaining({
196+
id: pendingRecords[0].id,
197+
}),
198+
);
199+
});
200+
201+
it('should handle failure for item that has retries left', async () => {
202+
const pendingRecord = generateEscrowCompletion(
203+
EscrowCompletionStatus.PENDING,
204+
);
205+
pendingRecord.retriesCount = mockServerConfigService.maxRetryCount - 1;
206+
mockEscrowCompletionRepository.findByStatus.mockResolvedValueOnce([
207+
{
208+
...pendingRecord,
209+
},
210+
]);
211+
212+
await service.processPendingRecords();
213+
214+
expect(mockEscrowCompletionRepository.updateOne).toHaveBeenCalledTimes(
215+
1,
216+
);
217+
expect(mockEscrowCompletionRepository.updateOne).toHaveBeenCalledWith({
218+
...pendingRecord,
219+
retriesCount: pendingRecord.retriesCount + 1,
220+
waitUntil: expect.any(Date),
221+
});
222+
});
223+
224+
it('should handle failure for item that has no retries left', async () => {
225+
const pendingRecord = generateEscrowCompletion(
226+
EscrowCompletionStatus.PENDING,
227+
);
228+
pendingRecord.retriesCount = mockServerConfigService.maxRetryCount;
229+
mockEscrowCompletionRepository.findByStatus.mockResolvedValueOnce([
230+
{
231+
...pendingRecord,
232+
},
233+
]);
234+
235+
await service.processPendingRecords();
236+
237+
expect(mockEscrowCompletionRepository.updateOne).toHaveBeenCalledTimes(
238+
1,
239+
);
240+
expect(mockEscrowCompletionRepository.updateOne).toHaveBeenCalledWith({
241+
...pendingRecord,
242+
failureDetail: `Error message: ${testError.message}`,
243+
status: 'failed',
244+
});
245+
});
246+
});
247+
248+
it('should not process if escrow has pending status', async () => {
249+
const pendingRecord = generateEscrowCompletion(
250+
EscrowCompletionStatus.PENDING,
251+
);
252+
mockEscrowCompletionRepository.findByStatus.mockResolvedValueOnce([
253+
{
254+
...pendingRecord,
255+
},
256+
]);
257+
mockGetEscrowStatus.mockResolvedValue(faker.string.sample());
258+
259+
await service.processPendingRecords();
260+
261+
expect(mockEscrowCompletionRepository.updateOne).toHaveBeenCalledTimes(1);
262+
expect(mockEscrowCompletionRepository.updateOne).toHaveBeenCalledWith({
263+
...pendingRecord,
264+
status: EscrowCompletionStatus.AWAITING_PAYOUTS,
265+
});
266+
});
267+
268+
it('should correctly process escrow that has pending status', async () => {
269+
const pendingRecord = generateEscrowCompletion(
270+
EscrowCompletionStatus.PENDING,
271+
);
272+
mockEscrowCompletionRepository.findByStatus.mockResolvedValueOnce([
273+
{
274+
...pendingRecord,
275+
},
276+
]);
277+
mockGetEscrowStatus.mockResolvedValue(EscrowStatus.Pending);
278+
279+
const manifestUrl = faker.internet.url();
280+
mockedEscrowUtils.getEscrow.mockImplementationOnce(
281+
async (chainId, escrowAddress) => {
282+
if (
283+
chainId === pendingRecord.chainId &&
284+
escrowAddress === pendingRecord.escrowAddress
285+
) {
286+
return { manifestUrl } as any;
287+
}
288+
throw new Error('Test escrow not found');
289+
},
290+
);
291+
292+
const fortuneManifest = generateFortuneManifest();
293+
mockStorageService.downloadJsonLikeData.mockImplementationOnce(
294+
async (url) => {
295+
if (url === manifestUrl) {
296+
return fortuneManifest;
297+
}
298+
return null;
299+
},
300+
);
301+
const finalResultsUrl = faker.internet.url();
302+
const finalResultsHash = faker.string.hexadecimal({ length: 42 });
303+
mockFortuneResultsProcessor.storeResults.mockResolvedValueOnce({
304+
url: finalResultsUrl,
305+
hash: finalResultsHash,
306+
});
307+
const payoutsBatch = [
308+
{
309+
address: faker.finance.ethereumAddress(),
310+
amount: faker.number.bigInt({ min: 1, max: 42 }),
311+
},
312+
];
313+
mockFortunePayoutsCalculator.calculate.mockResolvedValueOnce(
314+
payoutsBatch,
315+
);
316+
317+
await service.processPendingRecords();
318+
319+
expect(mockFortuneResultsProcessor.storeResults).toHaveBeenCalledTimes(1);
320+
expect(mockFortuneResultsProcessor.storeResults).toHaveBeenCalledWith(
321+
pendingRecord.chainId,
322+
pendingRecord.escrowAddress,
323+
fortuneManifest,
324+
);
325+
expect(mockEscrowCompletionRepository.updateOne).toHaveBeenCalledWith(
326+
expect.objectContaining({
327+
id: pendingRecord.id,
328+
finalResultsUrl,
329+
finalResultsHash,
330+
}),
331+
);
332+
333+
expect(mockFortunePayoutsCalculator.calculate).toHaveBeenCalledTimes(1);
334+
expect(mockFortunePayoutsCalculator.calculate).toHaveBeenCalledWith({
335+
manifest: fortuneManifest,
336+
finalResultsUrl,
337+
chainId: pendingRecord.chainId,
338+
escrowAddress: pendingRecord.escrowAddress,
339+
});
340+
expect(spyOnCreateEscrowPayoutsBatch).toHaveBeenCalledTimes(1);
341+
expect(spyOnCreateEscrowPayoutsBatch).toHaveBeenCalledWith(
342+
pendingRecord.id,
343+
payoutsBatch,
344+
);
345+
346+
expect(mockEscrowCompletionRepository.updateOne).toHaveBeenCalledWith(
347+
expect.objectContaining({
348+
id: pendingRecord.id,
349+
status: EscrowCompletionStatus.AWAITING_PAYOUTS,
350+
}),
351+
);
352+
353+
expect(mockEscrowCompletionRepository.updateOne).toHaveBeenCalledTimes(2);
354+
});
355+
});
356+
});

packages/apps/reputation-oracle/server/src/modules/escrow-completion/escrow-completion.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ export class EscrowCompletionService {
198198

199199
await this.handleEscrowCompletionError(
200200
escrowCompletionEntity,
201-
`Error message: ${error.message})`,
201+
`Error message: ${error.message}`,
202202
);
203203
continue;
204204
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { faker } from '@faker-js/faker';
2+
3+
import { generateTestnetChainId } from '../../web3/fixtures';
4+
5+
import { EscrowCompletionStatus } from '../constants';
6+
import { EscrowCompletionEntity } from '../escrow-completion.entity';
7+
8+
export function generateEscrowCompletion(
9+
status: EscrowCompletionStatus,
10+
): EscrowCompletionEntity {
11+
return {
12+
id: faker.number.int(),
13+
chainId: generateTestnetChainId(),
14+
status,
15+
retriesCount: 0,
16+
waitUntil: new Date(),
17+
failureDetail: null,
18+
finalResultsUrl: null,
19+
finalResultsHash: null,
20+
escrowAddress: faker.finance.ethereumAddress(),
21+
createdAt: faker.date.recent(),
22+
updatedAt: new Date(),
23+
};
24+
}

0 commit comments

Comments
 (0)