From 9f89af87e2be5c0c7d242a7dc6309a1273d2b259 Mon Sep 17 00:00:00 2001 From: Luc van Kampen Date: Sat, 13 Jan 2024 12:52:40 +0000 Subject: [PATCH] Introduce Integration Tests (#39) * Introduce bun testing * Update Caching in Test Server * Bump * Update Testing * Bump * Bump * Update test/src/test_implementation.ts * Restructure datasets & introduce address and universal endpoint tests * Introduce bulk endpoint testing * Lint * Fix worker bulk implementation & test worker action * Update workflows * Update workflows * Update workflow * Update workflow * Update test * Update workflow * Update workflow --------- Co-authored-by: Antony1060 --- .github/workflows/build.yml | 12 ++- .github/workflows/pr_check.yml | 24 ++--- .github/workflows/test.yml | 41 ++++++++ server/src/database/mod.rs | 2 +- server/src/state.rs | 18 +++- shared/src/cache/mod.rs | 14 +++ test/.eslintrc.json | 24 +++++ test/.gitignore | 175 ++++++++++++++++++++++++++++++++ test/.prettierrc | 6 ++ test/README.md | 7 ++ test/bun.lockb | Bin 0 -> 116945 bytes test/data/bulk.ts | 169 ++++++++++++++++++++++++++++++ test/data/index.ts | 10 ++ test/data/single.ts | 103 +++++++++++++++++++ test/index.ts | 1 + test/package.json | 22 ++++ test/src/http_fetch.ts | 5 + test/src/test_implementation.ts | 19 ++++ test/tests/server.spec.ts | 88 ++++++++++++++++ test/tests/worker.spec.ts | 74 ++++++++++++++ test/tsconfig.json | 22 ++++ worker/src/bulk_util.rs | 35 +++++++ worker/src/http_util.rs | 19 ---- worker/src/lib.rs | 1 + worker/src/routes/address.rs | 6 +- worker/src/routes/name.rs | 6 +- worker/src/routes/universal.rs | 6 +- 27 files changed, 861 insertions(+), 48 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 test/.eslintrc.json create mode 100644 test/.gitignore create mode 100644 test/.prettierrc create mode 100644 test/README.md create mode 100755 test/bun.lockb create mode 100644 test/data/bulk.ts create mode 100644 test/data/index.ts create mode 100644 test/data/single.ts create mode 100644 test/index.ts create mode 100644 test/package.json create mode 100644 test/src/http_fetch.ts create mode 100644 test/src/test_implementation.ts create mode 100644 test/tests/server.spec.ts create mode 100644 test/tests/worker.spec.ts create mode 100644 test/tsconfig.json create mode 100644 worker/src/bulk_util.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d143f0..f6ac901 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,14 +3,21 @@ on: push: tags: - "v*.*.*" + workflow_call: env: CARGO_TERM_COLOR: always jobs: + test_server: + uses: ./.github/workflows/test.yml build: name: Build ENState 🚀 runs-on: ubuntu-latest + needs: [test_server] + env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" steps: - uses: actions/checkout@v3 with: @@ -20,7 +27,10 @@ jobs: rustup set auto-self-update disable rustup toolchain install stable --profile minimal - - uses: Swatinem/rust-cache@v2 + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.3 + with: + version: "v0.7.4" - run: cargo build --release working-directory: server diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml index d379e1a..a39c070 100644 --- a/.github/workflows/pr_check.yml +++ b/.github/workflows/pr_check.yml @@ -1,4 +1,4 @@ -name: Build and Deploy +name: PR Check on: pull_request: branches: @@ -19,6 +19,9 @@ jobs: target: x86_64-unknown-linux-gnu - path: worker target: wasm32-unknown-unknown + env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" steps: - uses: actions/checkout@v3 with: @@ -29,20 +32,13 @@ jobs: rustup toolchain install stable --profile minimal rustup target add ${{ matrix.target }} - - name: Set up cargo cache - uses: actions/cache@v3 - continue-on-error: false + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.3 with: - path: | - ~/.cargo/bin/ - ~/.cargo/registry/index/ - ~/.cargo/registry/cache/ - ~/.cargo/git/db/ - server/target/ - worker/target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: ${{ runner.os }}-cargo- + version: "v0.7.4" - run: cargo check --target ${{ matrix.target }} --release working-directory: ${{ matrix.path }} - + test: + uses: ./.github/workflows/test.yml + needs: [check] diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..ad13403 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,41 @@ +on: + workflow_call: + +jobs: + test: + name: Test ENState 🚀 + runs-on: ubuntu-latest + env: + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" + strategy: + matrix: + suite: [server, worker] + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - run: | + rustup set auto-self-update disable + rustup toolchain install stable --profile minimal + + - name: Run sccache-cache + uses: mozilla-actions/sccache-action@v0.0.3 + with: + version: "v0.7.4" + + - uses: oven-sh/setup-bun@v1 + + - run: bun install + working-directory: test + + - run: bun install --global wrangler + if: ${{ matrix.suite == 'worker' }} + + - name: Test + run: bun test ${{ matrix.suite }} + working-directory: test + env: + RPC_URL: https://rpc.ankr.com/eth + OPENSEA_API_KEY: ${{ secrets.OPENSEA_API_KEY }} diff --git a/server/src/database/mod.rs b/server/src/database/mod.rs index 1de0215..91fd998 100644 --- a/server/src/database/mod.rs +++ b/server/src/database/mod.rs @@ -4,7 +4,7 @@ use anyhow::Result; use redis::aio::ConnectionManager; pub async fn setup() -> Result { - let redis = redis::Client::open(env::var("REDIS_URL").expect("REDIS_URL should've been set"))?; + let redis = redis::Client::open(env::var("REDIS_URL")?)?; Ok(ConnectionManager::new(redis).await?) } diff --git a/server/src/state.rs b/server/src/state.rs index b5c45ee..2ef96d2 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -1,3 +1,4 @@ +use enstate_shared::cache::{CacheLayer, PassthroughCacheLayer}; use std::env; use std::sync::Arc; @@ -6,7 +7,7 @@ use enstate_shared::models::{ multicoin::cointype::{coins::CoinType, Coins}, records::Records, }; -use tracing::info; +use tracing::{info, warn}; use crate::provider::RoundRobin; use crate::{cache, database}; @@ -43,9 +44,18 @@ impl AppState { info!("Connecting to Redis..."); - let redis = database::setup().await.expect("Redis connection failed"); + let cache = database::setup().await.map_or_else( + |_| { + warn!("failed to connect to redis, using no cache"); - info!("Connected to Redis"); + Box::new(PassthroughCacheLayer {}) as Box + }, + |redis| { + info!("Connected to Redis"); + + Box::new(cache::Redis::new(redis)) as Box + }, + ); let provider = RoundRobin::new(rpc_urls); @@ -54,7 +64,7 @@ impl AppState { Self { service: ProfileService { - cache: Box::new(cache::Redis::new(redis)), + cache, rpc: Box::new(provider), opensea_api_key, profile_records: Arc::from(profile_records), diff --git a/shared/src/cache/mod.rs b/shared/src/cache/mod.rs index c506bec..3674352 100644 --- a/shared/src/cache/mod.rs +++ b/shared/src/cache/mod.rs @@ -13,3 +13,17 @@ pub trait CacheLayer: Send + Sync { async fn get(&self, key: &str) -> Result; async fn set(&self, key: &str, value: &str, expires: u32) -> Result<(), CacheError>; } + +pub struct PassthroughCacheLayer {} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl CacheLayer for PassthroughCacheLayer { + async fn get(&self, _key: &str) -> Result { + Err(CacheError::Other("".to_string())) + } + + async fn set(&self, _key: &str, _value: &str, _expires: u32) -> Result<(), CacheError> { + Ok(()) + } +} diff --git a/test/.eslintrc.json b/test/.eslintrc.json new file mode 100644 index 0000000..6047b27 --- /dev/null +++ b/test/.eslintrc.json @@ -0,0 +1,24 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2021 + }, + "extends": [ + "plugin:v3xlabs/recommended" + ], + "ignorePatterns": [ + "!**/*" + ], + "plugins": [ + "v3xlabs" + ], + "env": { + "node": true + }, + "globals": { + "Bun": false + }, + "rules": { + "sonarjs/no-duplicate-string": "off" + } +} \ No newline at end of file diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..468f82a --- /dev/null +++ b/test/.gitignore @@ -0,0 +1,175 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/test/.prettierrc b/test/.prettierrc new file mode 100644 index 0000000..59070c1 --- /dev/null +++ b/test/.prettierrc @@ -0,0 +1,6 @@ +{ + "tabWidth": 4, + "useTabs": false, + "singleQuote": true, + "printWidth": 100 +} \ No newline at end of file diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..4a658a8 --- /dev/null +++ b/test/README.md @@ -0,0 +1,7 @@ +# enstate-coverage + +To run: + +```bash +bun test +``` diff --git a/test/bun.lockb b/test/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..99bce4f625f2f9de8e1ab4a7a552d85c3fd36aad GIT binary patch literal 116945 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 I{tN#1Uo%QSfdBvi literal 0 HcmV?d00001 diff --git a/test/data/bulk.ts b/test/data/bulk.ts new file mode 100644 index 0000000..5aa3d7c --- /dev/null +++ b/test/data/bulk.ts @@ -0,0 +1,169 @@ +import qs from 'qs'; + +import { Dataset } from '.'; + +export const dataset_name_bulk: Dataset<{ + response: { address: string }[]; + response_length: number; +}> = [ + { + label: 'ETHRegistry', + arg: qs.stringify({ names: ['luc.eth', 'nick.eth'] }, { encode: false }), + expected: { + response: [ + { address: '0x225f137127d9067788314bc7fcc1f36746a3c3B5' }, + { address: '0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5' }, + ], + response_length: 2, + }, + }, + { + label: 'ETHRegistry (extra)', + arg: qs.stringify({ names: ['luc.eth', 'nick.eth', 'nick.eth'] }, { encode: false }), + expected: { + response: [ + { address: '0x225f137127d9067788314bc7fcc1f36746a3c3B5' }, + { address: '0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5' }, + ], + response_length: 2, + }, + }, + { + label: 'DNSRegistry', + arg: qs.stringify({ names: ['luc.computer', 'antony.sh'] }, { encode: false }), + expected: { + response: [ + { address: '0x225f137127d9067788314bc7fcc1f36746a3c3B5' }, + { address: '0x2B5c7025998f88550Ef2fEce8bf87935f542C190' }, + ], + response_length: 2, + }, + }, + { + label: 'CCIP', + arg: qs.stringify({ names: ['luc.willbreak.eth', 'lucemans.cb.id'] }, { encode: false }), + expected: { + response: [ + { address: '0x225f137127d9067788314bc7fcc1f36746a3c3B5' }, + { address: '0x4e7abb71BEe38011c54c30D0130c0c71Da09222b' }, + ], + response_length: 2, + }, + }, +]; + +export const dataset_address_bulk: Dataset<{ + response: { name: string }[]; + response_length: number; +}> = [ + { + label: 'ETHRegistry', + arg: qs.stringify( + { + addresses: [ + '0x225f137127d9067788314bc7fcc1f36746a3c3B5', + '0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5', + ], + }, + { encode: false } + ), + expected: { + response: [{ name: 'luc.eth' }, { name: 'nick.eth' }], + response_length: 2, + }, + }, + { + label: 'ETHRegistry (extra)', + arg: qs.stringify( + { + addresses: [ + '0x2B5c7025998f88550Ef2fEce8bf87935f542C190', + '0x2B5c7025998f88550Ef2fEce8bf87935F542c190', + ], + }, + { encode: false } + ), + expected: { response: [{ name: 'antony.sh' }], response_length: 1 }, + }, + { + label: 'DNSRegistry', + arg: qs.stringify( + { addresses: ['0x2B5c7025998f88550Ef2fEce8bf87935f542C190'] }, + { encode: false } + ), + expected: { response: [{ name: 'antony.sh' }], response_length: 1 }, + }, + // { + // label: 'CCIP', + // arg: qs.stringify( + // { names: ['luc.willbreak.eth', 'lucemans.cb.id'] }, + // { encode: false } + // ), + // expected: { + // response: [ + // { name: '0x225f137127d9067788314bc7fcc1f36746a3c3B5' }, + // { name: '0x225f137127d9067788314bc7fcc1f36746a3c3B5' }, + // ], + // response_length: 2, + // }, + // }, +]; + +export const dataset_universal_bulk: Dataset<{ + response: ({ address: string } | { name: string })[]; + response_length: number; +}> = [ + { + label: 'ETHRegistry', + arg: qs.stringify( + { + queries: ['luc.eth', '0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5'], + }, + { encode: false } + ), + expected: { + response: [ + { address: '0x225f137127d9067788314bc7fcc1f36746a3c3B5' }, + { name: 'nick.eth' }, + ], + response_length: 2, + }, + }, + { + label: 'DNSRegistry', + arg: qs.stringify( + { + queries: ['0x2B5c7025998f88550Ef2fEce8bf87935f542C190', 'antony.sh'], + }, + { encode: false } + ), + expected: { + response: [ + { name: 'antony.sh' }, + { address: '0x2B5c7025998f88550Ef2fEce8bf87935f542C190' }, + ], + response_length: 2, + }, + }, + { + label: 'Mixed', + arg: qs.stringify( + { + queries: [ + '0x2B5c7025998f88550Ef2fEce8bf87935f542C190', + 'luc.eth', + 'luc.willbreak.eth', + ], + }, + { encode: false } + ), + expected: { + response: [ + { name: 'antony.sh' }, + { address: '0x225f137127d9067788314bc7fcc1f36746a3c3B5' }, + { address: '0x225f137127d9067788314bc7fcc1f36746a3c3B5' }, + ], + response_length: 3, + }, + }, +]; diff --git a/test/data/index.ts b/test/data/index.ts new file mode 100644 index 0000000..e38c7af --- /dev/null +++ b/test/data/index.ts @@ -0,0 +1,10 @@ +export * from './bulk'; +export * from './single'; + +export type DatasetEntry = { + label: string; + arg: string; + expected: T; +}; + +export type Dataset = DatasetEntry[]; diff --git a/test/data/single.ts b/test/data/single.ts new file mode 100644 index 0000000..bee8580 --- /dev/null +++ b/test/data/single.ts @@ -0,0 +1,103 @@ +import { Dataset } from '.'; + +export const dataset_name_single: Dataset<{ address: string }> = [ + { + label: 'ETHRegistry', + arg: 'luc.eth', + expected: { address: '0x225f137127d9067788314bc7fcc1f36746a3c3B5' }, + }, + { + label: 'ETHRegistry', + arg: 'nick.eth', + expected: { address: '0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5' }, + }, + { + label: 'DNSRegistry', + arg: 'luc.computer', + expected: { address: '0x225f137127d9067788314bc7fcc1f36746a3c3B5' }, + }, + { + label: 'DNSRegistry', + arg: 'antony.sh', + expected: { address: '0x2B5c7025998f88550Ef2fEce8bf87935f542C190' }, + }, + { + label: 'CCIP Offchain RS', + arg: 'luc.willbreak.eth', + expected: { address: '0x225f137127d9067788314bc7fcc1f36746a3c3B5' }, + }, + { + label: 'CCIP Coinbase', + arg: 'lucemans.cb.id', + expected: { address: '0x4e7abb71BEe38011c54c30D0130c0c71Da09222b' }, + }, +]; + +export const dataset_address_single: Dataset<{ name: string }> = [ + { + label: 'ETHRegistry', + arg: '0x225f137127d9067788314bc7fcc1f36746a3c3B5', + expected: { name: 'luc.eth' }, + }, + { + label: 'ETHRegistry', + arg: '0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5', + expected: { name: 'nick.eth' }, + }, + // TODO: find another dns primary name address + // { + // label: 'DNSRegistry', + // arg: '0x225f137127d9067788314bc7fcc1f36746a3c3B5', + // expected: { name: 'luc.computer' }, + // }, + { + label: 'DNSRegistry', + arg: '0x2B5c7025998f88550Ef2fEce8bf87935f542C190', + expected: { name: 'antony.sh' }, + }, + // TODO: find 2 ccip primary name addresses + // { + // label: 'CCIP Offchain RS', + // arg: '0x225f137127d9067788314bc7fcc1f36746a3c3B5', + // expected: { name: 'luc.willbreak.eth' }, + // }, + // { + // label: 'CCIP Coinbase', + // arg: '0x4e7abb71BEe38011c54c30D0130c0c71Da09222b', + // expected: { name: 'lucemans.cb.id' }, + // }, +]; + +export const dataset_universal_single: Dataset<{ address: string } | { name: string }> = [ + { + label: 'ETHRegistry', + arg: 'luc.eth', + expected: { address: '0x225f137127d9067788314bc7fcc1f36746a3c3B5' }, + }, + { + label: 'ETHRegistry', + arg: '0xb8c2C29ee19D8307cb7255e1Cd9CbDE883A267d5', + expected: { name: 'nick.eth' }, + }, + { + label: 'DNSRegistry', + arg: 'luc.computer', + expected: { address: '0x225f137127d9067788314bc7fcc1f36746a3c3B5' }, + }, + { + label: 'DNSRegistry', + arg: '0x2B5c7025998f88550Ef2fEce8bf87935f542C190', + expected: { name: 'antony.sh' }, + }, + { + label: 'CCIP Offchain RS', + arg: 'luc.willbreak.eth', + expected: { address: '0x225f137127d9067788314bc7fcc1f36746a3c3B5' }, + }, + // TODO: refer to above todo + // { + // label: 'CCIP Coinbase', + // arg: '0x4e7abb71BEe38011c54c30D0130c0c71Da09222b', + // expected: { name: 'lucemans.cb.id' }, + // }, +]; diff --git a/test/index.ts b/test/index.ts new file mode 100644 index 0000000..4d99dce --- /dev/null +++ b/test/index.ts @@ -0,0 +1 @@ +console.log("Heya! You probably ment to run `bun test`"); diff --git a/test/package.json b/test/package.json new file mode 100644 index 0000000..dfc4013 --- /dev/null +++ b/test/package.json @@ -0,0 +1,22 @@ +{ + "name": "enstate-tests", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@typescript-eslint/parser": "^6.17.0", + "bun-types": "latest", + "eslint": "^8.56.0", + "eslint-plugin-v3xlabs": "^1.6.2", + "typescript": "^5.3.3" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@types/qs": "^6.9.11", + "qs": "^6.11.2" + }, + "scripts": { + "lint": "eslint -c .eslintrc.json --ext .ts ./src ./tests ./data" + } +} diff --git a/test/src/http_fetch.ts b/test/src/http_fetch.ts new file mode 100644 index 0000000..4fcaf73 --- /dev/null +++ b/test/src/http_fetch.ts @@ -0,0 +1,5 @@ +export const http_fetch = (url: string) => async (input: string) => { + const result = await fetch(url + input); + + return await result.json(); +}; diff --git a/test/src/test_implementation.ts b/test/src/test_implementation.ts new file mode 100644 index 0000000..fae29bf --- /dev/null +++ b/test/src/test_implementation.ts @@ -0,0 +1,19 @@ +import { describe, expect, test } from 'bun:test'; + +import { Dataset } from '../data'; + +export const test_implementation = , DataType extends {}>( + function_name: string, + resolve_function: (_input: string) => Promise>, + dataset: DataSet +) => { + describe('t/' + function_name, () => { + for (const { label, arg, expected } of dataset) { + test(label + ` (${arg})`, async () => { + const output = await resolve_function(arg); + + expect(output).toMatchObject(expected); + }); + } + }); +}; diff --git a/test/tests/server.spec.ts b/test/tests/server.spec.ts new file mode 100644 index 0000000..f8363d6 --- /dev/null +++ b/test/tests/server.spec.ts @@ -0,0 +1,88 @@ +import { Subprocess } from 'bun'; +import { afterAll, beforeAll } from 'bun:test'; + +import { dataset_address_bulk, dataset_name_bulk, dataset_universal_bulk } from '../data'; +import { + dataset_address_single, + dataset_name_single, + dataset_universal_single, +} from '../data/single'; +import { http_fetch } from '../src/http_fetch'; +import { test_implementation } from '../src/test_implementation'; + +const TEST_RELEASE = true; + +let server: Subprocess | undefined; + +beforeAll(async () => { + console.log('Building server...'); + + await new Promise((resolve) => { + Bun.spawn(['cargo', 'build', TEST_RELEASE ? '--release' : ''], { + cwd: '../server', + onExit() { + resolve(); + }, + }); + }); + + console.log('Build finished!'); + + server = Bun.spawn([`../server/target/${TEST_RELEASE ? 'release' : 'debug'}/enstate`], { + cwd: '../server', + }); + + console.log('Waiting for server to start...'); + + let attempts = 0; + + while (attempts < 10) { + try { + console.log('Attempting heartbeat...'); + await fetch('http://0.0.0.0:3000/'); + console.log('Heartbeat succes!'); + break; + } catch { + console.log('Waiting another 1s for heartbeat...'); + attempts++; + await new Promise((resolve) => setTimeout(resolve, 1000)); + continue; + } + } + + console.log('Ready to start testing'); +}); + +afterAll(async () => { + server?.kill(); +}); + +const PREFIX = 'server'; + +test_implementation(`${PREFIX}/name`, http_fetch('http://0.0.0.0:3000/n/'), dataset_name_single); +test_implementation( + `${PREFIX}/address`, + http_fetch('http://0.0.0.0:3000/a/'), + dataset_address_single +); +test_implementation( + `${PREFIX}/universal`, + http_fetch('http://0.0.0.0:3000/u/'), + dataset_universal_single +); + +test_implementation( + `${PREFIX}/bulk/name`, + http_fetch('http://0.0.0.0:3000/bulk/n?'), + dataset_name_bulk +); +test_implementation( + `${PREFIX}/bulk/address`, + http_fetch('http://0.0.0.0:3000/bulk/a?'), + dataset_address_bulk +); +test_implementation( + `${PREFIX}/bulk/universal`, + http_fetch('http://0.0.0.0:3000/bulk/u?'), + dataset_universal_bulk +); diff --git a/test/tests/worker.spec.ts b/test/tests/worker.spec.ts new file mode 100644 index 0000000..6b64ee6 --- /dev/null +++ b/test/tests/worker.spec.ts @@ -0,0 +1,74 @@ +import { Subprocess } from 'bun'; +import { afterAll, beforeAll } from 'bun:test'; + +import { dataset_address_bulk, dataset_name_bulk, dataset_universal_bulk } from '../data'; +import { + dataset_address_single, + dataset_name_single, + dataset_universal_single, +} from '../data/single'; +import { http_fetch } from '../src/http_fetch'; +import { test_implementation } from '../src/test_implementation'; + +let server: Subprocess<'ignore', 'pipe', 'inherit'> | undefined; + +beforeAll(async () => { + console.log('Building worker...'); + + server = Bun.spawn(['wrangler', 'dev', '--port', '3000'], { cwd: '../worker' }); + + console.log('Waiting for server to start...'); + + // TODO: fix + // eslint-disable-next-line no-constant-condition + while (true) { + try { + console.log('Attempting heartbeat...'); + await fetch('http://0.0.0.0:3000/'); + console.log('Heartbeat succes!'); + break; + } catch { + console.log('Waiting another 1s for heartbeat...'); + await new Promise((resolve) => setTimeout(resolve, 1000)); + continue; + } + } + + console.log('Ready to start testing'); +}); + +afterAll(async () => { + server?.kill(); + + await server?.exited; +}); + +const PREFIX = 'worker'; + +test_implementation(`${PREFIX}/name`, http_fetch('http://0.0.0.0:3000/n/'), dataset_name_single); +test_implementation( + `${PREFIX}/address`, + http_fetch('http://0.0.0.0:3000/a/'), + dataset_address_single +); +test_implementation( + `${PREFIX}/universal`, + http_fetch('http://0.0.0.0:3000/u/'), + dataset_universal_single +); + +test_implementation( + `${PREFIX}/bulk/name`, + http_fetch('http://0.0.0.0:3000/bulk/n?'), + dataset_name_bulk +); +test_implementation( + `${PREFIX}/bulk/address`, + http_fetch('http://0.0.0.0:3000/bulk/a?'), + dataset_address_bulk +); +test_implementation( + `${PREFIX}/bulk/universal`, + http_fetch('http://0.0.0.0:3000/bulk/u?'), + dataset_universal_bulk +); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..7556e1d --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "allowImportingTsExtensions": true, + "noEmit": true, + "composite": true, + "strict": true, + "downlevelIteration": true, + "skipLibCheck": true, + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "types": [ + "bun-types" // add Bun global + ] + } +} diff --git a/worker/src/bulk_util.rs b/worker/src/bulk_util.rs new file mode 100644 index 0000000..23ec9bd --- /dev/null +++ b/worker/src/bulk_util.rs @@ -0,0 +1,35 @@ +use crate::http_util::ValidationError; +use enstate_shared::utils::vec::dedup_ord; + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct BulkResponse { + pub(crate) response_length: usize, + pub(crate) response: Vec, +} + +impl From> for BulkResponse { + fn from(value: Vec) -> Self { + BulkResponse { + response_length: value.len(), + response: value, + } + } +} + +pub fn validate_bulk_input( + input: &[String], + max_len: usize, +) -> Result, ValidationError> { + let unique = dedup_ord( + &input + .iter() + .map(|entry| entry.to_lowercase()) + .collect::>(), + ); + + if unique.len() > max_len { + return Err(ValidationError::MaxLengthExceeded(max_len)); + } + + Ok(unique) +} diff --git a/worker/src/http_util.rs b/worker/src/http_util.rs index 3a698a7..43437d0 100644 --- a/worker/src/http_util.rs +++ b/worker/src/http_util.rs @@ -1,5 +1,4 @@ use enstate_shared::models::profile::error::ProfileError; -use enstate_shared::utils::vec::dedup_ord; use ethers::prelude::ProviderError; use http::status::StatusCode; use serde::de::DeserializeOwned; @@ -47,24 +46,6 @@ impl From for worker::Error { } } -pub fn validate_bulk_input( - input: &[String], - max_len: usize, -) -> Result, ValidationError> { - let unique = dedup_ord( - &input - .iter() - .map(|entry| entry.to_lowercase()) - .collect::>(), - ); - - if unique.len() > max_len { - return Err(ValidationError::MaxLengthExceeded(max_len)); - } - - Ok(unique) -} - #[derive(Serialize)] pub struct ErrorResponse { pub(crate) status: u16, diff --git a/worker/src/lib.rs b/worker/src/lib.rs index 04845e0..b30188b 100644 --- a/worker/src/lib.rs +++ b/worker/src/lib.rs @@ -14,6 +14,7 @@ use worker::{event, Context, Cors, Env, Headers, Method, Request, Response, Rout use crate::http_util::http_simple_status_error; use crate::kv_cache::CloudflareKVCache; +mod bulk_util; mod http_util; mod kv_cache; mod routes; diff --git a/worker/src/routes/address.rs b/worker/src/routes/address.rs index 91b347d..35006f6 100644 --- a/worker/src/routes/address.rs +++ b/worker/src/routes/address.rs @@ -5,9 +5,9 @@ use http::StatusCode; use serde::Deserialize; use worker::{Request, Response, RouteContext}; +use crate::bulk_util::{validate_bulk_input, BulkResponse}; use crate::http_util::{ - http_simple_status_error, parse_query, profile_http_error_mapper, validate_bulk_input, - FreshQuery, + http_simple_status_error, parse_query, profile_http_error_mapper, FreshQuery, }; pub async fn get(req: Request, ctx: RouteContext) -> worker::Result { @@ -58,5 +58,5 @@ pub async fn get_bulk(req: Request, ctx: RouteContext) -> worker .await .map_err(profile_http_error_mapper)?; - Response::from_json(&joined) + Response::from_json(&BulkResponse::from(joined)) } diff --git a/worker/src/routes/name.rs b/worker/src/routes/name.rs index 0cde98f..c2540c2 100644 --- a/worker/src/routes/name.rs +++ b/worker/src/routes/name.rs @@ -4,9 +4,9 @@ use http::StatusCode; use serde::Deserialize; use worker::{Request, Response, RouteContext}; +use crate::bulk_util::{validate_bulk_input, BulkResponse}; use crate::http_util::{ - http_simple_status_error, parse_query, profile_http_error_mapper, validate_bulk_input, - FreshQuery, + http_simple_status_error, parse_query, profile_http_error_mapper, FreshQuery, }; pub async fn get(req: Request, ctx: RouteContext) -> worker::Result { @@ -47,5 +47,5 @@ pub async fn get_bulk(req: Request, ctx: RouteContext) -> worker .await .map_err(profile_http_error_mapper)?; - Response::from_json(&joined) + Response::from_json(&BulkResponse::from(joined)) } diff --git a/worker/src/routes/universal.rs b/worker/src/routes/universal.rs index 527c05d..6410fe7 100644 --- a/worker/src/routes/universal.rs +++ b/worker/src/routes/universal.rs @@ -4,9 +4,9 @@ use http::StatusCode; use serde::Deserialize; use worker::{Request, Response, RouteContext}; +use crate::bulk_util::{validate_bulk_input, BulkResponse}; use crate::http_util::{ - http_simple_status_error, parse_query, profile_http_error_mapper, validate_bulk_input, - FreshQuery, + http_simple_status_error, parse_query, profile_http_error_mapper, FreshQuery, }; pub async fn get(req: Request, ctx: RouteContext) -> worker::Result { @@ -50,5 +50,5 @@ pub async fn get_bulk(req: Request, ctx: RouteContext) -> worker .await .map_err(profile_http_error_mapper)?; - Response::from_json(&joined) + Response::from_json(&BulkResponse::from(joined)) }