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 0000000..99bce4f Binary files /dev/null and b/test/bun.lockb differ 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)) }