Skip to content

Commit

Permalink
Implement e2e test flow and CI test
Browse files Browse the repository at this point in the history
  • Loading branch information
Siegrift committed Oct 2, 2023
1 parent 0a7c216 commit 81f5c3d
Show file tree
Hide file tree
Showing 17 changed files with 384 additions and 6 deletions.
32 changes: 31 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,40 @@ jobs:
run: pnpm run prettier:check && pnpm run eslint:check
- name: Test
run: pnpm run test
e2e:
runs-on: ubuntu-latest
name: E2e tests
steps:
- name: Clone repo
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8.8.0
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18.x'
cache: 'pnpm'
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
- name: Build Docker Images
run: pnpm run docker:build
- name: Start Services
# Start the e2e services in the background and wait a small amount of time for them to start.
run: |
pnpm run start:data-provider-api &
pnpm run start:signed-api &
pnpm run start:pusher &
sleep 5
- name: Run E2E Tests
run: pnpm run test:e2e

required-checks-passed:
name: All required checks passed
runs-on: ubuntu-latest
needs: [lint-build-test]
needs: [documentation, lint-build-test, e2e]
steps:
- run: exit 0
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ pusher.json
secrets.env
signed-api.json
.DS_Store

# Do not ignore e2e config files. These don't need to be listed on Dockerignore because e2e package is not dockerized.
!packages/e2e/**/pusher.json
!packages/e2e/**/secrets.env
!packages/e2e/**/.env
!packages/e2e/**/signed-api.json
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ A monorepo for managing signed data. Consists of:
- [common](./packages/common/README.md) - An internal-only package with common types and utilities used by other
packages.
- [pusher](./packages/pusher/README.md) - A service for pushing data provider signed data.
- TODO: mention e2e
- [e2e](./packages/e2e/README.md) - End to end test utilizing Mock API, pusher and signed API.

## Getting started

Expand Down
25 changes: 24 additions & 1 deletion packages/e2e/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
# e2e

TODO: docs
> End to end test utilizing Mock API, pusher and signed API.
## Getting started

1. If you are using Docker Desktop, you need to change the URL in `pusher/secrets.env` from `localhost` to
`host.docker.internal`, because pusher is running inside a Docker container.
2. Build the latest Docker images. Run `pnpm run docker:build` from the monorepo root. The e2e flow uses the docker
images.
3. This module contains services (or configurations) that are integrated together. Specifically:

- `pusher` - Contains the configuration for the pusher service.
- `signed-api` - Contains the configuration for the signed API service.
- `data-provider-api.ts` - Contains the configuration for the data provider API service (mocked express server).
- `user.ts` - Contains the configuration for the user service (infinite fetch from signed API).

You are free to modify the configurations to test different scenarios.

4. There are `start:<some-service>` scripts to start the services. It is recommended to start each service in a separate
terminal and in this order:

1. `pnpm run start:data-provider-api`
2. `pnpm run start:signed-api`
3. `pnpm run start:pusher`
4. `pnpm run start:user`
16 changes: 15 additions & 1 deletion packages/e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,30 @@
"scripts": {
"build": "tsc --project tsconfig.build.json",
"clean": "rm -rf coverage dist",
"dev": "ts-node src/index.ts",
"eslint:check": "eslint . --ext .js,.ts --max-warnings 0",
"eslint:fix": "eslint . --ext .js,.ts --fix",
"prettier:check": "prettier --check \"./**/*.{js,ts,md,yml,json}\"",
"prettier:fix": "prettier --write \"./**/*.{js,ts,md,yml,json}\"",
"start:data-provider-api": "ts-node src/data-provider-api.ts",
"start:pusher": "docker run -it --init --volume $(pwd)/src/pusher:/app/config --env-file ./src/pusher/.env --rm --memory=256m pusher:latest",
"start:signed-api": "docker run --publish 8090:8090 -it --init --volume $(pwd)/src/signed-api:/app/config --env-file ./src/signed-api/.env --rm --memory=256m api:latest",
"start:user": "ts-node src/user.ts",
"test": "jest --passWithNoTests",
"test:e2e": "jest",
"tsc": "tsc --project ."
},
"license": "MIT",
"dependencies": {
"@api3/promise-utils": "^0.4.0",
"axios": "^1.5.1",
"ethers": "^5.7.2",
"express": "^4.18.2",
"lodash": "^4.17.21",
"signed-api/common": "workspace:common@*",
"zod": "^3.22.2"
},
"devDependencies": {
"@types/express": "^4.17.18",
"@types/lodash": "^4.14.199"
}
}
52 changes: 52 additions & 0 deletions packages/e2e/src/data-provider-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import express from 'express';
import { logger } from './logger';

