Skip to content

Commit

Permalink
Small implementation changes, add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Siegrift committed Oct 25, 2023
1 parent aaaa785 commit 6a5b6d5
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 109 deletions.
5 changes: 4 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
const { join } = require('node:path');

/*
/**
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
* @type {import('jest').Config}
*/
module.exports = {
bail: true,
collectCoverage: true,
coverageDirectory: 'coverage',
coverageProvider: 'v8',
Expand All @@ -13,4 +15,5 @@ module.exports = {
restoreMocks: true,
setupFiles: [join(__dirname, './jest.setup.js')],
testEnvironment: 'jest-environment-node',
verbose: true,
};
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
"eslint:fix": "pnpm run eslint:check --fix",
"prettier:check": "prettier --check \"./**/*.{js,ts,md,json}\"",
"prettier:fix": "prettier --write \"./**/*.{js,ts,md,json}\"",
"test": "jest --verbose --runInBand --bail --detectOpenHandles --silent",
"test:e2e": "jest --selectProjects e2e --runInBand",
"test": "jest",
"test:e2e": "jest --runInBand",
"tsc": "tsc --project .",
"docker:build": "docker build -t api3/airseekerv2:latest -f docker/Dockerfile .",
"docker:run": "docker run -it --rm api3/airseekerv2:latest",
Expand Down
2 changes: 1 addition & 1 deletion src/signed-data-store/signed-data-store.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BigNumber, ethers } from 'ethers';
import * as localDataStore from './signed-data-store';
import { verifySignedDataIntegrity } from './signed-data-store';
import { generateRandomBytes32, signData } from '../../test/utils/evm';
import { generateRandomBytes32, signData } from '../../test/utils';
import type { SignedData } from '../types';

describe('datastore', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/state/state.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Config } from '../config/schema';

