Skip to content

Commit d77e7b0

Browse files
committed
test: payouts calculators
1 parent 6d924bc commit d77e7b0

File tree

8 files changed

+274
-24
lines changed

8 files changed

+274
-24
lines changed

packages/apps/reputation-oracle/server/jest.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ module.exports = {
1616
'^uuid$': require.resolve('uuid'),
1717
'^typeorm$': require.resolve('typeorm'),
1818
},
19+
clearMocks: true,
1920
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { faker } from '@faker-js/faker';
2+
3+
import { CvatJobType } from '../../../common/enums';
4+
import { CvatManifest } from '../../../common/types';
5+
6+
export function generateCvatManifest(): CvatManifest {
7+
return {
8+
annotation: {
9+
type: faker.helpers.arrayElement(Object.values(CvatJobType)),
10+
},
11+
validation: {
12+
min_quality: faker.number.float({ min: 0.1, max: 0.9 }),
13+
},
14+
job_bounty: faker.finance.amount({ max: 42 }),
15+
};
16+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { faker } from '@faker-js/faker';
2+
3+
import { FortuneJobType } from '../../../common/enums';
4+
import { FortuneFinalResult, FortuneManifest } from '../../../common/types';
5+
6+
export function generateFortuneManifest(): FortuneManifest {
7+
return {
8+
requestType: FortuneJobType.FORTUNE,
9+
fundAmount: Number(faker.finance.amount()),
10+
submissionsRequired: faker.number.int({ min: 2, max: 5 }),
11+
};
12+
}
13+
14+
export function generateFortuneSolution(error?: string): FortuneFinalResult {
15+
return {
16+
workerAddress: faker.finance.ethereumAddress(),
17+
solution: faker.string.sample(),
18+
error: error as FortuneFinalResult['error'],
19+
};
20+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './fortune';
2+
export * from './cvat';
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
jest.mock('@human-protocol/sdk');
2+
3+
import { createMock } from '@golevelup/ts-jest';
4+
import { faker } from '@faker-js/faker';
5+
import { EscrowClient } from '@human-protocol/sdk';
6+
import { Test } from '@nestjs/testing';
7+
import { ethers } from 'ethers';
8+
import _ from 'lodash';
9+
10+
import { CvatAnnotationMeta } from '../../../common/types';
11+
12+
import { StorageService } from '../../storage/storage.service';
13+
import { generateTestnetChainId } from '../../web3/fixtures';
14+
import { Web3Service } from '../../web3/web3.service';
15+
16+
import { CvatPayoutsCalculator } from './cvat-payouts-calculator';
17+
import { generateCvatManifest } from '../fixtures';
18+
19+
const mockedStorageService = createMock<StorageService>();
20+
const mockedWeb3Service = createMock<Web3Service>();
21+
22+
const mockedEscrowClient = jest.mocked(EscrowClient);
23+
24+
describe('CvatPayoutsCalculator', () => {
25+
let calculator: CvatPayoutsCalculator;
26+
27+
beforeAll(async () => {
28+
const moduleRef = await Test.createTestingModule({
29+
providers: [
30+
CvatPayoutsCalculator,
31+
{
32+
provide: StorageService,
33+
useValue: mockedStorageService,
34+
},
35+
{
36+
provide: Web3Service,
37+
useValue: mockedWeb3Service,
38+
},
39+
],
40+
}).compile();
41+
42+
calculator = moduleRef.get<CvatPayoutsCalculator>(CvatPayoutsCalculator);
43+
});
44+
45+
describe('calculate', () => {
46+
const chainId = generateTestnetChainId();
47+
const escrowAddress = faker.finance.ethereumAddress();
48+
49+
const mockedGetIntermediateResultsUrl = jest
50+
.fn()
51+
.mockImplementation(async () => faker.internet.url());
52+
53+
beforeAll(() => {
54+
mockedEscrowClient.build.mockResolvedValue({
55+
getIntermediateResultsUrl: mockedGetIntermediateResultsUrl,
56+
} as unknown as EscrowClient);
57+
});
58+
59+
it('throws when invalid annotation meta downloaded from valid url', async () => {
60+
const intermediateResultsUrl = faker.internet.url();
61+
mockedGetIntermediateResultsUrl.mockResolvedValueOnce(
62+
intermediateResultsUrl,
63+
);
64+
65+
mockedStorageService.downloadJsonLikeData.mockResolvedValueOnce({
66+
jobs: [],
67+
results: [],
68+
} as CvatAnnotationMeta);
69+
70+
await expect(
71+
calculator.calculate({
72+
finalResultsUrl: faker.internet.url(),
73+
chainId,
74+
escrowAddress,
75+
manifest: generateCvatManifest(),
76+
}),
77+
).rejects.toThrow('Invalid annotation meta');
78+
79+
expect(mockedStorageService.downloadJsonLikeData).toHaveBeenCalledWith(
80+
`${intermediateResultsUrl}/validation_meta.json`,
81+
);
82+
});
83+
84+
it('should properly calculate workers bounties', async () => {
85+
const annotators = [
86+
faker.finance.ethereumAddress(),
87+
faker.finance.ethereumAddress(),
88+
];
89+
90+
const jobsPerAnnotator = faker.number.int({ min: 1, max: 3 });
91+
92+
const annotationsMeta: CvatAnnotationMeta = {
93+
jobs: Array.from(
94+
{ length: jobsPerAnnotator * annotators.length },
95+
(_v, index: number) => ({
96+
job_id: index,
97+
final_result_id: faker.number.int(),
98+
}),
99+
),
100+
results: [],
101+
};
102+
for (const job of annotationsMeta.jobs) {
103+
const annotatorIndex = job.job_id % annotators.length;
104+
105+
annotationsMeta.results.push({
106+
id: job.final_result_id,
107+
job_id: job.job_id,
108+
annotator_wallet_address: annotators[annotatorIndex],
109+
annotation_quality: faker.number.float(),
110+
});
111+
}
112+
113+
// imitate weird case: job w/o result
114+
annotationsMeta.jobs.push({
115+
job_id: faker.number.int(),
116+
final_result_id: faker.number.int(),
117+
});
118+
// imitate weird case: result w/o job
119+
annotationsMeta.results.push({
120+
id: faker.number.int(),
121+
job_id: faker.number.int(),
122+
annotator_wallet_address: faker.helpers.arrayElement(annotators),
123+
annotation_quality: faker.number.float(),
124+
});
125+
126+
mockedStorageService.downloadJsonLikeData.mockResolvedValueOnce(
127+
annotationsMeta,
128+
);
129+
130+
const manifest = generateCvatManifest();
131+
132+
const payouts = await calculator.calculate({
133+
chainId,
134+
escrowAddress,
135+
manifest,
136+
finalResultsUrl: faker.internet.url(),
137+
});
138+
139+
const expectedAmountPerAnnotator =
140+
BigInt(jobsPerAnnotator) * ethers.parseUnits(manifest.job_bounty, 18);
141+
142+
const expectedPayouts = annotators.map((address) => ({
143+
address,
144+
amount: expectedAmountPerAnnotator,
145+
}));
146+
147+
expect(_.sortBy(payouts, 'address')).toEqual(
148+
_.sortBy(expectedPayouts, 'address'),
149+
);
150+
});
151+
});
152+
});

packages/apps/reputation-oracle/server/src/modules/escrow-completion/payouts-calculation/cvat-payouts-calculator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export class CvatPayoutsCalculator implements EscrowPayoutsCalculator {
4343
`${intermediateResultsUrl}/${CVAT_VALIDATION_META_FILENAME}`,
4444
);
4545

46-
if (annotations.jobs.length === 0 || annotations.results.length === 0) {
46+
if (!annotations.jobs.length || !annotations.results.length) {
4747
throw new Error('Invalid annotation meta');
4848
}
4949

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { createMock } from '@golevelup/ts-jest';
2+
import { faker } from '@faker-js/faker';
3+
import { Test } from '@nestjs/testing';
4+
import { ethers } from 'ethers';
5+
import _ from 'lodash';
6+
7+
import { StorageService } from '../../storage/storage.service';
8+
9+
import { generateFortuneManifest, generateFortuneSolution } from '../fixtures';
10+
import { FortunePayoutsCalculator } from './fortune-payouts-calculator';
11+
12+
const mockedStorageService = createMock<StorageService>();
13+
14+
describe('FortunePayoutsCalculator', () => {
15+
let calculator: FortunePayoutsCalculator;
16+
17+
beforeAll(async () => {
18+
const moduleRef = await Test.createTestingModule({
19+
providers: [
20+
FortunePayoutsCalculator,
21+
{
22+
provide: StorageService,
23+
useValue: mockedStorageService,
24+
},
25+
],
26+
}).compile();
27+
28+
calculator = moduleRef.get<FortunePayoutsCalculator>(
29+
FortunePayoutsCalculator,
30+
);
31+
});
32+
33+
describe('calculate', () => {
34+
it('should properly calculate payouts', async () => {
35+
const validSolutions = [
36+
generateFortuneSolution(),
37+
generateFortuneSolution(),
38+
];
39+
const results = faker.helpers.shuffle([
40+
...validSolutions,
41+
generateFortuneSolution('curse_word'),
42+
generateFortuneSolution('duplicated'),
43+
generateFortuneSolution(faker.string.sample()),
44+
]);
45+
const resultsUrl = faker.internet.url();
46+
mockedStorageService.downloadJsonLikeData.mockImplementationOnce(
47+
async (url) => {
48+
if (url === resultsUrl) {
49+
return results;
50+
}
51+
throw new Error('Results not found');
52+
},
53+
);
54+
const manifest = generateFortuneManifest();
55+
56+
const payouts = await calculator.calculate({
57+
chainId: faker.number.int(),
58+
escrowAddress: faker.finance.ethereumAddress(),
59+
finalResultsUrl: resultsUrl,
60+
manifest,
61+
});
62+
63+
const expectedPayouts = validSolutions.map((s) => ({
64+
address: s.workerAddress,
65+
amount:
66+
BigInt(ethers.parseUnits(manifest.fundAmount.toString(), 'ether')) /
67+
BigInt(validSolutions.length),
68+
}));
69+
70+
expect(_.sortBy(payouts, 'address')).toEqual(
71+
_.sortBy(expectedPayouts, 'address'),
72+
);
73+
});
74+
});
75+
});

packages/apps/reputation-oracle/server/src/modules/escrow-completion/results-processing/fortune-results-processor.spec.ts

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,20 @@ import { createMock } from '@golevelup/ts-jest';
22
import { faker } from '@faker-js/faker';
33
import { Test } from '@nestjs/testing';
44

5-
import { FortuneJobType } from '../../../common/enums';
6-
import { FortuneFinalResult, FortuneManifest } from '../../../common/types';
5+
import { FortuneFinalResult } from '../../../common/types';
76

87
import { PgpEncryptionService } from '../../encryption/pgp-encryption.service';
98
import { StorageService } from '../../storage/storage.service';
109
import { Web3Service } from '../../web3/web3.service';
1110

11+
import { generateFortuneManifest, generateFortuneSolution } from '../fixtures';
1212
import { BaseEscrowResultsProcessor } from './escrow-results-processor';
1313
import { FortuneResultsProcessor } from './fortune-results-processor';
1414

1515
const mockedStorageService = createMock<StorageService>();
1616
const mockedPgpEncryptionService = createMock<PgpEncryptionService>();
1717
const mockedWeb3Service = createMock<Web3Service>();
1818

19-
function generateFortuneSolution(
20-
error?: FortuneFinalResult['error'],
21-
): FortuneFinalResult {
22-
return {
23-
workerAddress: faker.finance.ethereumAddress(),
24-
solution: faker.string.sample(),
25-
error,
26-
};
27-
}
28-
2919
describe('FortuneResultsProcessor', () => {
3020
let processor: FortuneResultsProcessor;
3121

@@ -66,11 +56,7 @@ describe('FortuneResultsProcessor', () => {
6656
});
6757

6858
describe('assertResultsComplete', () => {
69-
const testManifest: FortuneManifest = {
70-
requestType: FortuneJobType.FORTUNE,
71-
fundAmount: Number(faker.finance.amount()),
72-
submissionsRequired: faker.number.int({ min: 2, max: 5 }),
73-
};
59+
const testManifest = generateFortuneManifest();
7460

7561
it('throws if results is not json', async () => {
7662
await expect(
@@ -105,11 +91,7 @@ describe('FortuneResultsProcessor', () => {
10591
() => generateFortuneSolution(),
10692
);
10793
solutions.pop();
108-
solutions.push(
109-
generateFortuneSolution(
110-
faker.helpers.arrayElement(['curse_word', 'duplicated']),
111-
),
112-
);
94+
solutions.push(generateFortuneSolution(faker.string.sample()));
11395

11496
await expect(
11597
processor['assertResultsComplete'](
@@ -123,7 +105,9 @@ describe('FortuneResultsProcessor', () => {
123105
const solutions: FortuneFinalResult[] = Array.from(
124106
{ length: testManifest.submissionsRequired * 2 },
125107
(i: number) =>
126-
generateFortuneSolution(i % 2 === 0 ? 'duplicated' : undefined),
108+
generateFortuneSolution(
109+
i % 2 === 0 ? faker.string.sample() : undefined,
110+
),
127111
);
128112

129113
await expect(

0 commit comments

Comments
 (0)