const app = express();
const PORT = 9876 || process.env.PORT;

interface Asset {
value: number;
// Everytime the API is queried, the value will be updated by a random percentage.
deltaPercent: number;
name: string;
}

const assets: Asset[] = [
{
value: 1000,
deltaPercent: 10,
name: 'MOCK-ETH/USD',
},
{
value: 5000,
deltaPercent: 2,
name: 'MOCK-BTC/USD',
},
{
value: 750,
deltaPercent: 80,
name: 'MOCK-ABC/DEF',
},
{
value: 50000,
deltaPercent: 20,
name: 'MOCK-HJK/KOP',
},
];

app.get('/', (_req, res) => {
logger.debug('Request GET /');

for (const asset of assets) {
asset.value = parseFloat((asset.value * (1 + ((Math.random() - 0.5) * asset.deltaPercent) / 100)).toFixed(5));
}

const response = Object.fromEntries(assets.map((asset) => [asset.name, asset.value]));
logger.debug('Response GET /', response);

res.json(response);
});

app.listen(PORT, () => {
logger.info(`Server is running on http://localhost:${PORT}`);
});
1 change: 0 additions & 1 deletion packages/e2e/src/index.ts

This file was deleted.

8 changes: 8 additions & 0 deletions packages/e2e/src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { createLogger } from 'signed-api/common';

export const logger = createLogger({
colorize: true,
enabled: true,
minLevel: 'debug',
format: 'pretty',
});
4 changes: 4 additions & 0 deletions packages/e2e/src/pusher/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
LOGGER_ENABLED=true
LOG_COLORIZE=true
LOG_FORMAT=pretty
LOG_LEVEL=debug
111 changes: 111 additions & 0 deletions packages/e2e/src/pusher/pusher.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
{
"airnodeWalletMnemonic": "diamond result history offer forest diagram crop armed stumble orchard stage glance",
"rateLimiting": { "Mock API": { "maxConcurrency": 25, "minTime": 0 } },
"templates": {
"0xd1c9c79ccb6e24f77c14456b9b037ded9bfd0709468297e4e1e1bedbfe1bbf1a": {
"endpointId": "0xa02c7e24d1d73f429927eedb78185a7d7d7c82d410acc3914cf6213aa29fea3f",
"parameters": [{ "type": "string32", "name": "name", "value": "MOCK-ETH/USD" }]
},
"0x4edc7440a34c8420ff592e4b2455785a9218c388a3112a2a92beedcb3e8feeba": {
"endpointId": "0xa02c7e24d1d73f429927eedb78185a7d7d7c82d410acc3914cf6213aa29fea3f",
"parameters": [{ "type": "string32", "name": "name", "value": "MOCK-BTC/USD" }]
},
"0xcc72aa1d058c5db90c4595a6fcfb320dce997b978dcb6d1f176a121c05d47eeb": {
"endpointId": "0xa02c7e24d1d73f429927eedb78185a7d7d7c82d410acc3914cf6213aa29fea3f",
"parameters": [{ "type": "string32", "name": "name", "value": "MOCK-ABC/DEF" }]
},
"0xcd2516b29a7634861a010155a3666b1be9af9cdb2d442e8f1ac1607c42862f9e": {
"endpointId": "0xa02c7e24d1d73f429927eedb78185a7d7d7c82d410acc3914cf6213aa29fea3f",
"parameters": [{ "type": "string32", "name": "name", "value": "MOCK-HJK/KOP" }]
}
},
"endpoints": {
"0xa02c7e24d1d73f429927eedb78185a7d7d7c82d410acc3914cf6213aa29fea3f": {
"endpointName": "mock-feed",
"oisTitle": "Mock API"
}
},
"triggers": {
"signedApiUpdates": [
{
"signedApiName": "localhost",
"templateIds": [
"0xd1c9c79ccb6e24f77c14456b9b037ded9bfd0709468297e4e1e1bedbfe1bbf1a",
"0x4edc7440a34c8420ff592e4b2455785a9218c388a3112a2a92beedcb3e8feeba"
],
"fetchInterval": 5,
"updateDelay": 0
},
{
"signedApiName": "localhost",
"templateIds": [
"0x4edc7440a34c8420ff592e4b2455785a9218c388a3112a2a92beedcb3e8feeba",
"0xcc72aa1d058c5db90c4595a6fcfb320dce997b978dcb6d1f176a121c05d47eeb",
"0xcd2516b29a7634861a010155a3666b1be9af9cdb2d442e8f1ac1607c42862f9e"
],
"fetchInterval": 3,
"updateDelay": 5
},
{
"signedApiName": "localhost",
"templateIds": [
"0xd1c9c79ccb6e24f77c14456b9b037ded9bfd0709468297e4e1e1bedbfe1bbf1a",
"0xcc72aa1d058c5db90c4595a6fcfb320dce997b978dcb6d1f176a121c05d47eeb",
"0xcd2516b29a7634861a010155a3666b1be9af9cdb2d442e8f1ac1607c42862f9e"
],
"fetchInterval": 1,
"updateDelay": 0
}
]
},
"signedApis": [
{
"name": "localhost",
"url": "http://${HOST_IP}:8090"
}
],
"ois": [
{
"oisFormat": "2.2.0",
"title": "Mock API",
"version": "0.2.0",
"apiSpecifications": {
"components": {
"securitySchemes": {}
},
"paths": {
"/": { "get": { "parameters": [] } }
},
"servers": [{ "url": "http://${HOST_IP}:9876" }],
"security": {}
},
"endpoints": [
{
"fixedOperationParameters": [],
"name": "mock-feed",
"operation": { "method": "get", "path": "/" },
"parameters": [{ "name": "name" }],
"reservedParameters": [
{ "name": "_type", "fixed": "int256" },
{ "name": "_times", "fixed": "1000000000000000000" }
],
"preProcessingSpecifications": [
{
"environment": "Node",
"value": "const output = {};",
"timeoutMs": 5000
}
],
"postProcessingSpecifications": [
{
"environment": "Node",
"value": "const output = input[endpointParameters.name];",
"timeoutMs": 5000
}
]
}
]
}
],
"apiCredentials": []
}
4 changes: 4 additions & 0 deletions packages/e2e/src/pusher/secrets.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Secrets must NOT be quoted.
#
# Use "host.docker.internal" if using Docker for Desktop.
HOST_IP=localhost
16 changes: 16 additions & 0 deletions packages/e2e/src/signed-api/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
LOGGER_ENABLED=true
LOG_COLORIZE=true
LOG_FORMAT=pretty
LOG_LEVEL=debug