interface State {
export interface State {
config: Config;
dataFetcherInterval?: NodeJS.Timeout;
}
Expand Down
18 changes: 18 additions & 0 deletions src/update-feeds/temporary-contract-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// NOTE: The function is currently returning static data, because the contract is not yet finalized, but we mark it as
// async in advance.
//
// eslint-disable-next-line @typescript-eslint/require-await
export const getStaticActiveDapis = async (_offset: number, _limit: number) => {
return {
totalCount: 1,
dapiNames: ['MOCK_FEED'],
dataFeedIds: ['0xebba8507d616ed80766292d200a3598fdba656d9938cecc392765d4a284a69a4'],
updateParameters: [{ deviationThresholdInPercentage: 0.5, deviationReference: 0.5, heartbeatInterval: 100 }],
// NOTE: We will need to decode this from the contract, because it will store the template IDs as encoded bytes.
dataFeedTemplateIds: [['0xcc35bd1800c06c12856a87311dd95bfcbb3add875844021d59a929d79f3c99bd']],
signedApiUrls: [['http://localhost:8080']],
airnodeAddresses: ['0xbF3137b0a7574563a23a8fC8badC6537F98197CC'],
};
};

export type ActiveDapisBatch = Awaited<ReturnType<typeof getStaticActiveDapis>>;
192 changes: 192 additions & 0 deletions src/update-feeds/update-feeds.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { runUpdateFeed, startUpdateFeedsLoops } from './update-feeds';
import * as contractMockModule from './temporary-contract-mock';
import * as stateModule from '../state';
import { logger } from '../logger';
import { allowPartial } from '../../test/utils';
import type { Chain } from '../config/schema';

describe(startUpdateFeedsLoops.name, () => {
it('starts staggered update loops for a chain', async () => {
jest.spyOn(stateModule, 'getState').mockReturnValue(
allowPartial<stateModule.State>({
config: {
chains: {
'123': {
dataFeedUpdateInterval: 1, // Have just 1 second update interval to make the test run quicker.
providers: {
'first-provider': { url: 'first-provider-url' },
'second-provider': { url: 'second-provider-url' },
},
},
},
},
})
);
const intervalCalls = [] as number[];
jest.spyOn(global, 'setInterval').mockImplementation((() => {
intervalCalls.push(Date.now());
}) as any);
jest.spyOn(logger, 'debug');

await startUpdateFeedsLoops();

// Expect the intervals to be called with the correct stagger time.
expect(setInterval).toHaveBeenCalledTimes(2);
expect(intervalCalls[1]! - intervalCalls[0]!).toBeGreaterThanOrEqual(500);

// Expect the logs to be called with the correct context.
expect(logger.debug).toHaveBeenCalledTimes(3);
expect(logger.debug).toHaveBeenCalledWith('Starting update loops for chain', {
chainId: '123',
staggerTime: 500,
providerNames: ['first-provider', 'second-provider'],
});
expect(logger.debug).toHaveBeenCalledWith('Starting update feed loop', {
chainId: '123',
providerName: 'first-provider',
});
expect(logger.debug).toHaveBeenCalledWith('Starting update feed loop', {
chainId: '123',
providerName: 'second-provider',
});
});

it('starts the update loops in parallel for each chain', async () => {
jest.spyOn(stateModule, 'getState').mockReturnValue(
allowPartial<stateModule.State>({
config: {
chains: {
'123': {
dataFeedUpdateInterval: 1,
providers: {
'first-provider': { url: 'first-provider-url' },
},
},
'456': {
dataFeedUpdateInterval: 1,
providers: {
'another-provider': { url: 'another-provider-url' },
},
},
},
},
})
);
const intervalCalls = [] as number[];
jest.spyOn(global, 'setInterval').mockImplementation((() => {
intervalCalls.push(Date.now());
}) as any);
jest.spyOn(logger, 'debug');

await startUpdateFeedsLoops();

// Expect the intervals to be called with the correct stagger time.
expect(setInterval).toHaveBeenCalledTimes(2);
expect(intervalCalls[1]! - intervalCalls[0]!).toBeLessThan(50); // Ensures that the loops are run in parallel.

// Expect the logs to be called with the correct context.
expect(logger.debug).toHaveBeenCalledTimes(4);
expect(logger.debug).toHaveBeenCalledWith('Starting update loops for chain', {
chainId: '123',
staggerTime: 1000,
providerNames: ['first-provider'],
});
expect(logger.debug).toHaveBeenCalledWith('Starting update loops for chain', {
chainId: '456',
staggerTime: 1000,
providerNames: ['another-provider'],
});
expect(logger.debug).toHaveBeenCalledWith('Starting update feed loop', {
chainId: '123',
providerName: 'first-provider',
});
expect(logger.debug).toHaveBeenCalledWith('Starting update feed loop', {
chainId: '456',
providerName: 'another-provider',
});
});
});

describe(runUpdateFeed.name, () => {
it('aborts when fetching first dAPIs batch fails', async () => {
jest.spyOn(contractMockModule, 'getStaticActiveDapis').mockRejectedValue(new Error('provider-error'));
jest.spyOn(logger, 'error');

await runUpdateFeed(
'provider-name',
allowPartial<Chain>({ dataFeedBatchSize: 2, dataFeedUpdateInterval: 10 }),
'123'
);

// Expect the logs to be called with the correct context.
expect(logger.error).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith('Failed to get first active dAPIs batch', new Error('provider-error'), {
chainId: '123',
providerName: 'provider-name',
});
});

it('fetches other batches in a staggered way and logs errors', async () => {
// Prepare the mocked contract so it returns three batches (of size 1) of dAPIs and the second batch fails to load.
const mockedFeed = await contractMockModule.getStaticActiveDapis(0, 0);
const firstBatch = { ...mockedFeed, totalCount: 3 };
const thirdBatch = { ...mockedFeed, totalCount: 3 };
const getStaticActiveDapisCalls = [] as number[];
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(contractMockModule, 'getStaticActiveDapis').mockImplementationOnce(async () => {
getStaticActiveDapisCalls.push(Date.now());
return firstBatch;
});
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(contractMockModule, 'getStaticActiveDapis').mockImplementationOnce(async () => {
getStaticActiveDapisCalls.push(Date.now());
throw new Error('provider-error');
});
// eslint-disable-next-line @typescript-eslint/require-await
jest.spyOn(contractMockModule, 'getStaticActiveDapis').mockImplementationOnce(async () => {
getStaticActiveDapisCalls.push(Date.now());
return thirdBatch;
});
jest.spyOn(logger, 'debug');
jest.spyOn(logger, 'error');

await runUpdateFeed(
'provider-name',
allowPartial<Chain>({ dataFeedBatchSize: 1, dataFeedUpdateInterval: 1.5 }),
'123'
);

// Expect the contract to fetch the batches to be called with the correct stagger time.
expect(getStaticActiveDapisCalls).toHaveLength(3);
expect(getStaticActiveDapisCalls[1]! - getStaticActiveDapisCalls[0]!).toBeGreaterThanOrEqual(500);
expect(getStaticActiveDapisCalls[2]! - getStaticActiveDapisCalls[1]!).toBeGreaterThanOrEqual(500);

// Expect the logs to be called with the correct context.
expect(logger.error).toHaveBeenCalledTimes(1);
expect(logger.error).toHaveBeenCalledWith('Failed to get active dAPIs batch', new Error('provider-error'), {
chainId: '123',
providerName: 'provider-name',
});
expect(logger.debug).toHaveBeenCalledTimes(4);
expect(logger.debug).toHaveBeenCalledWith('Fetching first batch of dAPIs batches', {
chainId: '123',
providerName: 'provider-name',
});
expect(logger.debug).toHaveBeenCalledWith('Fetching batches of active dAPIs', {
batchesCount: 3,
staggerTime: 500,
chainId: '123',
providerName: 'provider-name',
});
expect(logger.debug).toHaveBeenCalledWith('Fetching batch of active dAPIs', {
batchIndex: 1,
chainId: '123',
providerName: 'provider-name',
});
expect(logger.debug).toHaveBeenCalledWith('Fetching batch of active dAPIs', {
batchIndex: 2,
chainId: '123',
providerName: 'provider-name',
});
});
});
Loading

0 comments on commit 6a5b6d5

Please sign in to comment.