From 1c2f3d9ecc68dd5b8b781d4676296c30ec37e748 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Thu, 21 Sep 2023 02:08:20 +0900 Subject: [PATCH 01/25] chore: migrate fetch lib from cross-fetch to ofetch --- package.json | 4 ++-- src/sign/index.ts | 2 +- src/utils.ts | 2 +- yarn.lock | 54 ++++++++++++++++++++--------------------------- 4 files changed, 27 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index 8a09b03dd..61fbc0932 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ "@ethersproject/wallet": "^5.6.2", "ajv": "^8.11.0", "ajv-formats": "^2.1.1", - "cross-fetch": "^3.1.6", "json-to-graphql-query": "^2.2.4", - "lodash.set": "^4.3.2" + "lodash.set": "^4.3.2", + "ofetch": "^1.3.3" }, "devDependencies": { "@rollup/plugin-commonjs": "^18.1.0", diff --git a/src/sign/index.ts b/src/sign/index.ts index 6c703a61d..a57139170 100644 --- a/src/sign/index.ts +++ b/src/sign/index.ts @@ -1,4 +1,4 @@ -import fetch from 'cross-fetch'; +import { ofetch as fetch } from 'ofetch'; import { Web3Provider } from '@ethersproject/providers'; import { Wallet } from '@ethersproject/wallet'; import { getAddress } from '@ethersproject/address'; diff --git a/src/utils.ts b/src/utils.ts index 6193e9f05..b3c9a0047 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import fetch from 'cross-fetch'; +import { ofetch as fetch } from 'ofetch'; import { Interface } from '@ethersproject/abi'; import { Contract } from '@ethersproject/contracts'; import { isAddress } from '@ethersproject/address'; diff --git a/yarn.lock b/yarn.lock index b214a4e6f..72175819e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1789,13 +1789,6 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" -cross-fetch@^3.1.6: - version "3.1.6" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.6.tgz#bae05aa31a4da760969756318feeee6e70f15d6c" - integrity sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g== - dependencies: - node-fetch "^2.6.11" - cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -1899,6 +1892,11 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" +destr@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/destr/-/destr-2.0.1.tgz#2fc7bddc256fed1183e03f8d148391dde4023cb2" + integrity sha512-M1Ob1zPSIvlARiJUkKqvAZ3VAqQY6Jcuth/pBKQ2b1dX/Qx0OnJ8Vux6J2H5PTMQeRzWrrbTu70VxBfv/OPDJA== + diff-sequences@^29.4.3: version "29.4.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.3.tgz#9314bc1fabe09267ffeca9cbafc457d8499a13f2" @@ -3439,12 +3437,10 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -node-fetch@^2.6.11: - version "2.6.11" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25" - integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w== - dependencies: - whatwg-url "^5.0.0" +node-fetch-native@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.4.0.tgz#fbe8ac033cb6aa44bd106b5e4fd2b6277ba70fa1" + integrity sha512-F5kfEj95kX8tkDhUCYdV8dg3/8Olx/94zB8+ZNthFs6Bz31UpUi8Xh40TN3thLwXgrwXry1pEg9lJ++tLWTcqA== node-gyp@^7.1.0: version "7.1.2" @@ -3641,6 +3637,15 @@ octal@^1.0.0: resolved "https://registry.yarnpkg.com/octal/-/octal-1.0.0.tgz#63e7162a68efbeb9e213588d58e989d1e5c4530b" integrity sha1-Y+cWKmjvvrniE1iNWOmJ0eXEUws= +ofetch@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/ofetch/-/ofetch-1.3.3.tgz#588cb806a28e5c66c2c47dd8994f9059a036d8c0" + integrity sha512-s1ZCMmQWXy4b5K/TW9i/DtiN8Ku+xCiHcjQ6/J/nDdssirrQNOoB165Zu8EqLMA2lln1JUth9a0aW9Ap2ctrUg== + dependencies: + destr "^2.0.1" + node-fetch-native "^1.4.0" + ufo "^1.3.0" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -4779,11 +4784,6 @@ tough-cookie@~2.5.0: psl "^1.1.28" punycode "^2.1.1" -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - tslib@1.11.1, tslib@^1.8.1, tslib@^1.9.0: version "1.11.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" @@ -4855,6 +4855,11 @@ ufo@^1.1.2: resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.1.2.tgz#d0d9e0fa09dece0c31ffd57bd363f030a35cfe76" integrity sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ== +ufo@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.3.0.tgz#c92f8ac209daff607c57bbd75029e190930a0019" + integrity sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw== + unique-filename@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" @@ -4987,19 +4992,6 @@ vlq@^0.2.2: resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow== -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" From 0304356313abb4e2a1b29c29c4957ca0a44d37aa Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Thu, 21 Sep 2023 20:47:13 +0900 Subject: [PATCH 02/25] feat: add timeout to fetch requests --- src/sign/index.ts | 1 + src/utils.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/sign/index.ts b/src/sign/index.ts index a57139170..8a92088c4 100644 --- a/src/sign/index.ts +++ b/src/sign/index.ts @@ -87,6 +87,7 @@ export default class Client { Accept: 'application/json', 'Content-Type': 'application/json' }, + timeout: 20e3, body: JSON.stringify(envelop) }; return new Promise((resolve, reject) => { diff --git a/src/utils.ts b/src/utils.ts index b3c9a0047..e87a47e98 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -143,6 +143,7 @@ export async function subgraphRequest(url: string, query, options: any = {}) { 'Content-Type': 'application/json', ...options?.headers }, + timeout: 20e3, body: JSON.stringify({ query: jsonToGraphQLQuery({ query }) }) }); let responseData: any = await res.text(); @@ -184,7 +185,7 @@ export function getUrl(uri, gateway = gateways[0]) { export async function getJSON(uri, options: any = {}) { const url = getUrl(uri, options.gateways); - return fetch(url).then((res) => res.json()); + return fetch(url, { timeout: 30e3 }).then((res) => res.json()); } export async function ipfsGet( @@ -193,7 +194,7 @@ export async function ipfsGet( protocolType = 'ipfs' ) { const url = `https://${gateway}/${protocolType}/${ipfsHash}`; - return fetch(url).then((res) => res.json()); + return fetch(url, { timeout: 20e3 }).then((res) => res.json()); } export async function sendTransaction( @@ -235,6 +236,7 @@ export async function getScores( const res = await fetch(scoreApiUrl, { method: 'POST', headers: scoreApiHeaders, + timeout: 60e3, body: JSON.stringify({ params }) }); const obj = await res.json(); @@ -268,6 +270,7 @@ export async function getVp( const init = { method: 'POST', headers: scoreApiHeaders, + timeout: 60e3, body: JSON.stringify({ jsonrpc: '2.0', method: 'get_vp', @@ -309,6 +312,7 @@ export async function validate( const init = { method: 'POST', headers: scoreApiHeaders, + timeout: 30e3, body: JSON.stringify({ jsonrpc: '2.0', method: 'validate', From 60faabe4b5d6110ff02a16c4db0658421f4521c1 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Thu, 21 Sep 2023 23:50:03 +0900 Subject: [PATCH 03/25] chore: fix tests --- src/utils.ts | 101 +++++++++++------------ test/e2e/utils.spec.ts | 125 +++++++++++++++++++++++++++++ test/e2e/utils/blockfinder.spec.ts | 28 +++++++ test/e2e/utils/getScores.spec.ts | 30 ------- 4 files changed, 201 insertions(+), 83 deletions(-) create mode 100644 test/e2e/utils.spec.ts delete mode 100644 test/e2e/utils/getScores.spec.ts diff --git a/src/utils.ts b/src/utils.ts index e87a47e98..a08b1b1df 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -136,33 +136,28 @@ export async function multicall( } export async function subgraphRequest(url: string, query, options: any = {}) { - const res = await fetch(url, { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - ...options?.headers - }, - timeout: 20e3, - body: JSON.stringify({ query: jsonToGraphQLQuery({ query }) }) - }); - let responseData: any = await res.text(); try { - responseData = JSON.parse(responseData); + const init = { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...options?.headers + }, + timeout: 20e3, + body: { query: jsonToGraphQLQuery({ query }) } + }; + + return (await fetch(url, init)).data; } catch (e) { - throw new Error( - `Errors found in subgraphRequest: URL: ${url}, Status: ${res.status}, Response: ${responseData}` - ); - } - if (responseData.errors) { - throw new Error( - `Errors found in subgraphRequest: URL: ${url}, Status: ${ - res.status - }, Response: ${JSON.stringify(responseData.errors)}` + return Promise.reject( + e.data?.error || { + code: e.status || 0, + message: e.statusText || e.toString(), + data: e.data || '' + } ); } - const { data } = responseData; - return data || {}; } export function getUrl(uri, gateway = gateways[0]) { @@ -233,26 +228,25 @@ export async function getScores( strategies, addresses }; + const res = await fetch(scoreApiUrl, { method: 'POST', headers: scoreApiHeaders, timeout: 60e3, body: JSON.stringify({ params }) }); - const obj = await res.json(); - - if (obj.error) { - return Promise.reject(obj.error); - } return options.returnValue === 'all' - ? obj.result - : obj.result[options.returnValue || 'scores']; + ? res.result + : res.result[options.returnValue || 'scores']; } catch (e) { - if (e.errno) { - return Promise.reject({ code: e.errno, message: e.toString(), data: '' }); - } - return Promise.reject(e); + return Promise.reject( + e.data?.error || { + code: e.status || 0, + message: e.statusText || e.toString(), + data: e.data || '' + } + ); } } @@ -271,7 +265,7 @@ export async function getVp( method: 'POST', headers: scoreApiHeaders, timeout: 60e3, - body: JSON.stringify({ + body: { jsonrpc: '2.0', method: 'get_vp', params: { @@ -282,19 +276,19 @@ export async function getVp( space, delegation } - }) + } }; try { - const res = await fetch(options.url, init); - const json = await res.json(); - if (json.error) return Promise.reject(json.error); - if (json.result) return json.result; + return (await fetch(options.url, init)).result; } catch (e) { - if (e.errno) { - return Promise.reject({ code: e.errno, message: e.toString(), data: '' }); - } - return Promise.reject(e); + return Promise.reject( + e.data?.error || { + code: e.status || 0, + message: e.statusText || e.toString(), + data: e.data || '' + } + ); } } @@ -313,7 +307,7 @@ export async function validate( method: 'POST', headers: scoreApiHeaders, timeout: 30e3, - body: JSON.stringify({ + body: { jsonrpc: '2.0', method: 'validate', params: { @@ -324,19 +318,20 @@ export async function validate( snapshot, params } - }) + } }; try { const res = await fetch(options.url, init); - const json = await res.json(); - if (json.error) return Promise.reject(json.error); - return json.result; + return res.result; } catch (e) { - if (e.errno) { - return Promise.reject({ code: e.errno, message: e.toString(), data: '' }); - } - return Promise.reject(e); + return Promise.reject( + e.data?.error || { + code: e.status || 0, + message: e.statusText || e.toString(), + data: e.data || '' + } + ); } } diff --git a/test/e2e/utils.spec.ts b/test/e2e/utils.spec.ts new file mode 100644 index 000000000..050fc54e9 --- /dev/null +++ b/test/e2e/utils.spec.ts @@ -0,0 +1,125 @@ +import { test, expect, describe } from 'vitest'; +import { getScores, getVp, validate } from '../../src/utils'; + +describe('test getScores', () => { + test('should return a promise rejection on error from score-api', async () => { + expect.assertions(1); + await expect(getScores('test.eth', [], '1', ['0x0'])).to.rejects.toEqual({ + code: 500, + message: 'unauthorized', + data: 'something wrong with the strategies' + }); + }); + + test('should return a promise rejection with JSON-RPC format on network error (no response)', async () => { + expect.assertions(1); + await expect( + getScores( + 'test.eth', + [], + '1', + [''], + 'latest', + 'https://score-null.snapshot.org' + ) + ).to.rejects.toEqual({ + code: 0, + message: + 'FetchError: [POST] "https://score-null.snapshot.org/api/scores": request to https://score-null.snapshot.org/api/scores failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', + data: '' + }); + }); + + test('should return a promise rejection with JSON-RPC format on network error (not found)', async () => { + expect.assertions(1); + await expect( + getScores('test.eth', [], '1', [''], 'latest', 'https://google.com') + ).to.rejects.toEqual( + expect.objectContaining({ + code: 404 + }) + ); + }); +}); + +describe('test getVp', () => { + const defaultOptions = ['0x0', '1', [], 'latest', 'test.eth', false]; + + test('should return a promise rejection on error from score-api', async () => { + expect.assertions(1); + await expect(getVp(...defaultOptions)).to.rejects.toEqual({ + code: 400, + message: 'unauthorized', + data: 'invalid address' + }); + }); + + test('should return a promise rejection with JSON-RPC format on network error (no response)', async () => { + expect.assertions(1); + await expect( + getVp(...defaultOptions, { + url: 'https://score-null.snapshot.org' + }) + ).to.rejects.toEqual({ + code: 0, + message: + 'FetchError: [POST] "https://score-null.snapshot.org": request to https://score-null.snapshot.org/ failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', + data: '' + }); + }); + + test('should return a promise rejection with JSON-RPC format on network error (not found)', async () => { + expect.assertions(1); + await expect( + getVp(...defaultOptions, { + url: 'https://google.com' + }) + ).to.rejects.toEqual( + expect.objectContaining({ + code: 405, + message: 'Method Not Allowed' + }) + ); + }); +}); + +describe('test validate', () => { + const defaultOptions = ['', '', 'test.eth', '1', 'latest', {}]; + + test('should return a promise rejection on error from score-api', async () => { + expect.assertions(1); + await expect(validate(...defaultOptions)).to.rejects.toEqual({ + code: 400, + message: 'unauthorized', + data: 'invalid address' + }); + }); + + test('should return a promise rejection with JSON-RPC format on network error (no response)', async () => { + expect.assertions(1); + await expect( + validate(...defaultOptions, { + url: 'https://score-null.snapshot.org' + }) + ).to.rejects.toEqual({ + code: 0, + message: + 'FetchError: [POST] "https://score-null.snapshot.org": request to https://score-null.snapshot.org/ failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', + data: '' + }); + }); + + test('should return a promise rejection with JSON-RPC format on network error (not found)', async () => { + expect.assertions(1); + await expect( + validate(...defaultOptions, { + url: 'https://google.com' + }) + ).to.rejects.toEqual( + expect.objectContaining({ + code: 405, + message: 'Method Not Allowed' + }) + ); + }); +}); diff --git a/test/e2e/utils/blockfinder.spec.ts b/test/e2e/utils/blockfinder.spec.ts index 01396525c..3efdc45da 100644 --- a/test/e2e/utils/blockfinder.spec.ts +++ b/test/e2e/utils/blockfinder.spec.ts @@ -4,6 +4,7 @@ import getProvider from '../../../src/utils/provider'; describe('Test block finder', () => { const provider = getProvider('1'); + test('getSnapshots should work without errors and return object', async () => { expect( await getSnapshots('1', 17789783, provider, ['5', '137']) @@ -13,6 +14,7 @@ describe('Test block finder', () => { '5': 9421169 }); }); + test('getSnapshots should return all latest if snapshot is latest', async () => { expect( await getSnapshots('1', 'latest', provider, ['5', '137']) @@ -21,4 +23,30 @@ describe('Test block finder', () => { '5': 'latest' }); }); + + test('getSnapshots should throw on network error', async () => { + await expect( + getSnapshots('1', 17780783, provider, ['5', '137'], { + blockFinderUrl: 'http://localhost:12345' + }) + ).to.rejects.toEqual({ + code: 0, + message: + 'FetchError: [POST] "http://localhost:12345": request to http://localhost:12345/ failed, reason: connect ECONNREFUSED 127.0.0.1:12345', + data: '' + }); + }); + + test('getSnapshots should throw on subgraph error', async () => { + await expect( + getSnapshots('1', 17780783, provider, ['1234455'], { + blockFinderUrl: 'http://google.com' + }) + ).to.rejects.toEqual( + expect.objectContaining({ + code: 405, + message: 'Method Not Allowed' + }) + ); + }); }); diff --git a/test/e2e/utils/getScores.spec.ts b/test/e2e/utils/getScores.spec.ts deleted file mode 100644 index 183280536..000000000 --- a/test/e2e/utils/getScores.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { test, expect, describe } from 'vitest'; -import { getScores } from '../../../src/utils'; - -describe('test getScores', () => { - test('getScores should return a promise rejection on error from score-api', async () => { - expect.assertions(1); - await expect( - getScores('test.eth', [], '1', ['0x0']) - ).to.rejects.toHaveProperty('code'); - }); - - test('getScores should return a promise rejection with JSON-RPC format on network error', async () => { - expect.assertions(1); - await expect( - getScores( - 'test.eth', - [], - '1', - [''], - 'latest', - 'https://score-null.snapshot.org' - ) - ).to.rejects.toEqual({ - code: 'ENOTFOUND', - message: - 'FetchError: request to https://score-null.snapshot.org/api/scores failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', - data: '' - }); - }); -}); From 170b932580be21438c60ce4b3d6cc6300ad76e02 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Fri, 22 Sep 2023 00:03:33 +0900 Subject: [PATCH 04/25] chore: add more tests --- src/utils.ts | 3 +- test/e2e/utils.spec.ts | 66 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index a08b1b1df..3b6edf184 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -322,8 +322,7 @@ export async function validate( }; try { - const res = await fetch(options.url, init); - return res.result; + return (await fetch(options.url, init)).result; } catch (e) { return Promise.reject( e.data?.error || { diff --git a/test/e2e/utils.spec.ts b/test/e2e/utils.spec.ts index 050fc54e9..16f93cec8 100644 --- a/test/e2e/utils.spec.ts +++ b/test/e2e/utils.spec.ts @@ -1,7 +1,7 @@ import { test, expect, describe } from 'vitest'; import { getScores, getVp, validate } from '../../src/utils'; -describe('test getScores', () => { +describe('getScores', () => { test('should return a promise rejection on error from score-api', async () => { expect.assertions(1); await expect(getScores('test.eth', [], '1', ['0x0'])).to.rejects.toEqual({ @@ -42,12 +42,41 @@ describe('test getScores', () => { }); }); -describe('test getVp', () => { - const defaultOptions = ['0x0', '1', [], 'latest', 'test.eth', false]; +describe('getVp', () => { + const address = '0xeF8305E140ac520225DAf050e2f71d5fBcC543e7'; + const network = '1'; + const strategies = [ + { + name: 'eth-balance', + network: '1', + params: {} + }, + { + name: 'eth-balance', + network: '10', + params: {} + } + ]; + const s = 15109700; + const space = 'fabien.eth'; + const delegation = false; + + const defaultOptions = [address, network, strategies, s, space, delegation]; + + test('should return a voting power', async () => { + expect.assertions(1); + expect(await getVp(...defaultOptions)).toEqual({ + vp: 10.49214268914954, + vp_by_strategy: [10.443718706159482, 0.04842398299005922], + vp_state: 'final' + }); + }); test('should return a promise rejection on error from score-api', async () => { expect.assertions(1); - await expect(getVp(...defaultOptions)).to.rejects.toEqual({ + await expect( + getVp('test', network, strategies, s, space, delegation) + ).to.rejects.toEqual({ code: 400, message: 'unauthorized', data: 'invalid address' @@ -83,12 +112,35 @@ describe('test getVp', () => { }); }); -describe('test validate', () => { - const defaultOptions = ['', '', 'test.eth', '1', 'latest', {}]; +describe('validate', () => { + const validation = 'basic'; + const author = '0xeF8305E140ac520225DAf050e2f71d5fBcC543e7'; + const space = 'fabien.eth'; + const network = '1'; + const params = { + minScore: 0.9, + strategies: [ + { + name: 'eth-balance', + params: {} + } + ] + }; + + const defaultOptions = [validation, author, space, network, 'latest', params]; + + test('should return a boolean', async () => { + expect.assertions(1); + expect( + await validate(validation, author, space, network, 'latest', params) + ).toEqual(false); + }); test('should return a promise rejection on error from score-api', async () => { expect.assertions(1); - await expect(validate(...defaultOptions)).to.rejects.toEqual({ + await expect( + validate(validation, 'test', space, network, 'latest', params) + ).to.rejects.toEqual({ code: 400, message: 'unauthorized', data: 'invalid address' From b2121689dc3fa3ef0c23057e59bf0ac44e036877 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Fri, 22 Sep 2023 00:51:27 +0900 Subject: [PATCH 05/25] chore: add more test --- test/e2e/utils.spec.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/e2e/utils.spec.ts b/test/e2e/utils.spec.ts index 16f93cec8..24d000d85 100644 --- a/test/e2e/utils.spec.ts +++ b/test/e2e/utils.spec.ts @@ -2,6 +2,28 @@ import { test, expect, describe } from 'vitest'; import { getScores, getVp, validate } from '../../src/utils'; describe('getScores', () => { + test('should return a score', async () => { + expect.assertions(1); + expect( + await getScores( + 'fabien.eth', + [ + { + name: 'eth-balance', + network: '1', + params: {} + } + ], + '1', + ['0xeF8305E140ac520225DAf050e2f71d5fBcC543e7'] + ) + ).toEqual([ + { + '0xeF8305E140ac520225DAf050e2f71d5fBcC543e7': 0.041582733391515345 + } + ]); + }); + test('should return a promise rejection on error from score-api', async () => { expect.assertions(1); await expect(getScores('test.eth', [], '1', ['0x0'])).to.rejects.toEqual({ From 3dc29cbc3b488a72d3c920daec4a791ed240b5ec Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Fri, 22 Sep 2023 17:35:09 +0900 Subject: [PATCH 06/25] chore: remove uneeded JSON.stringify --- src/sign/index.ts | 2 +- src/utils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/index.ts b/src/sign/index.ts index 8a92088c4..df6555542 100644 --- a/src/sign/index.ts +++ b/src/sign/index.ts @@ -88,7 +88,7 @@ export default class Client { 'Content-Type': 'application/json' }, timeout: 20e3, - body: JSON.stringify(envelop) + body: envelop }; return new Promise((resolve, reject) => { fetch(address, init) diff --git a/src/utils.ts b/src/utils.ts index 3b6edf184..f7a4b8e21 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -233,7 +233,7 @@ export async function getScores( method: 'POST', headers: scoreApiHeaders, timeout: 60e3, - body: JSON.stringify({ params }) + body: { params } }); return options.returnValue === 'all' From 77a6c2fc9ae34c4eb34408f2e7ddf0dbc4d655b5 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Fri, 22 Sep 2023 18:02:17 +0900 Subject: [PATCH 07/25] chore: split long line --- src/utils.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index f7a4b8e21..caf6ae078 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -148,7 +148,8 @@ export async function subgraphRequest(url: string, query, options: any = {}) { body: { query: jsonToGraphQLQuery({ query }) } }; - return (await fetch(url, init)).data; + const body = await fetch(url, init); + return body.data; } catch (e) { return Promise.reject( e.data?.error || { @@ -229,7 +230,7 @@ export async function getScores( addresses }; - const res = await fetch(scoreApiUrl, { + const body = await fetch(scoreApiUrl, { method: 'POST', headers: scoreApiHeaders, timeout: 60e3, @@ -237,8 +238,8 @@ export async function getScores( }); return options.returnValue === 'all' - ? res.result - : res.result[options.returnValue || 'scores']; + ? body.result + : body.result[options.returnValue || 'scores']; } catch (e) { return Promise.reject( e.data?.error || { @@ -280,7 +281,8 @@ export async function getVp( }; try { - return (await fetch(options.url, init)).result; + const body = await fetch(options.url, init); + return body.result; } catch (e) { return Promise.reject( e.data?.error || { @@ -322,7 +324,8 @@ export async function validate( }; try { - return (await fetch(options.url, init)).result; + const body = await fetch(options.url, init); + return body.result; } catch (e) { return Promise.reject( e.data?.error || { From cfd524671ebcf49c1155dfc0e01ebf11125c5414 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Fri, 22 Sep 2023 19:29:08 +0900 Subject: [PATCH 08/25] fix: handle errors when parsing json response --- src/utils.ts | 19 +++++++++++-- test/e2e/utils.spec.ts | 63 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index caf6ae078..ae175142b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -181,7 +181,15 @@ export function getUrl(uri, gateway = gateways[0]) { export async function getJSON(uri, options: any = {}) { const url = getUrl(uri, options.gateways); - return fetch(url, { timeout: 30e3 }).then((res) => res.json()); + return fetch(url, { + timeout: 30e3, + parseResponse: JSON.parse, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + ...options?.headers + } + }); } export async function ipfsGet( @@ -190,7 +198,14 @@ export async function ipfsGet( protocolType = 'ipfs' ) { const url = `https://${gateway}/${protocolType}/${ipfsHash}`; - return fetch(url, { timeout: 20e3 }).then((res) => res.json()); + return fetch(url, { + timeout: 20e3, + parseResponse: JSON.parse, + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + }); } export async function sendTransaction( diff --git a/test/e2e/utils.spec.ts b/test/e2e/utils.spec.ts index 24d000d85..91be46935 100644 --- a/test/e2e/utils.spec.ts +++ b/test/e2e/utils.spec.ts @@ -1,5 +1,5 @@ import { test, expect, describe } from 'vitest'; -import { getScores, getVp, validate } from '../../src/utils'; +import { getScores, getVp, validate, getJSON, ipfsGet } from '../../src/utils'; describe('getScores', () => { test('should return a score', async () => { @@ -22,7 +22,7 @@ describe('getScores', () => { '0xeF8305E140ac520225DAf050e2f71d5fBcC543e7': 0.041582733391515345 } ]); - }); + }, 20e3); test('should return a promise rejection on error from score-api', async () => { expect.assertions(1); @@ -55,7 +55,7 @@ describe('getScores', () => { test('should return a promise rejection with JSON-RPC format on network error (not found)', async () => { expect.assertions(1); await expect( - getScores('test.eth', [], '1', [''], 'latest', 'https://google.com') + getScores('test.eth', [], '1', [''], 'latest', 'https://hub.snapshot.org') ).to.rejects.toEqual( expect.objectContaining({ code: 404 @@ -123,7 +123,7 @@ describe('getVp', () => { expect.assertions(1); await expect( getVp(...defaultOptions, { - url: 'https://google.com' + url: 'https://httpstat.us/405' }) ).to.rejects.toEqual( expect.objectContaining({ @@ -187,7 +187,7 @@ describe('validate', () => { expect.assertions(1); await expect( validate(...defaultOptions, { - url: 'https://google.com' + url: 'https://httpstat.us/405' }) ).to.rejects.toEqual( expect.objectContaining({ @@ -197,3 +197,56 @@ describe('validate', () => { ); }); }); + +describe('getJSON', () => { + test('should return a JSON', async () => { + expect.assertions(1); + expect(await getJSON('https://hub.snapshot.org')).toEqual( + expect.objectContaining({ name: 'snapshot-hub' }) + ); + }); + + test('should throw an error when the response is not a JSON file', async () => { + expect.assertions(1); + await expect(() => getJSON('https://snapshot.org')).rejects.toThrowError( + /Unexpected.*JSON/ + ); + }); + + test('should throw an error on network error (no response)', async () => { + expect.assertions(1); + await expect(() => + getJSON('https://score-null.snapshot.org') + ).rejects.toThrowError('ENOTFOUND'); + }); + + test('should throw an error on network error (not found)', async () => { + expect.assertions(1); + await expect(() => getJSON('https://httpstat.us/405')).rejects.toThrowError( + '405 Method Not Allowed' + ); + }); +}); + +describe('ipfsGet', () => { + const cid = 'bafkreibatgmdqdxsair3j52zfhtntegshtypq2qbex3fgtorwx34kzippe'; + + test('should return a JSON', async () => { + expect.assertions(1); + expect(await ipfsGet('pineapple.fyi', cid)).toEqual({ name: 'Vitalik' }); + }); + + test('should throw an error on network error (no response)', async () => { + expect.assertions(1); + await expect(() => + ipfsGet('score-null.snapshot.org', cid) + ).rejects.toThrowError('ENOTFOUND'); + }); + + test('should throw an error on network error (not found)', async () => { + expect.assertions(1); + await expect(() => ipfsGet('httpstat.us/404', cid)).rejects.toThrowError( + '404 Not Found' + ); + }); +}); From fc18bab062812d901575fda52452405b732d4d5c Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Sat, 23 Sep 2023 00:59:28 +0900 Subject: [PATCH 09/25] fix: migrate fetch request to ofetch API --- src/sign/index.ts | 17 +++++++++-------- test/e2e/sign/index.spec.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 test/e2e/sign/index.spec.ts diff --git a/src/sign/index.ts b/src/sign/index.ts index df6555542..a51763918 100644 --- a/src/sign/index.ts +++ b/src/sign/index.ts @@ -90,14 +90,15 @@ export default class Client { timeout: 20e3, body: envelop }; - return new Promise((resolve, reject) => { - fetch(address, init) - .then((res) => { - if (res.ok) return resolve(res.json()); - throw res; - }) - .catch((e) => e.json().then((json) => reject(json))); - }); + + try { + return await fetch(address, init); + } catch (e) { + const isSequencerError = + e.data?.hasOwnProperty('error') && + e.data?.hasOwnProperty('error_description'); + return Promise.reject(isSequencerError ? e.data : e); + } } async space(web3: Web3Provider | Wallet, address: string, message: Space) { diff --git a/test/e2e/sign/index.spec.ts b/test/e2e/sign/index.spec.ts new file mode 100644 index 000000000..c167df764 --- /dev/null +++ b/test/e2e/sign/index.spec.ts @@ -0,0 +1,31 @@ +import { describe, test, expect } from 'vitest'; +import Client from '../../../src/sign/'; + +describe('Client', () => { + describe('send()', () => { + describe('on error', () => { + const payload = { address: '', sig: '', data: '' }; + + test('should return the error from sequencer as a Promise rejection', async () => { + expect.assertions(1); + const client = new Client(); + await expect(client.send(payload)).to.rejects.toEqual({ + error: 'client_error', + error_description: 'wrong envelope format' + }); + }); + + test.each([ + ['ENOTFOUND', 'https://unknown.snapshot.org'], + ['404 Not Found', 'https://httpstat.us/404'] + ])( + 'should return the network error (%s) as Promise rejection', + async (code, url) => { + expect.assertions(1); + const client = new Client(url); + await expect(() => client.send(payload)).rejects.toThrowError(code); + } + ); + }); + }); +}); From 836dddf34d7e9b1bbd55ce3d96d4a8670fcb155d Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Mon, 25 Sep 2023 23:59:54 +0900 Subject: [PATCH 10/25] chore: bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 62bbdced9..cdea6b159 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@snapshot-labs/snapshot.js", - "version": "0.6.2", + "version": "1.0.0-beta.0", "repository": "snapshot-labs/snapshot.js", "license": "MIT", "main": "dist/snapshot.cjs.js", From bc6403256c986333a89467b4af546926eda55cf2 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Wed, 27 Sep 2023 20:58:15 +0900 Subject: [PATCH 11/25] chore: add more todo test --- test/e2e/sign/index.spec.ts | 18 ++++++++++++++++++ test/e2e/utils.spec.ts | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/test/e2e/sign/index.spec.ts b/test/e2e/sign/index.spec.ts index c167df764..c7e7bea88 100644 --- a/test/e2e/sign/index.spec.ts +++ b/test/e2e/sign/index.spec.ts @@ -27,5 +27,23 @@ describe('Client', () => { } ); }); + + it('initializes correctly with given parameters', () => {}); + + describe('Address Assignment', () => { + it('assigns address to this.address when envelop.sig is not "0x" or relayerURL is not provided', () => {}); + it('assigns address to this.options.relayerURL when envelop.sig is "0x" and relayerURL is provided', () => {}); + }); + + describe('Fetch Call', () => { + it('calls fetch with correct address and init parameters', () => {}); + it('returns correctly when fetch call is successful', () => {}); + it('throws an error when fetch call fails', () => {}); + }); + + describe('Error Handling', () => { + it('throws sequencer error when error object has both error and error_description properties', () => {}); + it('throws non-sequencer error when error object does not have both error and error_description properties', () => {}); + }); }); }); diff --git a/test/e2e/utils.spec.ts b/test/e2e/utils.spec.ts index 91be46935..601af66ec 100644 --- a/test/e2e/utils.spec.ts +++ b/test/e2e/utils.spec.ts @@ -50,6 +50,10 @@ describe('getScores', () => { 'FetchError: [POST] "https://score-null.snapshot.org/api/scores": request to https://score-null.snapshot.org/api/scores failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', data: '' }); + + it('should handle missing headers in options', () => {}); + it('should handle additional headers provided in options', () => {}); + it('should properly handle timeout', () => {}); }); test('should return a promise rejection with JSON-RPC format on network error (not found)', async () => { @@ -132,6 +136,10 @@ describe('getVp', () => { }) ); }); + + it('should handle invalid network parameter', () => {}); + it('should handle missing headers in options', () => {}); + it('should handle additional headers provided in options', () => {}); }); describe('validate', () => { From 137ffa1a8fb074c92dc93f28b969649ee96858c8 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Wed, 27 Sep 2023 20:59:59 +0900 Subject: [PATCH 12/25] chore: rename test function --- test/e2e/sign/index.spec.ts | 31 +++++++++++++++++++++++-------- test/e2e/utils.spec.ts | 12 ++++++------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/test/e2e/sign/index.spec.ts b/test/e2e/sign/index.spec.ts index c7e7bea88..f5329697f 100644 --- a/test/e2e/sign/index.spec.ts +++ b/test/e2e/sign/index.spec.ts @@ -28,22 +28,37 @@ describe('Client', () => { ); }); - it('initializes correctly with given parameters', () => {}); + test.todo('initializes correctly with given parameters', () => {}); describe('Address Assignment', () => { - it('assigns address to this.address when envelop.sig is not "0x" or relayerURL is not provided', () => {}); - it('assigns address to this.options.relayerURL when envelop.sig is "0x" and relayerURL is provided', () => {}); + test.todo( + 'assigns address to this.address when envelop.sig is not "0x" or relayerURL is not provided', + () => {} + ); + test.todo( + 'assigns address to this.options.relayerURL when envelop.sig is "0x" and relayerURL is provided', + () => {} + ); }); describe('Fetch Call', () => { - it('calls fetch with correct address and init parameters', () => {}); - it('returns correctly when fetch call is successful', () => {}); - it('throws an error when fetch call fails', () => {}); + test.todo( + 'calls fetch with correct address and init parameters', + () => {} + ); + test.todo('returns correctly when fetch call is successful', () => {}); + test.todo('throws an error when fetch call fails', () => {}); }); describe('Error Handling', () => { - it('throws sequencer error when error object has both error and error_description properties', () => {}); - it('throws non-sequencer error when error object does not have both error and error_description properties', () => {}); + test.todo( + 'throws sequencer error when error object has both error and error_description properties', + () => {} + ); + test.todo( + 'throws non-sequencer error when error object does not have both error and error_description properties', + () => {} + ); }); }); }); diff --git a/test/e2e/utils.spec.ts b/test/e2e/utils.spec.ts index 601af66ec..67554edf5 100644 --- a/test/e2e/utils.spec.ts +++ b/test/e2e/utils.spec.ts @@ -51,9 +51,9 @@ describe('getScores', () => { data: '' }); - it('should handle missing headers in options', () => {}); - it('should handle additional headers provided in options', () => {}); - it('should properly handle timeout', () => {}); + test.todo('should handle missing headers in options', () => {}); + test.todo('should handle additional headers provided in options', () => {}); + test.todo('should properly handle timeout', () => {}); }); test('should return a promise rejection with JSON-RPC format on network error (not found)', async () => { @@ -137,9 +137,9 @@ describe('getVp', () => { ); }); - it('should handle invalid network parameter', () => {}); - it('should handle missing headers in options', () => {}); - it('should handle additional headers provided in options', () => {}); + test.todo('should handle invalid network parameter', () => {}); + test.todo('should handle missing headers in options', () => {}); + test.todo('should handle additional headers provided in options', () => {}); }); describe('validate', () => { From 2cb0b93e6c021582a61805c23c047b25cb8f4909 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Wed, 27 Sep 2023 22:50:29 +0900 Subject: [PATCH 13/25] chore: use .js extension for test files --- test/e2e/sign/{index.spec.ts => index.spec.js} | 0 test/e2e/{utils.spec.ts => utils.spec.js} | 0 test/e2e/utils/{blockfinder.spec.ts => blockfinder.spec.js} | 0 test/{schema.spec.ts => schema.spec.js} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename test/e2e/sign/{index.spec.ts => index.spec.js} (100%) rename test/e2e/{utils.spec.ts => utils.spec.js} (100%) rename test/e2e/utils/{blockfinder.spec.ts => blockfinder.spec.js} (100%) rename test/{schema.spec.ts => schema.spec.js} (100%) diff --git a/test/e2e/sign/index.spec.ts b/test/e2e/sign/index.spec.js similarity index 100% rename from test/e2e/sign/index.spec.ts rename to test/e2e/sign/index.spec.js diff --git a/test/e2e/utils.spec.ts b/test/e2e/utils.spec.js similarity index 100% rename from test/e2e/utils.spec.ts rename to test/e2e/utils.spec.js diff --git a/test/e2e/utils/blockfinder.spec.ts b/test/e2e/utils/blockfinder.spec.js similarity index 100% rename from test/e2e/utils/blockfinder.spec.ts rename to test/e2e/utils/blockfinder.spec.js diff --git a/test/schema.spec.ts b/test/schema.spec.js similarity index 100% rename from test/schema.spec.ts rename to test/schema.spec.js From 925f2b84fde80410449015450055140fdc8168cb Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Thu, 28 Sep 2023 04:08:08 +0900 Subject: [PATCH 14/25] chore: improve/add more tests --- src/utils.ts | 81 +++-- test/e2e/utils.spec.js | 492 +++++++++++++++++++---------- test/e2e/utils/blockfinder.spec.js | 116 ++++--- 3 files changed, 444 insertions(+), 245 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index ae175142b..036f81f7b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -19,6 +19,8 @@ import voting from './voting'; interface Options { url?: string; + timeout?: number; + headers?: any; } interface Strategy { @@ -135,7 +137,7 @@ export async function multicall( } } -export async function subgraphRequest(url: string, query, options: any = {}) { +export async function subgraphRequest(url: string, query, options?: Options) { try { const init = { method: 'POST', @@ -144,24 +146,36 @@ export async function subgraphRequest(url: string, query, options: any = {}) { 'Content-Type': 'application/json', ...options?.headers }, - timeout: 20e3, + timeout: options?.timeout || 20e3, body: { query: jsonToGraphQLQuery({ query }) } }; const body = await fetch(url, init); + + if (body.errors) { + return Promise.reject(body); + } + return body.data; } catch (e) { return Promise.reject( - e.data?.error || { - code: e.status || 0, - message: e.statusText || e.toString(), - data: e.data || '' - } + e.data?.errors + ? e.data + : { + errors: [ + { + message: e.statusText || e.toString(), + extensions: { + code: e.status || 0 + } + } + ] + } ); } } -export function getUrl(uri, gateway = gateways[0]) { +export function getUrl(uri: string, gateway = gateways[0]) { const ipfsGateway = `https://${gateway}`; if (!uri) return null; if ( @@ -179,33 +193,40 @@ export function getUrl(uri, gateway = gateways[0]) { return uri; } -export async function getJSON(uri, options: any = {}) { - const url = getUrl(uri, options.gateways); - return fetch(url, { - timeout: 30e3, - parseResponse: JSON.parse, +export async function getJSON( + uri: string, + options: Options & { gateways?: string } = {} +) { + const url = getUrl(uri, options.gateways) || ''; + const body = await fetch(url, { + timeout: options.timeout || 30e3, headers: { Accept: 'application/json', 'Content-Type': 'application/json', ...options?.headers } }); + + return typeof body === 'string' ? JSON.parse(body) : body; } export async function ipfsGet( gateway: string, ipfsHash: string, - protocolType = 'ipfs' + protocolType = 'ipfs', + options: Options = {} ) { const url = `https://${gateway}/${protocolType}/${ipfsHash}`; - return fetch(url, { - timeout: 20e3, - parseResponse: JSON.parse, + const body = await fetch(url, { + timeout: options.timeout || 20e3, headers: { Accept: 'application/json', - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + ...options.headers } }); + + return typeof body === 'string' ? JSON.parse(body) : body; } export async function sendTransaction( @@ -230,10 +251,10 @@ export async function getScores( addresses: string[], snapshot: number | string = 'latest', scoreApiUrl = 'https://score.snapshot.org', - options: any = {} + options: Options & { returnValue?: string; pathname?: string } = {} ) { const url = new URL(scoreApiUrl); - url.pathname = '/api/scores'; + url.pathname = options.pathname || '/api/scores'; scoreApiUrl = url.toString(); try { @@ -248,7 +269,7 @@ export async function getScores( const body = await fetch(scoreApiUrl, { method: 'POST', headers: scoreApiHeaders, - timeout: 60e3, + timeout: options.timeout || 60e3, body: { params } }); @@ -273,14 +294,13 @@ export async function getVp( snapshot: number | 'latest', space: string, delegation: boolean, - options?: Options + options: Options = {} ) { - if (!options) options = {}; - if (!options.url) options.url = 'https://score.snapshot.org'; + const url = options.url || 'https://score.snapshot.org'; const init = { method: 'POST', headers: scoreApiHeaders, - timeout: 60e3, + timeout: options.timeout || 60e3, body: { jsonrpc: '2.0', method: 'get_vp', @@ -296,7 +316,7 @@ export async function getVp( }; try { - const body = await fetch(options.url, init); + const body = await fetch(url, init); return body.result; } catch (e) { return Promise.reject( @@ -316,14 +336,13 @@ export async function validate( network: string, snapshot: number | 'latest', params: any, - options: any + options: Options = {} ) { - if (!options) options = {}; - if (!options.url) options.url = 'https://score.snapshot.org'; + const url = options.url || 'https://score.snapshot.org'; const init = { method: 'POST', headers: scoreApiHeaders, - timeout: 30e3, + timeout: options.timeout || 30e3, body: { jsonrpc: '2.0', method: 'validate', @@ -339,7 +358,7 @@ export async function validate( }; try { - const body = await fetch(options.url, init); + const body = await fetch(url, init); return body.result; } catch (e) { return Promise.reject( diff --git a/test/e2e/utils.spec.js b/test/e2e/utils.spec.js index 67554edf5..3ada28d3a 100644 --- a/test/e2e/utils.spec.js +++ b/test/e2e/utils.spec.js @@ -1,70 +1,116 @@ import { test, expect, describe } from 'vitest'; import { getScores, getVp, validate, getJSON, ipfsGet } from '../../src/utils'; +const SCORE_API_URL = 'https://score.snapshot.org'; + describe('getScores', () => { - test('should return a score', async () => { - expect.assertions(1); - expect( - await getScores( - 'fabien.eth', - [ - { - name: 'eth-balance', - network: '1', - params: {} - } - ], - '1', - ['0xeF8305E140ac520225DAf050e2f71d5fBcC543e7'] - ) - ).toEqual([ + const payload = [ + 'fabien.eth', + [ + { + name: 'eth-balance', + network: '1', + params: {} + } + ], + '1', + ['0xeF8305E140ac520225DAf050e2f71d5fBcC543e7'], + 'latest' + ]; + + describe('on success', () => { + const scoresResponse = [ { '0xeF8305E140ac520225DAf050e2f71d5fBcC543e7': 0.041582733391515345 } - ]); - }, 20e3); + ]; + const stateResponse = 'pending'; - test('should return a promise rejection on error from score-api', async () => { - expect.assertions(1); - await expect(getScores('test.eth', [], '1', ['0x0'])).to.rejects.toEqual({ - code: 500, - message: 'unauthorized', - data: 'something wrong with the strategies' + test('should return only the scores property by default', async () => { + expect.assertions(1); + expect(await getScores(...payload)).toEqual(scoresResponse); }); - }); - test('should return a promise rejection with JSON-RPC format on network error (no response)', async () => { - expect.assertions(1); - await expect( - getScores( - 'test.eth', - [], - '1', - [''], - 'latest', - 'https://score-null.snapshot.org' - ) - ).to.rejects.toEqual({ - code: 0, - message: - 'FetchError: [POST] "https://score-null.snapshot.org/api/scores": request to https://score-null.snapshot.org/api/scores failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', - data: '' - }); - - test.todo('should handle missing headers in options', () => {}); - test.todo('should handle additional headers provided in options', () => {}); - test.todo('should properly handle timeout', () => {}); + test('should return the full scores object', async () => { + expect.assertions(1); + expect( + await getScores(...payload, SCORE_API_URL, { + returnValue: 'all' + }) + ).toEqual({ + scores: scoresResponse, + state: stateResponse + }); + }); + + test('should return only the given field', async () => { + expect.assertions(1); + expect( + await getScores(...payload, SCORE_API_URL, { + returnValue: 'state' + }) + ).toEqual(stateResponse); + }); + + test('should return undefined when the given field does not exist', async () => { + expect.assertions(1); + expect( + await getScores(...payload, SCORE_API_URL, { + returnValue: 'test' + }) + ).toEqual(undefined); + }); }); - test('should return a promise rejection with JSON-RPC format on network error (not found)', async () => { - expect.assertions(1); - await expect( - getScores('test.eth', [], '1', [''], 'latest', 'https://hub.snapshot.org') - ).to.rejects.toEqual( - expect.objectContaining({ - code: 404 - }) - ); + describe('on error', () => { + test('should return the JSON-RPC error from score-api', async () => { + expect.assertions(1); + await expect(getScores('test.eth', [], '1', ['0x0'])).to.rejects.toEqual({ + code: 500, + message: 'unauthorized', + data: 'something wrong with the strategies' + }); + }); + + test('should return a JSON-RPC-like error on network error (no response)', async () => { + expect.assertions(1); + await expect( + getScores(...payload, 'https://score-null.snapshot.org') + ).to.rejects.toEqual({ + code: 0, + message: + 'FetchError: [POST] "https://score-null.snapshot.org/api/scores": request to https://score-null.snapshot.org/api/scores failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', + data: '' + }); + }); + + test('should return a JSON-RPC-like error on network error (not found)', async () => { + expect.assertions(1); + await expect( + getScores(...payload, 'https://httpstat.us', { pathname: '404' }) + ).to.rejects.toEqual( + expect.objectContaining({ + code: 404, + message: 'Not Found' + }) + ); + }); + + test('should return a JSON-RPC-like error on network error (timeout)', async () => { + expect.assertions(1); + await expect( + getScores(...payload, 'https://httpstat.us/?sleep=5000', { + timeout: 500, + pathname: '200' + }) + ).to.rejects.toEqual( + expect.objectContaining({ + code: 0, + message: + 'FetchError: [POST] "https://httpstat.us/200?sleep=5000": The operation was aborted.' + }) + ); + }); }); }); @@ -86,60 +132,75 @@ describe('getVp', () => { const s = 15109700; const space = 'fabien.eth'; const delegation = false; - const defaultOptions = [address, network, strategies, s, space, delegation]; - test('should return a voting power', async () => { - expect.assertions(1); - expect(await getVp(...defaultOptions)).toEqual({ - vp: 10.49214268914954, - vp_by_strategy: [10.443718706159482, 0.04842398299005922], - vp_state: 'final' + describe('on success', () => { + test('should return a voting power', async () => { + expect.assertions(1); + expect(await getVp(...defaultOptions)).toEqual({ + vp: 10.49214268914954, + vp_by_strategy: [10.443718706159482, 0.04842398299005922], + vp_state: 'final' + }); }); }); - test('should return a promise rejection on error from score-api', async () => { - expect.assertions(1); - await expect( - getVp('test', network, strategies, s, space, delegation) - ).to.rejects.toEqual({ - code: 400, - message: 'unauthorized', - data: 'invalid address' + describe('on error', () => { + test('should return a JSON-RPC error from score-api', async () => { + expect.assertions(1); + await expect( + getVp('test', network, strategies, s, space, delegation) + ).to.rejects.toEqual({ + code: 400, + message: 'unauthorized', + data: 'invalid address' + }); }); - }); - test('should return a promise rejection with JSON-RPC format on network error (no response)', async () => { - expect.assertions(1); - await expect( - getVp(...defaultOptions, { - url: 'https://score-null.snapshot.org' - }) - ).to.rejects.toEqual({ - code: 0, - message: - 'FetchError: [POST] "https://score-null.snapshot.org": request to https://score-null.snapshot.org/ failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', - data: '' + test('should return a JSON-RPC-like error on network error (no response)', async () => { + expect.assertions(1); + await expect( + getVp(...defaultOptions, { + url: 'https://score-null.snapshot.org' + }) + ).to.rejects.toEqual({ + code: 0, + message: + 'FetchError: [POST] "https://score-null.snapshot.org": request to https://score-null.snapshot.org/ failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', + data: '' + }); }); - }); - test('should return a promise rejection with JSON-RPC format on network error (not found)', async () => { - expect.assertions(1); - await expect( - getVp(...defaultOptions, { - url: 'https://httpstat.us/405' - }) - ).to.rejects.toEqual( - expect.objectContaining({ - code: 405, - message: 'Method Not Allowed' - }) - ); - }); + test('should return a JSON-RPC-like error on network error (not found)', async () => { + expect.assertions(1); + await expect( + getVp(...defaultOptions, { + url: 'https://httpstat.us/404' + }) + ).to.rejects.toEqual( + expect.objectContaining({ + code: 404, + message: 'Not Found' + }) + ); + }); - test.todo('should handle invalid network parameter', () => {}); - test.todo('should handle missing headers in options', () => {}); - test.todo('should handle additional headers provided in options', () => {}); + test('should return a JSON-RPC-like error on network error (timeout)', async () => { + expect.assertions(1); + await expect( + getVp(...defaultOptions, { + url: 'https://httpstat.us/200?sleep=5000', + timeout: 500 + }) + ).to.rejects.toEqual( + expect.objectContaining({ + code: 0, + message: + 'FetchError: [POST] "https://httpstat.us/200?sleep=5000": The operation was aborted.' + }) + ); + }); + }); }); describe('validate', () => { @@ -156,105 +217,188 @@ describe('validate', () => { } ] }; - const defaultOptions = [validation, author, space, network, 'latest', params]; - test('should return a boolean', async () => { - expect.assertions(1); - expect( - await validate(validation, author, space, network, 'latest', params) - ).toEqual(false); + describe('on success', () => { + test('should return a boolean', async () => { + expect.assertions(1); + expect(await validate(...defaultOptions)).toEqual(false); + }); }); - test('should return a promise rejection on error from score-api', async () => { - expect.assertions(1); - await expect( - validate(validation, 'test', space, network, 'latest', params) - ).to.rejects.toEqual({ - code: 400, - message: 'unauthorized', - data: 'invalid address' + describe('on error', () => { + test('should return the JSON-RPC error from score-api', async () => { + expect.assertions(1); + await expect( + validate(validation, 'test', space, network, 'latest', params) + ).to.rejects.toEqual({ + code: 400, + message: 'unauthorized', + data: 'invalid address' + }); }); - }); - test('should return a promise rejection with JSON-RPC format on network error (no response)', async () => { - expect.assertions(1); - await expect( - validate(...defaultOptions, { - url: 'https://score-null.snapshot.org' - }) - ).to.rejects.toEqual({ - code: 0, - message: - 'FetchError: [POST] "https://score-null.snapshot.org": request to https://score-null.snapshot.org/ failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', - data: '' + test('should return a JSON-RPC-like error on network error (no response)', async () => { + expect.assertions(1); + await expect( + validate(...defaultOptions, { + url: 'https://score-null.snapshot.org' + }) + ).to.rejects.toEqual({ + code: 0, + message: + 'FetchError: [POST] "https://score-null.snapshot.org": request to https://score-null.snapshot.org/ failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', + data: '' + }); + }); + + test('should return a JSON-RPC-like error on network error (not found)', async () => { + expect.assertions(1); + await expect( + validate(...defaultOptions, { + url: 'https://httpstat.us/404' + }) + ).to.rejects.toEqual( + expect.objectContaining({ + code: 404, + message: 'Not Found' + }) + ); }); - }); - test('should return a promise rejection with JSON-RPC format on network error (not found)', async () => { - expect.assertions(1); - await expect( - validate(...defaultOptions, { - url: 'https://httpstat.us/405' - }) - ).to.rejects.toEqual( - expect.objectContaining({ - code: 405, - message: 'Method Not Allowed' - }) - ); + test('should return a JSON-RPC-like error on network error (timeout)', async () => { + expect.assertions(1); + await expect( + validate(...defaultOptions, { + url: 'https://httpstat.us/200?sleep=5000', + timeout: 500 + }) + ).to.rejects.toEqual( + expect.objectContaining({ + code: 0, + message: + 'FetchError: [POST] "https://httpstat.us/200?sleep=5000": The operation was aborted.' + }) + ); + }); }); }); describe('getJSON', () => { - test('should return a JSON', async () => { - expect.assertions(1); - expect(await getJSON('https://hub.snapshot.org')).toEqual( - expect.objectContaining({ name: 'snapshot-hub' }) - ); - }); + describe('on success', () => { + test('should return a JSON object from the specified URL', async () => { + expect.assertions(1); + expect(await getJSON('https://hub.snapshot.org')).toEqual( + expect.objectContaining({ name: 'snapshot-hub' }) + ); + }); - test('should throw an error when the response is not a JSON file', async () => { - expect.assertions(1); - await expect(() => getJSON('https://snapshot.org')).rejects.toThrowError( - /Unexpected.*JSON/ - ); + test('should return a JSON object from the specified CID', async () => { + expect.assertions(1); + expect( + await getJSON( + 'bafkreib5epjzumf3omr7rth5mtcsz4ugcoh3ut4d46hx5xhwm4b3pqr2vi' + ) + ).toEqual(expect.objectContaining({ status: 'OK' })); + }); }); - test('should throw an error on network error (no response)', async () => { - expect.assertions(1); - await expect(() => - getJSON('https://score-null.snapshot.org') - ).rejects.toThrowError('ENOTFOUND'); - }); + describe('on error', () => { + test('should throw an error when the response is not a JSON file', async () => { + expect.assertions(1); + await expect(() => getJSON('https://snapshot.org')).rejects.toThrowError( + /Unexpected.*JSON/ + ); + }); + + test('should throw an error when the url is an empty string', async () => { + expect.assertions(1); + await expect(() => getJSON('')).rejects.toThrowError(/Invalid URL/); + }); + + test('should throw an error when the given argument is not valid CID', async () => { + expect.assertions(1); + await expect(() => getJSON('test-cid')).rejects.toThrowError( + '500 Internal Server Error' + ); + }); + + test('should throw an error when the url is not valid', async () => { + expect.assertions(1); + await expect(() => getJSON('https:// testurl.com')).rejects.toThrowError( + /Invalid URL/ + ); + }); - test('should throw an error on network error (not found)', async () => { - expect.assertions(1); - await expect(() => getJSON('https://httpstat.us/405')).rejects.toThrowError( - '405 Method Not Allowed' - ); + test('should throw an error on network error (no response)', async () => { + expect.assertions(1); + await expect(() => + getJSON('https://score-null.snapshot.org') + ).rejects.toThrowError('ENOTFOUND'); + }); + + test('should throw an error on network error (not found)', async () => { + expect.assertions(1); + await expect(() => + getJSON('https://httpstat.us/404') + ).rejects.toThrowError('404 Not Found'); + }); + + test('should throw an error on network error (timeout)', async () => { + expect.assertions(1); + await expect(() => + getJSON('https://httpstat.us/200?sleep5000', { timeout: 500 }) + ).rejects.toThrowError( + '[GET] "https://httpstat.us/200?sleep5000": The operation was aborted.' + ); + }); }); }); describe('ipfsGet', () => { const cid = 'bafkreibatgmdqdxsair3j52zfhtntegshtypq2qbex3fgtorwx34kzippe'; - test('should return a JSON', async () => { - expect.assertions(1); - expect(await ipfsGet('pineapple.fyi', cid)).toEqual({ name: 'Vitalik' }); + describe('on success', () => { + test('should return a JSON object', async () => { + expect.assertions(1); + expect(await ipfsGet('pineapple.fyi', cid)).toEqual({ name: 'Vitalik' }); + }); }); - test('should throw an error on network error (no response)', async () => { - expect.assertions(1); - await expect(() => - ipfsGet('score-null.snapshot.org', cid) - ).rejects.toThrowError('ENOTFOUND'); - }); + describe('on error', () => { + test('should throw an error when the response is not a JSON file', async () => { + expect.assertions(1); + await expect(() => ipfsGet('snapshot.org', cid)).rejects.toThrowError( + /Unexpected.*JSON/ + ); + }); - test('should throw an error on network error (not found)', async () => { - expect.assertions(1); - await expect(() => ipfsGet('httpstat.us/404', cid)).rejects.toThrowError( - '404 Not Found' - ); + test('should throw an error on network error (no response)', async () => { + expect.assertions(1); + await expect(() => + ipfsGet('score-null.snapshot.org', cid) + ).rejects.toThrowError('ENOTFOUND'); + }); + + test('should throw an error on network error (not found)', async () => { + expect.assertions(1); + await expect(() => ipfsGet('httpstat.us/404', cid)).rejects.toThrowError( + '404 Not Found' + ); + }); + + test('should throw an error on network error (on invalid protocol argument)', async () => { + expect.assertions(1); + await expect(() => + ipfsGet('pineapple.fyi', cid, 'test') + ).rejects.toThrowError('404 Not Found'); + }); + + test('should throw an error on network error (timeout)', async () => { + expect.assertions(1); + await expect(() => + ipfsGet('httpstat.us/200?sleep5000', cid, 'ipfs', { timeout: 500 }) + ).rejects.toThrowError('aborted'); + }); }); }); diff --git a/test/e2e/utils/blockfinder.spec.js b/test/e2e/utils/blockfinder.spec.js index 3efdc45da..c20760767 100644 --- a/test/e2e/utils/blockfinder.spec.js +++ b/test/e2e/utils/blockfinder.spec.js @@ -2,51 +2,87 @@ import { test, expect, describe } from 'vitest'; import { getSnapshots } from '../../../src/utils/blockfinder'; import getProvider from '../../../src/utils/provider'; -describe('Test block finder', () => { +describe('Blockfinder', () => { const provider = getProvider('1'); - test('getSnapshots should work without errors and return object', async () => { - expect( - await getSnapshots('1', 17789783, provider, ['5', '137']) - ).toMatchObject({ - '1': 17789783, - '137': 45609596, - '5': 9421169 - }); - }); + describe('getSnapshot()', () => { + describe('on success', () => { + test('should return a list of blocks per network', async () => { + expect( + await getSnapshots('1', 17789783, provider, ['5', '137']) + ).toMatchObject({ + 1: 17789783, + 137: 45609596, + 5: 9421169 + }); + }); - test('getSnapshots should return all latest if snapshot is latest', async () => { - expect( - await getSnapshots('1', 'latest', provider, ['5', '137']) - ).toMatchObject({ - '137': 'latest', - '5': 'latest' + test('should return all latest if snapshot is latest', async () => { + expect( + await getSnapshots('1', 'latest', provider, ['5', '137']) + ).toMatchObject({ + 137: 'latest', + 5: 'latest' + }); + }); }); - }); - test('getSnapshots should throw on network error', async () => { - await expect( - getSnapshots('1', 17780783, provider, ['5', '137'], { - blockFinderUrl: 'http://localhost:12345' - }) - ).to.rejects.toEqual({ - code: 0, - message: - 'FetchError: [POST] "http://localhost:12345": request to http://localhost:12345/ failed, reason: connect ECONNREFUSED 127.0.0.1:12345', - data: '' - }); - }); + describe('on error', () => { + test('should throw a GraphQL error from blockfinder', async () => { + await expect( + getSnapshots('1', 17780783, provider, ['5', '4', '137']) + ).to.rejects.toEqual({ + errors: [ + { + message: 'invalid network', + locations: [ + { + line: 1, + column: 9 + } + ], + path: ['blocks'], + extensions: { + code: 'INVALID_NETWORK' + } + } + ], + data: { + blocks: null + } + }); + }); + + test('should throw a graphql-like error on network error (invalid hostname)', async () => { + await expect( + getSnapshots('1', 17789785, provider, ['5', '137'], { + blockFinderUrl: 'http://localhost:12345' + }) + ).to.rejects.toEqual({ + errors: [ + { + extensions: { code: 0 }, + message: + 'FetchError: [POST] "http://localhost:12345": request to http://localhost:12345/ failed, reason: connect ECONNREFUSED 127.0.0.1:12345' + } + ] + }); + }); - test('getSnapshots should throw on subgraph error', async () => { - await expect( - getSnapshots('1', 17780783, provider, ['1234455'], { - blockFinderUrl: 'http://google.com' - }) - ).to.rejects.toEqual( - expect.objectContaining({ - code: 405, - message: 'Method Not Allowed' - }) - ); + test('should throw a graphql-like error on network error (not found)', async () => { + await expect( + getSnapshots('1', 17789786, provider, ['5', '137'], { + blockFinderUrl: 'http://google.com' + }) + ).to.rejects.toEqual({ + errors: [ + { + extensions: { code: 405 }, + message: 'Method Not Allowed' + } + ] + }); + }); + }); }); }); From ba1889c990b0b210bf313ea496208909dfc1c889 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Thu, 28 Sep 2023 23:15:15 +0900 Subject: [PATCH 15/25] chore: add more tests --- src/sign/index.ts | 2 +- test/e2e/sign/index.spec.js | 97 +++++++++++++++++++++---------------- 2 files changed, 57 insertions(+), 42 deletions(-) diff --git a/src/sign/index.ts b/src/sign/index.ts index a51763918..aced84ef6 100644 --- a/src/sign/index.ts +++ b/src/sign/index.ts @@ -87,7 +87,7 @@ export default class Client { Accept: 'application/json', 'Content-Type': 'application/json' }, - timeout: 20e3, + timeout: this.options.timeout || 20e3, body: envelop }; diff --git a/test/e2e/sign/index.spec.js b/test/e2e/sign/index.spec.js index f5329697f..d45e66f20 100644 --- a/test/e2e/sign/index.spec.js +++ b/test/e2e/sign/index.spec.js @@ -1,12 +1,53 @@ import { describe, test, expect } from 'vitest'; +import { Wallet } from '@ethersproject/wallet'; import Client from '../../../src/sign/'; describe('Client', () => { describe('send()', () => { + describe('on success', () => { + test('should return a JSON-RPC object', async () => { + expect.assertions(5); + const client = new Client(); + const pk = + 'f5c1c920babf354b83c9ab1634a10ff50a2835715482b36f181319a109c30921'; + const wallet = new Wallet(pk); + const address = wallet.address; + const types = { + Follow: [ + { name: 'from', type: 'address' }, + { name: 'space', type: 'string' } + ] + }; + const domain = { name: 'snapshot', version: '0.1.4' }; + const message = { + from: address, + space: 'fabien.eth', + timestamp: Math.floor(Date.now() / 1000) + }; + const sig = await wallet._signTypedData(domain, types, message); + + const result = await client.send({ + address, + sig, + data: { + domain, + types, + message + } + }); + + expect(result).toHaveProperty('id'); + expect(result).toHaveProperty('ipfs'); + expect(result).toHaveProperty('relayer'); + expect(result).not.toHaveProperty('error'); + expect(result).not.toHaveProperty('error_description'); + }); + }); + describe('on error', () => { const payload = { address: '', sig: '', data: '' }; - test('should return the error from sequencer as a Promise rejection', async () => { + test('should return the JSON-RPC error from sequencer', async () => { expect.assertions(1); const client = new Client(); await expect(client.send(payload)).to.rejects.toEqual({ @@ -18,47 +59,21 @@ describe('Client', () => { test.each([ ['ENOTFOUND', 'https://unknown.snapshot.org'], ['404 Not Found', 'https://httpstat.us/404'] - ])( - 'should return the network error (%s) as Promise rejection', - async (code, url) => { - expect.assertions(1); - const client = new Client(url); - await expect(() => client.send(payload)).rejects.toThrowError(code); - } - ); - }); - - test.todo('initializes correctly with given parameters', () => {}); - - describe('Address Assignment', () => { - test.todo( - 'assigns address to this.address when envelop.sig is not "0x" or relayerURL is not provided', - () => {} - ); - test.todo( - 'assigns address to this.options.relayerURL when envelop.sig is "0x" and relayerURL is provided', - () => {} - ); - }); - - describe('Fetch Call', () => { - test.todo( - 'calls fetch with correct address and init parameters', - () => {} - ); - test.todo('returns correctly when fetch call is successful', () => {}); - test.todo('throws an error when fetch call fails', () => {}); - }); + ])('should throw an error on network error (%s)', async (code, url) => { + expect.assertions(1); + const client = new Client(url); + await expect(() => client.send(payload)).rejects.toThrowError(code); + }); - describe('Error Handling', () => { - test.todo( - 'throws sequencer error when error object has both error and error_description properties', - () => {} - ); - test.todo( - 'throws non-sequencer error when error object does not have both error and error_description properties', - () => {} - ); + test('should throw an error on network error (timeout)', async () => { + expect.assertions(1); + const client = new Client('https://httpstat.us/200?sleep=5000', { + timeout: 500 + }); + await expect(() => client.send(payload)).rejects.toThrowError( + 'aborted' + ); + }); }); }); }); From 1610b3d9e0d906095e077024c44b4d4c5fc6354e Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Thu, 28 Sep 2023 23:57:57 +0900 Subject: [PATCH 16/25] chore: fix tests --- test/e2e/utils.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/utils.spec.js b/test/e2e/utils.spec.js index 3ada28d3a..db9c31e29 100644 --- a/test/e2e/utils.spec.js +++ b/test/e2e/utils.spec.js @@ -347,9 +347,9 @@ describe('getJSON', () => { test('should throw an error on network error (timeout)', async () => { expect.assertions(1); await expect(() => - getJSON('https://httpstat.us/200?sleep5000', { timeout: 500 }) + getJSON('https://httpstat.us/200?sleep=5000', { timeout: 500 }) ).rejects.toThrowError( - '[GET] "https://httpstat.us/200?sleep5000": The operation was aborted.' + '[GET] "https://httpstat.us/200?sleep=5000": The operation was aborted.' ); }); }); @@ -397,7 +397,7 @@ describe('ipfsGet', () => { test('should throw an error on network error (timeout)', async () => { expect.assertions(1); await expect(() => - ipfsGet('httpstat.us/200?sleep5000', cid, 'ipfs', { timeout: 500 }) + ipfsGet('httpstat.us/200?sleep=5000', cid, 'ipfs', { timeout: 500 }) ).rejects.toThrowError('aborted'); }); }); From 305935bda8a7dec9567ec511794cc1a0d96f2dea Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Fri, 29 Sep 2023 00:04:44 +0900 Subject: [PATCH 17/25] chore: fix tests --- test/e2e/utils.spec.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/e2e/utils.spec.js b/test/e2e/utils.spec.js index db9c31e29..4044736d6 100644 --- a/test/e2e/utils.spec.js +++ b/test/e2e/utils.spec.js @@ -397,7 +397,12 @@ describe('ipfsGet', () => { test('should throw an error on network error (timeout)', async () => { expect.assertions(1); await expect(() => - ipfsGet('httpstat.us/200?sleep=5000', cid, 'ipfs', { timeout: 500 }) + ipfsGet( + `httpstat.us/200?sleep=5000&cachebuster=${Date.now()}`, + cid, + 'ipfs', + { timeout: 500 } + ) ).rejects.toThrowError('aborted'); }); }); From bfad8f722fa7371fe2686227ae5a5be6dd9b0059 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Fri, 29 Sep 2023 01:37:19 +0900 Subject: [PATCH 18/25] fix: add more tests --- src/utils.ts | 11 ++ test/e2e/utils.spec.js | 232 +++++++++++++++++++++++++++++++---------- 2 files changed, 190 insertions(+), 53 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 036f81f7b..39c7231f5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -152,6 +152,17 @@ export async function subgraphRequest(url: string, query, options?: Options) { const body = await fetch(url, init); + if (typeof body === 'string') { + return Promise.reject({ + errors: [ + { + message: 'Body is not a JSON object', + extensions: { code: 'INVALID_JSON' } + } + ] + }); + } + if (body.errors) { return Promise.reject(body); } diff --git a/test/e2e/utils.spec.js b/test/e2e/utils.spec.js index 4044736d6..baebe677a 100644 --- a/test/e2e/utils.spec.js +++ b/test/e2e/utils.spec.js @@ -1,5 +1,12 @@ import { test, expect, describe } from 'vitest'; -import { getScores, getVp, validate, getJSON, ipfsGet } from '../../src/utils'; +import { + getScores, + getVp, + validate, + getJSON, + ipfsGet, + subgraphRequest +} from '../../src/utils'; const SCORE_API_URL = 'https://score.snapshot.org'; @@ -63,18 +70,18 @@ describe('getScores', () => { }); describe('on error', () => { - test('should return the JSON-RPC error from score-api', async () => { + test('should return the JSON-RPC error from score-api', () => { expect.assertions(1); - await expect(getScores('test.eth', [], '1', ['0x0'])).to.rejects.toEqual({ + expect(getScores('test.eth', [], '1', ['0x0'])).to.rejects.toEqual({ code: 500, message: 'unauthorized', data: 'something wrong with the strategies' }); }); - test('should return a JSON-RPC-like error on network error (no response)', async () => { + test('should return a JSON-RPC-like error on network error (no response)', () => { expect.assertions(1); - await expect( + expect( getScores(...payload, 'https://score-null.snapshot.org') ).to.rejects.toEqual({ code: 0, @@ -84,9 +91,9 @@ describe('getScores', () => { }); }); - test('should return a JSON-RPC-like error on network error (not found)', async () => { + test('should return a JSON-RPC-like error on network error (not found)', () => { expect.assertions(1); - await expect( + expect( getScores(...payload, 'https://httpstat.us', { pathname: '404' }) ).to.rejects.toEqual( expect.objectContaining({ @@ -96,9 +103,9 @@ describe('getScores', () => { ); }); - test('should return a JSON-RPC-like error on network error (timeout)', async () => { + test('should return a JSON-RPC-like error on network error (timeout)', () => { expect.assertions(1); - await expect( + expect( getScores(...payload, 'https://httpstat.us/?sleep=5000', { timeout: 500, pathname: '200' @@ -146,9 +153,9 @@ describe('getVp', () => { }); describe('on error', () => { - test('should return a JSON-RPC error from score-api', async () => { + test('should return a JSON-RPC error from score-api', () => { expect.assertions(1); - await expect( + expect( getVp('test', network, strategies, s, space, delegation) ).to.rejects.toEqual({ code: 400, @@ -157,9 +164,9 @@ describe('getVp', () => { }); }); - test('should return a JSON-RPC-like error on network error (no response)', async () => { + test('should return a JSON-RPC-like error on network error (no response)', () => { expect.assertions(1); - await expect( + expect( getVp(...defaultOptions, { url: 'https://score-null.snapshot.org' }) @@ -171,9 +178,9 @@ describe('getVp', () => { }); }); - test('should return a JSON-RPC-like error on network error (not found)', async () => { + test('should return a JSON-RPC-like error on network error (not found)', () => { expect.assertions(1); - await expect( + expect( getVp(...defaultOptions, { url: 'https://httpstat.us/404' }) @@ -185,9 +192,9 @@ describe('getVp', () => { ); }); - test('should return a JSON-RPC-like error on network error (timeout)', async () => { + test('should return a JSON-RPC-like error on network error (timeout)', () => { expect.assertions(1); - await expect( + expect( getVp(...defaultOptions, { url: 'https://httpstat.us/200?sleep=5000', timeout: 500 @@ -227,9 +234,9 @@ describe('validate', () => { }); describe('on error', () => { - test('should return the JSON-RPC error from score-api', async () => { + test('should return the JSON-RPC error from score-api', () => { expect.assertions(1); - await expect( + expect( validate(validation, 'test', space, network, 'latest', params) ).to.rejects.toEqual({ code: 400, @@ -238,9 +245,9 @@ describe('validate', () => { }); }); - test('should return a JSON-RPC-like error on network error (no response)', async () => { + test('should return a JSON-RPC-like error on network error (no response)', () => { expect.assertions(1); - await expect( + expect( validate(...defaultOptions, { url: 'https://score-null.snapshot.org' }) @@ -252,9 +259,9 @@ describe('validate', () => { }); }); - test('should return a JSON-RPC-like error on network error (not found)', async () => { + test('should return a JSON-RPC-like error on network error (not found)', () => { expect.assertions(1); - await expect( + expect( validate(...defaultOptions, { url: 'https://httpstat.us/404' }) @@ -266,9 +273,9 @@ describe('validate', () => { ); }); - test('should return a JSON-RPC-like error on network error (timeout)', async () => { + test('should return a JSON-RPC-like error on network error (timeout)', () => { expect.assertions(1); - await expect( + expect( validate(...defaultOptions, { url: 'https://httpstat.us/200?sleep=5000', timeout: 500 @@ -304,49 +311,49 @@ describe('getJSON', () => { }); describe('on error', () => { - test('should throw an error when the response is not a JSON file', async () => { + test('should throw an error when the response is not a JSON file', () => { expect.assertions(1); - await expect(() => getJSON('https://snapshot.org')).rejects.toThrowError( + expect(() => getJSON('https://snapshot.org')).rejects.toThrowError( /Unexpected.*JSON/ ); }); - test('should throw an error when the url is an empty string', async () => { + test('should throw an error when the url is an empty string', () => { expect.assertions(1); - await expect(() => getJSON('')).rejects.toThrowError(/Invalid URL/); + expect(() => getJSON('')).rejects.toThrowError(/Invalid URL/); }); - test('should throw an error when the given argument is not valid CID', async () => { + test('should throw an error when the given argument is not valid CID', () => { expect.assertions(1); - await expect(() => getJSON('test-cid')).rejects.toThrowError( + expect(() => getJSON('test-cid')).rejects.toThrowError( '500 Internal Server Error' ); }); - test('should throw an error when the url is not valid', async () => { + test('should throw an error when the url is not valid', () => { expect.assertions(1); - await expect(() => getJSON('https:// testurl.com')).rejects.toThrowError( + expect(() => getJSON('https:// testurl.com')).rejects.toThrowError( /Invalid URL/ ); }); - test('should throw an error on network error (no response)', async () => { + test('should throw an error on network error (no response)', () => { expect.assertions(1); - await expect(() => + expect(() => getJSON('https://score-null.snapshot.org') ).rejects.toThrowError('ENOTFOUND'); }); - test('should throw an error on network error (not found)', async () => { + test('should throw an error on network error (not found)', () => { expect.assertions(1); - await expect(() => - getJSON('https://httpstat.us/404') - ).rejects.toThrowError('404 Not Found'); + expect(() => getJSON('https://httpstat.us/404')).rejects.toThrowError( + '404 Not Found' + ); }); - test('should throw an error on network error (timeout)', async () => { + test('should throw an error on network error (timeout)', () => { expect.assertions(1); - await expect(() => + expect(() => getJSON('https://httpstat.us/200?sleep=5000', { timeout: 500 }) ).rejects.toThrowError( '[GET] "https://httpstat.us/200?sleep=5000": The operation was aborted.' @@ -366,37 +373,37 @@ describe('ipfsGet', () => { }); describe('on error', () => { - test('should throw an error when the response is not a JSON file', async () => { + test('should throw an error when the response is not a JSON file', () => { expect.assertions(1); - await expect(() => ipfsGet('snapshot.org', cid)).rejects.toThrowError( + expect(() => ipfsGet('snapshot.org', cid)).rejects.toThrowError( /Unexpected.*JSON/ ); }); - test('should throw an error on network error (no response)', async () => { + test('should throw an error on network error (no response)', () => { expect.assertions(1); - await expect(() => + expect(() => ipfsGet('score-null.snapshot.org', cid) ).rejects.toThrowError('ENOTFOUND'); }); - test('should throw an error on network error (not found)', async () => { + test('should throw an error on network error (not found)', () => { expect.assertions(1); - await expect(() => ipfsGet('httpstat.us/404', cid)).rejects.toThrowError( + expect(() => ipfsGet('httpstat.us/404', cid)).rejects.toThrowError( '404 Not Found' ); }); - test('should throw an error on network error (on invalid protocol argument)', async () => { + test('should throw an error on network error (on invalid protocol argument)', () => { expect.assertions(1); - await expect(() => - ipfsGet('pineapple.fyi', cid, 'test') - ).rejects.toThrowError('404 Not Found'); + expect(() => ipfsGet('pineapple.fyi', cid, 'test')).rejects.toThrowError( + '404 Not Found' + ); }); - test('should throw an error on network error (timeout)', async () => { + test('should throw an error on network error (timeout)', () => { expect.assertions(1); - await expect(() => + expect(() => ipfsGet( `httpstat.us/200?sleep=5000&cachebuster=${Date.now()}`, cid, @@ -407,3 +414,122 @@ describe('ipfsGet', () => { }); }); }); + +describe('subgraphRequest', () => { + const query = { + blocks: { + __args: { + where: { + ts: 0, + network_in: ['1'] + } + }, + network: true, + number: true + } + }; + const HOST = 'https://blockfinder.snapshot.org'; + + describe('on success', () => { + test('should return a JSON object', async () => { + expect.assertions(1); + expect(await subgraphRequest(HOST, query)).toEqual({ + blocks: [{ network: '1', number: 0 }] + }); + }); + }); + + describe('on error', () => { + const invalidQuery = { + blocks: { + __args: { + where: { + ts: 0, + network_in: ['4'] + } + }, + network: true, + number: true + } + }; + + test('should return the error response from subgraph', () => { + expect.assertions(1); + expect(subgraphRequest(HOST, invalidQuery)).to.rejects.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'invalid network', + extensions: { code: 'INVALID_NETWORK' } + }) + ] + }) + ); + }); + + test('should return an errors object on not JSON response', () => { + expect.assertions(1); + expect( + subgraphRequest('https://httpstat.us/200', query, { + headers: { + Accept: 'application/xml', + 'Content-Type': 'application/xml' + } + }) + ).to.rejects.toEqual({ + errors: [ + { + message: 'Body is not a JSON object', + extensions: { code: 'INVALID_JSON' } + } + ] + }); + }); + + test('should return an errors object on network error (no response)', () => { + expect.assertions(1); + expect( + subgraphRequest('https://test-null.snapshot.org', query) + ).to.rejects.toEqual({ + errors: [ + { + extensions: { code: 0 }, + message: + 'FetchError: [POST] "https://test-null.snapshot.org": request to https://test-null.snapshot.org/ failed, reason: getaddrinfo ENOTFOUND test-null.snapshot.org' + } + ] + }); + }); + + test('should return an errors object on network error (not found)', () => { + expect.assertions(1); + expect( + subgraphRequest('https://httpstat.us/404', query) + ).to.rejects.toEqual({ + errors: [ + { + extensions: { code: 404 }, + message: 'Not Found' + } + ] + }); + }); + + test('should return an errors object on network error (timeout)', () => { + expect.assertions(1); + expect( + subgraphRequest('https://httpstat.us/200?sleep=5000', query, { + timeout: 500 + }) + ).to.rejects.toEqual({ + errors: [ + { + extensions: { code: 0 }, + message: + 'FetchError: [POST] "https://httpstat.us/200?sleep=5000": The operation was aborted.' + } + ] + }); + }); + }); +}); From 3bb41c96df444db2e372b860c5f463c55993501e Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Fri, 29 Sep 2023 02:07:56 +0900 Subject: [PATCH 19/25] chore: fix tests --- test/e2e/utils/blockfinder.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/utils/blockfinder.spec.js b/test/e2e/utils/blockfinder.spec.js index c20760767..61355e16e 100644 --- a/test/e2e/utils/blockfinder.spec.js +++ b/test/e2e/utils/blockfinder.spec.js @@ -72,13 +72,13 @@ describe('Blockfinder', () => { test('should throw a graphql-like error on network error (not found)', async () => { await expect( getSnapshots('1', 17789786, provider, ['5', '137'], { - blockFinderUrl: 'http://google.com' + blockFinderUrl: 'http://httpstat.us/404' }) ).to.rejects.toEqual({ errors: [ { - extensions: { code: 405 }, - message: 'Method Not Allowed' + extensions: { code: 404 }, + message: 'Not Found' } ] }); From 3d1dc2a2608bb5da3670f325536d3932e7ddd5f4 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Sat, 30 Sep 2023 02:05:50 +0900 Subject: [PATCH 20/25] chore: remove redundant function calls --- test/e2e/utils.spec.js | 52 +++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/test/e2e/utils.spec.js b/test/e2e/utils.spec.js index baebe677a..2b64df70b 100644 --- a/test/e2e/utils.spec.js +++ b/test/e2e/utils.spec.js @@ -72,7 +72,7 @@ describe('getScores', () => { describe('on error', () => { test('should return the JSON-RPC error from score-api', () => { expect.assertions(1); - expect(getScores('test.eth', [], '1', ['0x0'])).to.rejects.toEqual({ + expect(getScores('test.eth', [], '1', ['0x0'])).rejects.toEqual({ code: 500, message: 'unauthorized', data: 'something wrong with the strategies' @@ -83,7 +83,7 @@ describe('getScores', () => { expect.assertions(1); expect( getScores(...payload, 'https://score-null.snapshot.org') - ).to.rejects.toEqual({ + ).rejects.toEqual({ code: 0, message: 'FetchError: [POST] "https://score-null.snapshot.org/api/scores": request to https://score-null.snapshot.org/api/scores failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', @@ -95,7 +95,7 @@ describe('getScores', () => { expect.assertions(1); expect( getScores(...payload, 'https://httpstat.us', { pathname: '404' }) - ).to.rejects.toEqual( + ).rejects.toEqual( expect.objectContaining({ code: 404, message: 'Not Found' @@ -110,7 +110,7 @@ describe('getScores', () => { timeout: 500, pathname: '200' }) - ).to.rejects.toEqual( + ).rejects.toEqual( expect.objectContaining({ code: 0, message: @@ -157,7 +157,7 @@ describe('getVp', () => { expect.assertions(1); expect( getVp('test', network, strategies, s, space, delegation) - ).to.rejects.toEqual({ + ).rejects.toEqual({ code: 400, message: 'unauthorized', data: 'invalid address' @@ -170,7 +170,7 @@ describe('getVp', () => { getVp(...defaultOptions, { url: 'https://score-null.snapshot.org' }) - ).to.rejects.toEqual({ + ).rejects.toEqual({ code: 0, message: 'FetchError: [POST] "https://score-null.snapshot.org": request to https://score-null.snapshot.org/ failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', @@ -184,7 +184,7 @@ describe('getVp', () => { getVp(...defaultOptions, { url: 'https://httpstat.us/404' }) - ).to.rejects.toEqual( + ).rejects.toEqual( expect.objectContaining({ code: 404, message: 'Not Found' @@ -199,7 +199,7 @@ describe('getVp', () => { url: 'https://httpstat.us/200?sleep=5000', timeout: 500 }) - ).to.rejects.toEqual( + ).rejects.toEqual( expect.objectContaining({ code: 0, message: @@ -238,7 +238,7 @@ describe('validate', () => { expect.assertions(1); expect( validate(validation, 'test', space, network, 'latest', params) - ).to.rejects.toEqual({ + ).rejects.toEqual({ code: 400, message: 'unauthorized', data: 'invalid address' @@ -251,7 +251,7 @@ describe('validate', () => { validate(...defaultOptions, { url: 'https://score-null.snapshot.org' }) - ).to.rejects.toEqual({ + ).rejects.toEqual({ code: 0, message: 'FetchError: [POST] "https://score-null.snapshot.org": request to https://score-null.snapshot.org/ failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', @@ -265,7 +265,7 @@ describe('validate', () => { validate(...defaultOptions, { url: 'https://httpstat.us/404' }) - ).to.rejects.toEqual( + ).rejects.toEqual( expect.objectContaining({ code: 404, message: 'Not Found' @@ -280,7 +280,7 @@ describe('validate', () => { url: 'https://httpstat.us/200?sleep=5000', timeout: 500 }) - ).to.rejects.toEqual( + ).rejects.toEqual( expect.objectContaining({ code: 0, message: @@ -455,7 +455,7 @@ describe('subgraphRequest', () => { test('should return the error response from subgraph', () => { expect.assertions(1); - expect(subgraphRequest(HOST, invalidQuery)).to.rejects.toEqual( + expect(subgraphRequest(HOST, invalidQuery)).rejects.toEqual( expect.objectContaining({ errors: [ expect.objectContaining({ @@ -476,7 +476,7 @@ describe('subgraphRequest', () => { 'Content-Type': 'application/xml' } }) - ).to.rejects.toEqual({ + ).rejects.toEqual({ errors: [ { message: 'Body is not a JSON object', @@ -490,7 +490,7 @@ describe('subgraphRequest', () => { expect.assertions(1); expect( subgraphRequest('https://test-null.snapshot.org', query) - ).to.rejects.toEqual({ + ).rejects.toEqual({ errors: [ { extensions: { code: 0 }, @@ -503,16 +503,16 @@ describe('subgraphRequest', () => { test('should return an errors object on network error (not found)', () => { expect.assertions(1); - expect( - subgraphRequest('https://httpstat.us/404', query) - ).to.rejects.toEqual({ - errors: [ - { - extensions: { code: 404 }, - message: 'Not Found' - } - ] - }); + expect(subgraphRequest('https://httpstat.us/404', query)).rejects.toEqual( + { + errors: [ + { + extensions: { code: 404 }, + message: 'Not Found' + } + ] + } + ); }); test('should return an errors object on network error (timeout)', () => { @@ -521,7 +521,7 @@ describe('subgraphRequest', () => { subgraphRequest('https://httpstat.us/200?sleep=5000', query, { timeout: 500 }) - ).to.rejects.toEqual({ + ).rejects.toEqual({ errors: [ { extensions: { code: 0 }, From dbc44e0feeb1636348aae070069a2aa7316983ee Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Sat, 30 Sep 2023 02:54:41 +0900 Subject: [PATCH 21/25] chore: fix test --- test/e2e/utils.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/e2e/utils.spec.js b/test/e2e/utils.spec.js index 2b64df70b..3aae7329c 100644 --- a/test/e2e/utils.spec.js +++ b/test/e2e/utils.spec.js @@ -420,7 +420,7 @@ describe('subgraphRequest', () => { blocks: { __args: { where: { - ts: 0, + ts: 1640000000, network_in: ['1'] } }, @@ -434,7 +434,7 @@ describe('subgraphRequest', () => { test('should return a JSON object', async () => { expect.assertions(1); expect(await subgraphRequest(HOST, query)).toEqual({ - blocks: [{ network: '1', number: 0 }] + blocks: [{ network: '1', number: 13841761 }] }); }); }); @@ -444,7 +444,7 @@ describe('subgraphRequest', () => { blocks: { __args: { where: { - ts: 0, + ts: 1640000000, network_in: ['4'] } }, From 261a6e42d1bc8f70d9a96731908993495e01a731 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Sat, 30 Sep 2023 02:58:51 +0900 Subject: [PATCH 22/25] chore: add retry to flaky tests --- test/e2e/utils.spec.js | 281 +++++++++++++++++++++++++---------------- 1 file changed, 174 insertions(+), 107 deletions(-) diff --git a/test/e2e/utils.spec.js b/test/e2e/utils.spec.js index 3aae7329c..c68dd1d07 100644 --- a/test/e2e/utils.spec.js +++ b/test/e2e/utils.spec.js @@ -38,46 +38,66 @@ describe('getScores', () => { expect(await getScores(...payload)).toEqual(scoresResponse); }); - test('should return the full scores object', async () => { - expect.assertions(1); - expect( - await getScores(...payload, SCORE_API_URL, { - returnValue: 'all' - }) - ).toEqual({ - scores: scoresResponse, - state: stateResponse - }); - }); - - test('should return only the given field', async () => { - expect.assertions(1); - expect( - await getScores(...payload, SCORE_API_URL, { - returnValue: 'state' - }) - ).toEqual(stateResponse); - }); - - test('should return undefined when the given field does not exist', async () => { - expect.assertions(1); - expect( - await getScores(...payload, SCORE_API_URL, { - returnValue: 'test' - }) - ).toEqual(undefined); - }); + test( + 'should return the full scores object', + async () => { + expect.assertions(1); + expect( + await getScores(...payload, SCORE_API_URL, { + returnValue: 'all' + }) + ).toEqual({ + scores: scoresResponse, + state: stateResponse + }); + }, + 15e3, + 3 + ); + + test( + 'should return only the given field', + async () => { + expect.assertions(1); + expect( + await getScores(...payload, SCORE_API_URL, { + returnValue: 'state' + }) + ).toEqual(stateResponse); + }, + 15e3, + 3 + ); + + test( + 'should return undefined when the given field does not exist', + async () => { + expect.assertions(1); + expect( + await getScores(...payload, SCORE_API_URL, { + returnValue: 'test' + }) + ).toEqual(undefined); + }, + 15e3, + 3 + ); }); describe('on error', () => { - test('should return the JSON-RPC error from score-api', () => { - expect.assertions(1); - expect(getScores('test.eth', [], '1', ['0x0'])).rejects.toEqual({ - code: 500, - message: 'unauthorized', - data: 'something wrong with the strategies' - }); - }); + test( + 'should return the JSON-RPC error from score-api', + () => { + expect.assertions(1); + expect(getScores('test.eth', [], '1', ['0x0'])).rejects.toEqual({ + code: 500, + message: 'unauthorized', + data: 'something wrong with the strategies' + }); + }, + 15e3, + 3 + ); test('should return a JSON-RPC-like error on network error (no response)', () => { expect.assertions(1); @@ -142,27 +162,37 @@ describe('getVp', () => { const defaultOptions = [address, network, strategies, s, space, delegation]; describe('on success', () => { - test('should return a voting power', async () => { - expect.assertions(1); - expect(await getVp(...defaultOptions)).toEqual({ - vp: 10.49214268914954, - vp_by_strategy: [10.443718706159482, 0.04842398299005922], - vp_state: 'final' - }); - }); + test( + 'should return a voting power', + async () => { + expect.assertions(1); + expect(await getVp(...defaultOptions)).toEqual({ + vp: 10.49214268914954, + vp_by_strategy: [10.443718706159482, 0.04842398299005922], + vp_state: 'final' + }); + }, + 15e3, + 3 + ); }); describe('on error', () => { - test('should return a JSON-RPC error from score-api', () => { - expect.assertions(1); - expect( - getVp('test', network, strategies, s, space, delegation) - ).rejects.toEqual({ - code: 400, - message: 'unauthorized', - data: 'invalid address' - }); - }); + test( + 'should return a JSON-RPC error from score-api', + () => { + expect.assertions(1); + expect( + getVp('test', network, strategies, s, space, delegation) + ).rejects.toEqual({ + code: 400, + message: 'unauthorized', + data: 'invalid address' + }); + }, + 15e3, + 3 + ); test('should return a JSON-RPC-like error on network error (no response)', () => { expect.assertions(1); @@ -227,23 +257,33 @@ describe('validate', () => { const defaultOptions = [validation, author, space, network, 'latest', params]; describe('on success', () => { - test('should return a boolean', async () => { - expect.assertions(1); - expect(await validate(...defaultOptions)).toEqual(false); - }); + test( + 'should return a boolean', + async () => { + expect.assertions(1); + expect(await validate(...defaultOptions)).toEqual(false); + }, + 15e3, + 3 + ); }); describe('on error', () => { - test('should return the JSON-RPC error from score-api', () => { - expect.assertions(1); - expect( - validate(validation, 'test', space, network, 'latest', params) - ).rejects.toEqual({ - code: 400, - message: 'unauthorized', - data: 'invalid address' - }); - }); + test( + 'should return the JSON-RPC error from score-api', + () => { + expect.assertions(1); + expect( + validate(validation, 'test', space, network, 'latest', params) + ).rejects.toEqual({ + code: 400, + message: 'unauthorized', + data: 'invalid address' + }); + }, + 15e3, + 3 + ); test('should return a JSON-RPC-like error on network error (no response)', () => { expect.assertions(1); @@ -293,21 +333,31 @@ describe('validate', () => { describe('getJSON', () => { describe('on success', () => { - test('should return a JSON object from the specified URL', async () => { - expect.assertions(1); - expect(await getJSON('https://hub.snapshot.org')).toEqual( - expect.objectContaining({ name: 'snapshot-hub' }) - ); - }); - - test('should return a JSON object from the specified CID', async () => { - expect.assertions(1); - expect( - await getJSON( - 'bafkreib5epjzumf3omr7rth5mtcsz4ugcoh3ut4d46hx5xhwm4b3pqr2vi' - ) - ).toEqual(expect.objectContaining({ status: 'OK' })); - }); + test( + 'should return a JSON object from the specified URL', + async () => { + expect.assertions(1); + expect(await getJSON('https://hub.snapshot.org')).toEqual( + expect.objectContaining({ name: 'snapshot-hub' }) + ); + }, + 3e3, + 3 + ); + + test( + 'should return a JSON object from the specified CID', + async () => { + expect.assertions(1); + expect( + await getJSON( + 'bafkreib5epjzumf3omr7rth5mtcsz4ugcoh3ut4d46hx5xhwm4b3pqr2vi' + ) + ).toEqual(expect.objectContaining({ status: 'OK' })); + }, + 15e3, + 3 + ); }); describe('on error', () => { @@ -366,10 +416,17 @@ describe('ipfsGet', () => { const cid = 'bafkreibatgmdqdxsair3j52zfhtntegshtypq2qbex3fgtorwx34kzippe'; describe('on success', () => { - test('should return a JSON object', async () => { - expect.assertions(1); - expect(await ipfsGet('pineapple.fyi', cid)).toEqual({ name: 'Vitalik' }); - }); + test( + 'should return a JSON object', + async () => { + expect.assertions(1); + expect(await ipfsGet('pineapple.fyi', cid)).toEqual({ + name: 'Vitalik' + }); + }, + 15e3, + 3 + ); }); describe('on error', () => { @@ -431,12 +488,17 @@ describe('subgraphRequest', () => { const HOST = 'https://blockfinder.snapshot.org'; describe('on success', () => { - test('should return a JSON object', async () => { - expect.assertions(1); - expect(await subgraphRequest(HOST, query)).toEqual({ - blocks: [{ network: '1', number: 13841761 }] - }); - }); + test( + 'should return a JSON object', + async () => { + expect.assertions(1); + expect(await subgraphRequest(HOST, query)).toEqual({ + blocks: [{ network: '1', number: 13841761 }] + }); + }, + 15e3, + 3 + ); }); describe('on error', () => { @@ -453,19 +515,24 @@ describe('subgraphRequest', () => { } }; - test('should return the error response from subgraph', () => { - expect.assertions(1); - expect(subgraphRequest(HOST, invalidQuery)).rejects.toEqual( - expect.objectContaining({ - errors: [ - expect.objectContaining({ - message: 'invalid network', - extensions: { code: 'INVALID_NETWORK' } - }) - ] - }) - ); - }); + test( + 'should return the error response from subgraph', + () => { + expect.assertions(1); + expect(subgraphRequest(HOST, invalidQuery)).rejects.toEqual( + expect.objectContaining({ + errors: [ + expect.objectContaining({ + message: 'invalid network', + extensions: { code: 'INVALID_NETWORK' } + }) + ] + }) + ); + }, + 15e3, + 3 + ); test('should return an errors object on not JSON response', () => { expect.assertions(1); From 58fc43656ce4a5a5c1453fda215bd372e0011f19 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Sat, 30 Sep 2023 03:03:11 +0900 Subject: [PATCH 23/25] chore: improve tests --- test/e2e/utils.spec.js | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/test/e2e/utils.spec.js b/test/e2e/utils.spec.js index c68dd1d07..3ffcb2e57 100644 --- a/test/e2e/utils.spec.js +++ b/test/e2e/utils.spec.js @@ -105,8 +105,7 @@ describe('getScores', () => { getScores(...payload, 'https://score-null.snapshot.org') ).rejects.toEqual({ code: 0, - message: - 'FetchError: [POST] "https://score-null.snapshot.org/api/scores": request to https://score-null.snapshot.org/api/scores failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', + message: expect.stringContaining('no response'), data: '' }); }); @@ -133,8 +132,7 @@ describe('getScores', () => { ).rejects.toEqual( expect.objectContaining({ code: 0, - message: - 'FetchError: [POST] "https://httpstat.us/200?sleep=5000": The operation was aborted.' + message: expect.stringContaining('The operation was aborted') }) ); }); @@ -202,8 +200,7 @@ describe('getVp', () => { }) ).rejects.toEqual({ code: 0, - message: - 'FetchError: [POST] "https://score-null.snapshot.org": request to https://score-null.snapshot.org/ failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', + message: expect.stringContaining('no response'), data: '' }); }); @@ -232,8 +229,7 @@ describe('getVp', () => { ).rejects.toEqual( expect.objectContaining({ code: 0, - message: - 'FetchError: [POST] "https://httpstat.us/200?sleep=5000": The operation was aborted.' + message: expect.stringContaining('The operation was aborted') }) ); }); @@ -293,8 +289,7 @@ describe('validate', () => { }) ).rejects.toEqual({ code: 0, - message: - 'FetchError: [POST] "https://score-null.snapshot.org": request to https://score-null.snapshot.org/ failed, reason: getaddrinfo ENOTFOUND score-null.snapshot.org', + message: expect.stringContaining('no response'), data: '' }); }); @@ -323,8 +318,7 @@ describe('validate', () => { ).rejects.toEqual( expect.objectContaining({ code: 0, - message: - 'FetchError: [POST] "https://httpstat.us/200?sleep=5000": The operation was aborted.' + message: expect.stringContaining('The operation was aborted') }) ); }); @@ -405,9 +399,7 @@ describe('getJSON', () => { expect.assertions(1); expect(() => getJSON('https://httpstat.us/200?sleep=5000', { timeout: 500 }) - ).rejects.toThrowError( - '[GET] "https://httpstat.us/200?sleep=5000": The operation was aborted.' - ); + ).rejects.toThrowError('The operation was aborted'); }); }); }); @@ -467,7 +459,7 @@ describe('ipfsGet', () => { 'ipfs', { timeout: 500 } ) - ).rejects.toThrowError('aborted'); + ).rejects.toThrowError('The operation was aborted'); }); }); }); @@ -561,8 +553,7 @@ describe('subgraphRequest', () => { errors: [ { extensions: { code: 0 }, - message: - 'FetchError: [POST] "https://test-null.snapshot.org": request to https://test-null.snapshot.org/ failed, reason: getaddrinfo ENOTFOUND test-null.snapshot.org' + message: expect.stringContaining('no response') } ] }); @@ -592,8 +583,7 @@ describe('subgraphRequest', () => { errors: [ { extensions: { code: 0 }, - message: - 'FetchError: [POST] "https://httpstat.us/200?sleep=5000": The operation was aborted.' + message: expect.stringContaining('The operation was aborted') } ] }); From 93f80699565908ec77961324201bbc48dbba21bd Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Tue, 3 Oct 2023 11:03:23 +0900 Subject: [PATCH 24/25] chore: fix tests for node 18 --- test/e2e/sign/index.spec.js | 2 +- test/e2e/utils.spec.js | 22 ++++++++++++---------- test/e2e/utils/blockfinder.spec.js | 3 +-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/test/e2e/sign/index.spec.js b/test/e2e/sign/index.spec.js index d45e66f20..89d907154 100644 --- a/test/e2e/sign/index.spec.js +++ b/test/e2e/sign/index.spec.js @@ -57,7 +57,7 @@ describe('Client', () => { }); test.each([ - ['ENOTFOUND', 'https://unknown.snapshot.org'], + ['no response', 'https://unknown.snapshot.org'], ['404 Not Found', 'https://httpstat.us/404'] ])('should throw an error on network error (%s)', async (code, url) => { expect.assertions(1); diff --git a/test/e2e/utils.spec.js b/test/e2e/utils.spec.js index 3ffcb2e57..2ca97466c 100644 --- a/test/e2e/utils.spec.js +++ b/test/e2e/utils.spec.js @@ -132,7 +132,7 @@ describe('getScores', () => { ).rejects.toEqual( expect.objectContaining({ code: 0, - message: expect.stringContaining('The operation was aborted') + message: expect.stringContaining('operation was aborted') }) ); }); @@ -229,7 +229,7 @@ describe('getVp', () => { ).rejects.toEqual( expect.objectContaining({ code: 0, - message: expect.stringContaining('The operation was aborted') + message: expect.stringContaining('operation was aborted') }) ); }); @@ -318,7 +318,7 @@ describe('validate', () => { ).rejects.toEqual( expect.objectContaining({ code: 0, - message: expect.stringContaining('The operation was aborted') + message: expect.stringContaining('operation was aborted') }) ); }); @@ -364,7 +364,9 @@ describe('getJSON', () => { test('should throw an error when the url is an empty string', () => { expect.assertions(1); - expect(() => getJSON('')).rejects.toThrowError(/Invalid URL/); + expect(() => getJSON('')).rejects.toThrowError( + /(Invalid|Failed to parse) URL/i + ); }); test('should throw an error when the given argument is not valid CID', () => { @@ -377,7 +379,7 @@ describe('getJSON', () => { test('should throw an error when the url is not valid', () => { expect.assertions(1); expect(() => getJSON('https:// testurl.com')).rejects.toThrowError( - /Invalid URL/ + /(Invalid|Failed to parse) URL/i ); }); @@ -385,7 +387,7 @@ describe('getJSON', () => { expect.assertions(1); expect(() => getJSON('https://score-null.snapshot.org') - ).rejects.toThrowError('ENOTFOUND'); + ).rejects.toThrowError('no response'); }); test('should throw an error on network error (not found)', () => { @@ -399,7 +401,7 @@ describe('getJSON', () => { expect.assertions(1); expect(() => getJSON('https://httpstat.us/200?sleep=5000', { timeout: 500 }) - ).rejects.toThrowError('The operation was aborted'); + ).rejects.toThrowError('operation was aborted'); }); }); }); @@ -433,7 +435,7 @@ describe('ipfsGet', () => { expect.assertions(1); expect(() => ipfsGet('score-null.snapshot.org', cid) - ).rejects.toThrowError('ENOTFOUND'); + ).rejects.toThrowError('no response'); }); test('should throw an error on network error (not found)', () => { @@ -459,7 +461,7 @@ describe('ipfsGet', () => { 'ipfs', { timeout: 500 } ) - ).rejects.toThrowError('The operation was aborted'); + ).rejects.toThrowError('operation was aborted'); }); }); }); @@ -583,7 +585,7 @@ describe('subgraphRequest', () => { errors: [ { extensions: { code: 0 }, - message: expect.stringContaining('The operation was aborted') + message: expect.stringContaining('operation was aborted') } ] }); diff --git a/test/e2e/utils/blockfinder.spec.js b/test/e2e/utils/blockfinder.spec.js index 61355e16e..0c6b633bd 100644 --- a/test/e2e/utils/blockfinder.spec.js +++ b/test/e2e/utils/blockfinder.spec.js @@ -62,8 +62,7 @@ describe('Blockfinder', () => { errors: [ { extensions: { code: 0 }, - message: - 'FetchError: [POST] "http://localhost:12345": request to http://localhost:12345/ failed, reason: connect ECONNREFUSED 127.0.0.1:12345' + message: expect.stringContaining('no response') } ] }); From cfb0e3e9ad6ed68c01b80db9fca8bc69c9d16105 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Mon, 9 Oct 2023 14:17:05 +0900 Subject: [PATCH 25/25] chore: bump feat version instead of major --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cdea6b159..1942c1680 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@snapshot-labs/snapshot.js", - "version": "1.0.0-beta.0", + "version": "0.8.0-beta.0", "repository": "snapshot-labs/snapshot.js", "license": "MIT", "main": "dist/snapshot.cjs.js",