# Available options
# local (default) - loads config/signed-api.json from the filesystem
# aws-s3 - loads the config file from AWS S3
CONFIG_SOURCE=local

# Set these variables if you would like to source your config from AWS S3
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_REGION=
AWS_S3_BUCKET_NAME=
AWS_S3_BUCKET_PATH=
17 changes: 17 additions & 0 deletions packages/e2e/src/signed-api/signed-api.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"endpoints": [
{
"urlPath": "/real-time",
"delaySeconds": 0
},
{
"urlPath": "/delayed",
"delaySeconds": 10
}
],
"maxBatchSize": 10,
"port": 8090,
"cache": {
"maxAgeSeconds": 0
}
}
28 changes: 28 additions & 0 deletions packages/e2e/src/user.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import axios from 'axios';
import { airnode, formatData } from './utils';

it('respects the delay', async () => {
const start = Date.now();
let [realCount, delayedCount] = [0, 0];

while (Date.now() - start < 15_000) {
const realTimeResponse = await axios.get(`http://localhost:8090/real-time/${airnode}`);
const realTimeData = formatData(realTimeResponse.data);
const delayedResponse = await axios.get(`http://localhost:8090/delayed/${airnode}`);
const delayedData = formatData(delayedResponse.data);

for (const data of realTimeData) {
expect(data.delay).toBeGreaterThan(0);
realCount++;
}
for (const data of delayedData) {
expect(data.delay).toBeGreaterThan(10_000);
delayedCount++;
}

await new Promise((resolve) => setTimeout(resolve, 300));
}

expect(realCount).toBeGreaterThan(0);
expect(delayedCount).toBeGreaterThan(0);
}, 20_000);
20 changes: 20 additions & 0 deletions packages/e2e/src/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import axios from 'axios';
import { logger } from './logger';
import { airnode, formatData } from './utils';

async function main() {
// eslint-disable-next-line no-constant-condition
while (true) {
logger.debug('Making requests');

const realTimeResponse = await axios.get(`http://localhost:8090/real-time/${airnode}`);
logger.debug('Response GET /real-time', formatData(realTimeResponse.data));

const delayedResponse = await axios.get(`http://localhost:8090/delayed/${airnode}`);
logger.debug('Response GET /delayed', formatData(delayedResponse.data));

await new Promise((resolve) => setTimeout(resolve, 1000));
}
}

main();
Loading

0 comments on commit 81f5c3d

Please sign in to comment.