From 15f823fda842f96b7ece0f7f92950c7c4cb77c17 Mon Sep 17 00:00:00 2001 From: Eela Nagaraj <7308464+eelanagaraj@users.noreply.github.com> Date: Fri, 21 Oct 2022 02:09:38 +0200 Subject: [PATCH] Audit tests (#9963) * Preliminary small combiner test fixes * Add disabled signers test cases for domain endpoint * Add signer timeout tests to domain combiner * Restructure PNP & LEGACY combiner tests before adding timeout and disabled tests * Add timeout and disabled tests for combiner PNP and LEGACY * Add signature configuration tests in combiner * Small combiner fixes * Small signer fixes --- .../combiner/package.json | 1 + .../combiner/test/integration/domain.test.ts | 1200 ++++++++++------- .../test/integration/legacypnp.test.ts | 782 ++++++----- .../combiner/test/integration/pnp.test.ts | 869 ++++++------ .../signer/test/integration/domain.test.ts | 12 +- .../signer/test/integration/legacypnp.test.ts | 6 +- .../signer/test/integration/pnp.test.ts | 6 +- 7 files changed, 1622 insertions(+), 1254 deletions(-) diff --git a/packages/phone-number-privacy/combiner/package.json b/packages/phone-number-privacy/combiner/package.json index f197564e5fe..3b0b57e19c1 100644 --- a/packages/phone-number-privacy/combiner/package.json +++ b/packages/phone-number-privacy/combiner/package.json @@ -49,6 +49,7 @@ "@celo/phone-number-privacy-signer": "2.0.0-dev", "@types/btoa": "^1.2.3", "@types/express": "^4.17.6", + "@types/supertest": "^2.0.12", "@types/uuid": "^7.0.3", "dotenv": "^8.2.0", "firebase-functions-test": "^0.3.3", diff --git a/packages/phone-number-privacy/combiner/test/integration/domain.test.ts b/packages/phone-number-privacy/combiner/test/integration/domain.test.ts index 16747f69fcf..f9cca121464 100644 --- a/packages/phone-number-privacy/combiner/test/integration/domain.test.ts +++ b/packages/phone-number-privacy/combiner/test/integration/domain.test.ts @@ -12,6 +12,7 @@ import { DomainRestrictedSignatureRequest, domainRestrictedSignatureRequestEIP712, DomainRestrictedSignatureResponse, + ErrorMessage, genSessionID, KEY_VERSION_HEADER, PoprfClient, @@ -231,6 +232,13 @@ describe('domainService', () => { const signerMigrationsPath = '../signer/src/common/database/migrations' + const expectedEvals: string[] = [ + '3QLFPV6VvnhhnZ7mOu0xm7BUUJIUVY6vEHvZONOtZ/c=', + 'BBG0fAZJ6VNQwjge+3vOCF3uBo5KCs2+er/f/2QcV58=', + '1/otd1fW1nhUoU3ubjFDS8/RX0OClvHDsmGdnz6fZVE=', + ] + const expectedEval = expectedEvals[config.domains.keys.currentVersion - 1] + beforeAll(async () => { keyProvider1 = new MockKeyProvider( new Map([ @@ -261,9 +269,6 @@ describe('domainService', () => { signerDB1 = await initSignerDatabase(signerConfig, signerMigrationsPath) signerDB2 = await initSignerDatabase(signerConfig, signerMigrationsPath) signerDB3 = await initSignerDatabase(signerConfig, signerMigrationsPath) - signer1 = startSigner(signerConfig, signerDB1, keyProvider1).listen(3001) - signer2 = startSigner(signerConfig, signerDB2, keyProvider2).listen(3002) - signer3 = startSigner(signerConfig, signerDB3, keyProvider3).listen(3003) }) afterEach(async () => { @@ -275,321 +280,411 @@ describe('domainService', () => { signer3?.close() }) - describe(`${CombinerEndpoint.DISABLE_DOMAIN}`, () => { - it('Should respond with 200 on valid request', async () => { - const res = await request(app) - .post(CombinerEndpoint.DISABLE_DOMAIN) - .send(await disableRequest()) - expect(res.status).toBe(200) - expect(res.body).toStrictEqual({ - success: true, - version: res.body.version, - status: { disabled: true, counter: 0, timer: 0, now: res.body.status.now }, - }) + describe('when signers are operating correctly', () => { + beforeEach(async () => { + signer1 = startSigner(signerConfig, signerDB1, keyProvider1).listen(3001) + signer2 = startSigner(signerConfig, signerDB2, keyProvider2).listen(3002) + signer3 = startSigner(signerConfig, signerDB3, keyProvider3).listen(3003) }) - it('Should respond with 200 on repeated valid requests', async () => { - const req = await disableRequest() - const res1 = await request(app).post(CombinerEndpoint.DISABLE_DOMAIN).send(req) - expect(res1.status).toBe(200) - const expectedResponse: DisableDomainResponse = { - success: true, - version: res1.body.version, - status: { disabled: true, counter: 0, timer: 0, now: res1.body.status.now }, - } - expect(res1.body).toStrictEqual(expectedResponse) - const res2 = await request(app).post(CombinerEndpoint.DISABLE_DOMAIN).send(req) - expect(res2.status).toBe(200) - expectedResponse.status.now = res2.body.status.now - expect(res2.body).toStrictEqual(expectedResponse) - }) + describe(`${CombinerEndpoint.DISABLE_DOMAIN}`, () => { + it('Should respond with 200 on valid request', async () => { + const res = await request(app) + .post(CombinerEndpoint.DISABLE_DOMAIN) + .send(await disableRequest()) + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: res.body.version, + status: { disabled: true, counter: 0, timer: 0, now: res.body.status.now }, + }) + }) + + it('Should respond with 200 on repeated valid requests', async () => { + const req = await disableRequest() + const res1 = await request(app).post(CombinerEndpoint.DISABLE_DOMAIN).send(req) + expect(res1.status).toBe(200) + const expectedResponse: DisableDomainResponse = { + success: true, + version: res1.body.version, + status: { disabled: true, counter: 0, timer: 0, now: res1.body.status.now }, + } + expect(res1.body).toStrictEqual(expectedResponse) + const res2 = await request(app).post(CombinerEndpoint.DISABLE_DOMAIN).send(req) + expect(res2.status).toBe(200) + expectedResponse.status.now = res2.body.status.now + expect(res2.body).toStrictEqual(expectedResponse) + }) - it('Should respond with 200 on extra request fields', async () => { - const req = await disableRequest() - // @ts-ignore Intentionally adding an extra field to the request type - req.options.extraField = noString + it('Should respond with 200 on extra request fields', async () => { + const req = await disableRequest() + // @ts-ignore Intentionally adding an extra field to the request type + req.options.extraField = noString - const res = await request(app).post(CombinerEndpoint.DISABLE_DOMAIN).send(req) + const res = await request(app).post(CombinerEndpoint.DISABLE_DOMAIN).send(req) - expect(res.status).toBe(200) - expect(res.body).toStrictEqual({ - success: true, - version: res.body.version, - status: { disabled: true, counter: 0, timer: 0, now: res.body.status.now }, + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: res.body.version, + status: { disabled: true, counter: 0, timer: 0, now: res.body.status.now }, + }) }) - }) - it('Should respond with 400 on missing request fields', async () => { - const badRequest = await disableRequest() - // @ts-ignore Intentionally deleting required field - delete badRequest.domain.version + it('Should respond with 400 on missing request fields', async () => { + const badRequest = await disableRequest() + // @ts-ignore Intentionally deleting required field + delete badRequest.domain.version - const res = await request(app).post(CombinerEndpoint.DISABLE_DOMAIN).send(badRequest) + const res = await request(app).post(CombinerEndpoint.DISABLE_DOMAIN).send(badRequest) - expect(res.status).toBe(400) - expect(res.body).toStrictEqual({ - success: false, - version: res.body.version, - error: WarningMessage.INVALID_INPUT, + expect(res.status).toBe(400) + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: WarningMessage.INVALID_INPUT, + }) }) - }) - it('Should respond with 400 on unknown domain', async () => { - // Create a requests with an invalid domain identifier. - const unknownRequest = await disableRequest() - // @ts-ignore UnknownDomain is (intentionally) not a valid domain identifier. - unknownRequest.domain.name = 'UnknownDomain' + it('Should respond with 400 on unknown domain', async () => { + // Create a requests with an invalid domain identifier. + const unknownRequest = await disableRequest() + // @ts-ignore UnknownDomain is (intentionally) not a valid domain identifier. + unknownRequest.domain.name = 'UnknownDomain' - const res = await request(app).post(CombinerEndpoint.DISABLE_DOMAIN).send(unknownRequest) + const res = await request(app).post(CombinerEndpoint.DISABLE_DOMAIN).send(unknownRequest) - expect(res.status).toBe(400) - expect(res.body).toStrictEqual({ - success: false, - version: res.body.version, - error: WarningMessage.INVALID_INPUT, + expect(res.status).toBe(400) + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: WarningMessage.INVALID_INPUT, + }) }) - }) - it('Should respond with 400 on bad encoding', async () => { - const badRequest1 = await disableRequest() - // @ts-ignore Intentionally not JSON - badRequest1.domain = 'Freddy' + it('Should respond with 400 on bad encoding', async () => { + const badRequest1 = await disableRequest() + // @ts-ignore Intentionally not JSON + badRequest1.domain = 'Freddy' - const res1 = await request(app).post(CombinerEndpoint.DISABLE_DOMAIN).send(badRequest1) + const res1 = await request(app).post(CombinerEndpoint.DISABLE_DOMAIN).send(badRequest1) - expect(res1.status).toBe(400) - expect(res1.body).toStrictEqual({ - success: false, - version: res1.body.version, - error: WarningMessage.INVALID_INPUT, - }) + expect(res1.status).toBe(400) + expect(res1.body).toStrictEqual({ + success: false, + version: res1.body.version, + error: WarningMessage.INVALID_INPUT, + }) - const badRequest2 = '' + const badRequest2 = '' - const res2 = await request(app).post(CombinerEndpoint.DISABLE_DOMAIN).send(badRequest2) + const res2 = await request(app).post(CombinerEndpoint.DISABLE_DOMAIN).send(badRequest2) - expect(res2.status).toBe(400) - expect(res2.body).toStrictEqual({ - success: false, - version: res2.body.version, - error: WarningMessage.INVALID_INPUT, + expect(res2.status).toBe(400) + expect(res2.body).toStrictEqual({ + success: false, + version: res2.body.version, + error: WarningMessage.INVALID_INPUT, + }) }) - }) - it('Should respond with 401 on failed auth', async () => { - // Create a manipulated request, which will have a bad signature. - const badRequest = await disableRequest() - badRequest.domain.salt = defined('badSalt') + it('Should respond with 401 on failed auth', async () => { + // Create a manipulated request, which will have a bad signature. + const badRequest = await disableRequest() + badRequest.domain.salt = defined('badSalt') - const res = await request(app).post(CombinerEndpoint.DISABLE_DOMAIN).send(badRequest) + const res = await request(app).post(CombinerEndpoint.DISABLE_DOMAIN).send(badRequest) - expect(res.status).toBe(401) - expect(res.body).toStrictEqual({ - success: false, - version: res.body.version, - error: WarningMessage.UNAUTHENTICATED_USER, + expect(res.status).toBe(401) + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: WarningMessage.UNAUTHENTICATED_USER, + }) }) - }) - // TODO(2.0.0, testing) test this with signers disabled too - // https://github.com/celo-org/celo-monorepo/issues/9811 - it('Should respond with 503 on disabled api', async () => { - const configWithApiDisabled: typeof combinerConfig = JSON.parse( - JSON.stringify(combinerConfig) - ) - configWithApiDisabled.domains.enabled = false - const appWithApiDisabled = startCombiner(configWithApiDisabled) + it('Should respond with 503 on disabled api', async () => { + const configWithApiDisabled: typeof combinerConfig = JSON.parse( + JSON.stringify(combinerConfig) + ) + configWithApiDisabled.domains.enabled = false + const appWithApiDisabled = startCombiner(configWithApiDisabled) - const req = await disableRequest() + const req = await disableRequest() - const res = await request(appWithApiDisabled).post(CombinerEndpoint.DISABLE_DOMAIN).send(req) + const res = await request(appWithApiDisabled) + .post(CombinerEndpoint.DISABLE_DOMAIN) + .send(req) - expect(res.status).toBe(503) - expect(res.body).toStrictEqual({ - success: false, - version: res.body.version, - error: WarningMessage.API_UNAVAILABLE, + expect(res.status).toBe(503) + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: WarningMessage.API_UNAVAILABLE, + }) }) }) - }) - describe(`${CombinerEndpoint.DOMAIN_QUOTA_STATUS}`, () => { - it('Should respond with 200 on valid request', async () => { - const res = await request(app) - .post(CombinerEndpoint.DOMAIN_QUOTA_STATUS) - .send(await quotaRequest()) - expect(res.status).toBe(200) - expect(res.body).toStrictEqual({ - success: true, - version: res.body.version, - status: { disabled: false, counter: 0, timer: 0, now: res.body.status.now }, + describe(`${CombinerEndpoint.DOMAIN_QUOTA_STATUS}`, () => { + it('Should respond with 200 on valid request', async () => { + const res = await request(app) + .post(CombinerEndpoint.DOMAIN_QUOTA_STATUS) + .send(await quotaRequest()) + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: res.body.version, + status: { disabled: false, counter: 0, timer: 0, now: res.body.status.now }, + }) + }) + + it('Should respond with 200 on repeated valid requests', async () => { + const req = await quotaRequest() + const res1 = await request(app).post(CombinerEndpoint.DOMAIN_QUOTA_STATUS).send(req) + const expectedResponse: DomainQuotaStatusResponse = { + success: true, + version: res1.body.version, + status: { disabled: false, counter: 0, timer: 0, now: res1.body.status.now }, + } + + expect(res1.status).toBe(200) + expect(res1.body).toStrictEqual(expectedResponse) + + const res2 = await request(app).post(CombinerEndpoint.DOMAIN_QUOTA_STATUS).send(req) + expect(res2.status).toBe(200) + // Prevent flakiness due to slight timing inconsistencies + expectedResponse.status.now = res2.body.status.now + expect(res2.body).toStrictEqual(expectedResponse) }) - }) - it('Should respond with 200 on repeated valid requests', async () => { - const req = await quotaRequest() - const res1 = await request(app).post(CombinerEndpoint.DOMAIN_QUOTA_STATUS).send(req) - const expectedResponse: DomainQuotaStatusResponse = { - success: true, - version: res1.body.version, - status: { disabled: false, counter: 0, timer: 0, now: res1.body.status.now }, - } + it('Should respond with 200 on extra request fields', async () => { + const req = await quotaRequest() + // @ts-ignore Intentionally adding an extra field to the request type + req.options.extraField = noString - expect(res1.status).toBe(200) - expect(res1.body).toStrictEqual(expectedResponse) + const res = await request(app).post(CombinerEndpoint.DOMAIN_QUOTA_STATUS).send(req) - const res2 = await request(app).post(CombinerEndpoint.DOMAIN_QUOTA_STATUS).send(req) - expect(res2.status).toBe(200) - // Prevent flakiness due to slight timing inconsistencies - expectedResponse.status.now = res2.body.status.now - expect(res2.body).toStrictEqual(expectedResponse) - }) + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: res.body.version, + status: { disabled: false, counter: 0, timer: 0, now: res.body.status.now }, + }) + }) - it('Should respond with 200 on extra request fields', async () => { - const req = await quotaRequest() - // @ts-ignore Intentionally adding an extra field to the request type - req.options.extraField = noString + it('Should respond with 400 on missing request fields', async () => { + const badRequest = await quotaRequest() + // @ts-ignore Intentionally deleting required field + delete badRequest.domain.version - const res = await request(app).post(CombinerEndpoint.DOMAIN_QUOTA_STATUS).send(req) + const res = await request(app).post(CombinerEndpoint.DOMAIN_QUOTA_STATUS).send(badRequest) - expect(res.status).toBe(200) - expect(res.body).toStrictEqual({ - success: true, - version: res.body.version, - status: { disabled: false, counter: 0, timer: 0, now: res.body.status.now }, + expect(res.status).toBe(400) + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: WarningMessage.INVALID_INPUT, + }) }) - }) - it('Should respond with 400 on missing request fields', async () => { - const badRequest = await quotaRequest() - // @ts-ignore Intentionally deleting required field - delete badRequest.domain.version + it('Should respond with 400 on unknown domain', async () => { + // Create a requests with an invalid domain identifier. + const unknownRequest = await quotaRequest() + // @ts-ignore UnknownDomain is (intentionally) not a valid domain identifier. + unknownRequest.domain.name = 'UnknownDomain' - const res = await request(app).post(CombinerEndpoint.DOMAIN_QUOTA_STATUS).send(badRequest) + const res = await request(app) + .post(CombinerEndpoint.DOMAIN_QUOTA_STATUS) + .send(unknownRequest) - expect(res.status).toBe(400) - expect(res.body).toStrictEqual({ - success: false, - version: res.body.version, - error: WarningMessage.INVALID_INPUT, + expect(res.status).toBe(400) + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: WarningMessage.INVALID_INPUT, + }) }) - }) - it('Should respond with 400 on unknown domain', async () => { - // Create a requests with an invalid domain identifier. - const unknownRequest = await quotaRequest() - // @ts-ignore UnknownDomain is (intentionally) not a valid domain identifier. - unknownRequest.domain.name = 'UnknownDomain' + it('Should respond with 400 on bad encoding', async () => { + const badRequest1 = await quotaRequest() + // @ts-ignore Intentionally not JSON + badRequest1.domain = 'Freddy' - const res = await request(app).post(CombinerEndpoint.DOMAIN_QUOTA_STATUS).send(unknownRequest) + const res1 = await request(app).post(CombinerEndpoint.DOMAIN_QUOTA_STATUS).send(badRequest1) - expect(res.status).toBe(400) - expect(res.body).toStrictEqual({ - success: false, - version: res.body.version, - error: WarningMessage.INVALID_INPUT, - }) - }) + expect(res1.status).toBe(400) + expect(res1.body).toStrictEqual({ + success: false, + version: res1.body.version, + error: WarningMessage.INVALID_INPUT, + }) - it('Should respond with 400 on bad encoding', async () => { - const badRequest1 = await quotaRequest() - // @ts-ignore Intentionally not JSON - badRequest1.domain = 'Freddy' + const badRequest2 = '' - const res1 = await request(app).post(CombinerEndpoint.DOMAIN_QUOTA_STATUS).send(badRequest1) + const res2 = await request(app).post(CombinerEndpoint.DOMAIN_QUOTA_STATUS).send(badRequest2) - expect(res1.status).toBe(400) - expect(res1.body).toStrictEqual({ - success: false, - version: res1.body.version, - error: WarningMessage.INVALID_INPUT, + expect(res2.status).toBe(400) + expect(res2.body).toStrictEqual({ + success: false, + version: res2.body.version, + error: WarningMessage.INVALID_INPUT, + }) }) - const badRequest2 = '' + it('Should respond with 401 on failed auth', async () => { + // Create a manipulated request, which will have a bad signature. + const badRequest = await quotaRequest() + badRequest.domain.salt = defined('badSalt') - const res2 = await request(app).post(CombinerEndpoint.DOMAIN_QUOTA_STATUS).send(badRequest2) + const res = await request(app).post(CombinerEndpoint.DOMAIN_QUOTA_STATUS).send(badRequest) - expect(res2.status).toBe(400) - expect(res2.body).toStrictEqual({ - success: false, - version: res2.body.version, - error: WarningMessage.INVALID_INPUT, + expect(res.status).toBe(401) + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: WarningMessage.UNAUTHENTICATED_USER, + }) }) - }) - it('Should respond with 401 on failed auth', async () => { - // Create a manipulated request, which will have a bad signature. - const badRequest = await quotaRequest() - badRequest.domain.salt = defined('badSalt') + it('Should respond with 503 on disabled api', async () => { + const configWithApiDisabled: typeof combinerConfig = JSON.parse( + JSON.stringify(combinerConfig) + ) + configWithApiDisabled.domains.enabled = false + const appWithApiDisabled = startCombiner(configWithApiDisabled) + + const req = await quotaRequest() - const res = await request(app).post(CombinerEndpoint.DOMAIN_QUOTA_STATUS).send(badRequest) + const res = await request(appWithApiDisabled) + .post(CombinerEndpoint.DOMAIN_QUOTA_STATUS) + .send(req) - expect(res.status).toBe(401) - expect(res.body).toStrictEqual({ - success: false, - version: res.body.version, - error: WarningMessage.UNAUTHENTICATED_USER, + expect(res.status).toBe(503) + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: WarningMessage.API_UNAVAILABLE, + }) }) }) - it('Should respond with 503 on disabled api', async () => { - const configWithApiDisabled: typeof combinerConfig = JSON.parse( - JSON.stringify(combinerConfig) - ) - configWithApiDisabled.domains.enabled = false - const appWithApiDisabled = startCombiner(configWithApiDisabled) + describe(`${CombinerEndpoint.DOMAIN_SIGN}`, () => { + it('Should respond with 200 on valid request', async () => { + const [req, poprfClient] = await signatureRequest() + const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req) + + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: res.body.version, + signature: res.body.signature, + status: { + disabled: false, + counter: 1, + timer: res.body.status.timer, + now: res.body.status.now, + }, + }) + const evaluation = poprfClient.unblindResponse(Buffer.from(res.body.signature, 'base64')) + expect(evaluation.toString('base64')).toEqual(expectedEval) + }) + + for (let i = 1; i <= 3; i++) { + it(`Should respond with 200 on valid request with key version header ${i}`, async () => { + const [req, poprfClient] = await signatureRequest(undefined, undefined, i) + + const res = await request(app) + .post(CombinerEndpoint.DOMAIN_SIGN) + .set(KEY_VERSION_HEADER, i.toString()) + .send(req) + + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: res.body.version, + signature: res.body.signature, + status: { + disabled: false, + counter: 1, + timer: res.body.status.timer, + now: res.body.status.now, + }, + }) + const evaluation = poprfClient.unblindResponse(Buffer.from(res.body.signature, 'base64')) + expect(evaluation.toString('base64')).toEqual(expectedEvals[i - 1]) + }) + } - const req = await quotaRequest() + it('Should respond with 200 if nonce > domainState', async () => { + const [req, poprfClient] = await signatureRequest(undefined, 2) - const res = await request(appWithApiDisabled) - .post(CombinerEndpoint.DOMAIN_QUOTA_STATUS) - .send(req) + const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req) - expect(res.status).toBe(503) - expect(res.body).toStrictEqual({ - success: false, - version: res.body.version, - error: WarningMessage.API_UNAVAILABLE, + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: res.body.version, + signature: res.body.signature, + status: { + disabled: false, + counter: 1, // counter gets incremented, not set to nonce value + timer: res.body.status.timer, + now: res.body.status.now, + }, + }) + const evaluation = poprfClient.unblindResponse(Buffer.from(res.body.signature, 'base64')) + expect(evaluation.toString('base64')).toEqual(expectedEval) }) - }) - }) - describe(`${CombinerEndpoint.DOMAIN_SIGN}`, () => { - const expectedEvals: string[] = [ - '3QLFPV6VvnhhnZ7mOu0xm7BUUJIUVY6vEHvZONOtZ/c=', - 'BBG0fAZJ6VNQwjge+3vOCF3uBo5KCs2+er/f/2QcV58=', - '1/otd1fW1nhUoU3ubjFDS8/RX0OClvHDsmGdnz6fZVE=', - ] - const expectedEval = expectedEvals[config.domains.keys.currentVersion - 1] - - it('Should respond with 200 on valid request', async () => { - const [req, poprfClient] = await signatureRequest() - const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req) - - expect(res.status).toBe(200) - expect(res.body).toStrictEqual({ - success: true, - version: res.body.version, - signature: res.body.signature, - status: { - disabled: false, - counter: 1, - timer: res.body.status.timer, - now: res.body.status.now, - }, - }) - const evaluation = poprfClient.unblindResponse(Buffer.from(res.body.signature, 'base64')) - expect(evaluation.toString('base64')).toEqual(expectedEval) - }) + it('Should respond with 200 on repeated valid requests', async () => { + const [req1, poprfClient] = await signatureRequest() + + const res1 = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req1) + expect(res1.status).toBe(200) + expect(res1.body).toStrictEqual({ + success: true, + version: res1.body.version, + signature: res1.body.signature, + status: { + disabled: false, + counter: 1, + timer: res1.body.status.timer, + now: res1.body.status.now, + }, + }) + const eval1 = poprfClient.unblindResponse(Buffer.from(res1.body.signature, 'base64')) + expect(eval1.toString('base64')).toEqual(expectedEval) + + // submit identical request with nonce set to 1 + req1.options.nonce = defined(1) + req1.options.signature = noString + req1.options.signature = defined( + await wallet.signTypedData(walletAddress, domainRestrictedSignatureRequestEIP712(req1)) + ) + const res2 = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req1) + + expect(res2.status).toBe(200) + expect(res2.body).toStrictEqual({ + success: true, + version: res2.body.version, + signature: res2.body.signature, + status: { + disabled: false, + counter: 2, + timer: res2.body.status.timer, + now: res2.body.status.now, + }, + }) + const eval2 = poprfClient.unblindResponse(Buffer.from(res1.body.signature, 'base64')) + expect(eval2).toEqual(eval1) + }) - for (let i = 1; i <= 3; i++) { - it(`Should respond with 200 on valid request with key version header ${i}`, async () => { - const [req, poprfClient] = await signatureRequest(undefined, undefined, i) + it('Should respond with 200 on extra request fields', async () => { + const [req, poprfClient] = await signatureRequest() + // @ts-ignore Intentionally adding an extra field to the request type + req.options.extraField = noString - const res = await request(app) - .post(CombinerEndpoint.DOMAIN_SIGN) - .set(KEY_VERSION_HEADER, i.toString()) - .send(req) + const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req) expect(res.status).toBe(200) expect(res.body).toStrictEqual({ @@ -604,304 +699,417 @@ describe('domainService', () => { }, }) const evaluation = poprfClient.unblindResponse(Buffer.from(res.body.signature, 'base64')) - expect(evaluation.toString('base64')).toEqual(expectedEvals[i - 1]) + expect(evaluation.toString('base64')).toEqual(expectedEval) }) - } - it('Should respond with 200 if nonce > domainState', async () => { - const [req, poprfClient] = await signatureRequest(undefined, 2) - - const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req) - - expect(res.status).toBe(200) - expect(res.body).toStrictEqual({ - success: true, - version: res.body.version, - signature: res.body.signature, - status: { - disabled: false, - counter: 1, // counter gets incremented, not set to nonce value - timer: res.body.status.timer, - now: res.body.status.now, - }, - }) - const evaluation = poprfClient.unblindResponse(Buffer.from(res.body.signature, 'base64')) - expect(evaluation.toString('base64')).toEqual(expectedEval) - }) + it('Should respond with 400 on missing request fields', async () => { + const [badRequest, _] = await signatureRequest() + // @ts-ignore Intentionally deleting required field + delete badRequest.domain.version - it('Should respond with 200 on repeated valid requests', async () => { - const [req1, poprfClient] = await signatureRequest() - - const res1 = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req1) - expect(res1.status).toBe(200) - expect(res1.body).toStrictEqual({ - success: true, - version: res1.body.version, - signature: res1.body.signature, - status: { - disabled: false, - counter: 1, - timer: res1.body.status.timer, - now: res1.body.status.now, - }, - }) - const eval1 = poprfClient.unblindResponse(Buffer.from(res1.body.signature, 'base64')) - expect(eval1.toString('base64')).toEqual(expectedEval) - - // submit identical request with nonce set to 1 - req1.options.nonce = defined(1) - req1.options.signature = noString - req1.options.signature = defined( - await wallet.signTypedData(walletAddress, domainRestrictedSignatureRequestEIP712(req1)) - ) - const res2 = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req1) - - expect(res2.status).toBe(200) - expect(res2.body).toStrictEqual({ - success: true, - version: res2.body.version, - signature: res2.body.signature, - status: { - disabled: false, - counter: 2, - timer: res2.body.status.timer, - now: res2.body.status.now, - }, - }) - const eval2 = poprfClient.unblindResponse(Buffer.from(res1.body.signature, 'base64')) - expect(eval2).toEqual(eval1) - }) + const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(badRequest) - it('Should respond with 200 on extra request fields', async () => { - const [req, poprfClient] = await signatureRequest() - // @ts-ignore Intentionally adding an extra field to the request type - req.options.extraField = noString - - const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req) - - expect(res.status).toBe(200) - expect(res.body).toStrictEqual({ - success: true, - version: res.body.version, - signature: res.body.signature, - status: { - disabled: false, - counter: 1, - timer: res.body.status.timer, - now: res.body.status.now, - }, - }) - const evaluation = poprfClient.unblindResponse(Buffer.from(res.body.signature, 'base64')) - expect(evaluation.toString('base64')).toEqual(expectedEval) - }) + expect(res.status).toBe(400) + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: WarningMessage.INVALID_INPUT, + }) + }) - it('Should respond with 400 on missing request fields', async () => { - const [badRequest, _] = await signatureRequest() - // @ts-ignore Intentionally deleting required field - delete badRequest.domain.version + it('Should respond with 400 on unknown domain', async () => { + // Create a requests with an invalid domain identifier. + const [unknownRequest, _] = await signatureRequest() + // @ts-ignore UnknownDomain is (intentionally) not a valid domain identifier. + unknownRequest.domain.name = 'UnknownDomain' - const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(badRequest) + const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(unknownRequest) - expect(res.status).toBe(400) - expect(res.body).toStrictEqual({ - success: false, - version: res.body.version, - error: WarningMessage.INVALID_INPUT, + expect(res.status).toBe(400) + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: WarningMessage.INVALID_INPUT, + }) }) - }) - it('Should respond with 400 on unknown domain', async () => { - // Create a requests with an invalid domain identifier. - const [unknownRequest, _] = await signatureRequest() - // @ts-ignore UnknownDomain is (intentionally) not a valid domain identifier. - unknownRequest.domain.name = 'UnknownDomain' + it('Should respond with 400 on bad encoding', async () => { + const [badRequest1, _] = await signatureRequest() + // @ts-ignore Intentionally not JSON + badRequest1.domain = 'Freddy' + + const res1 = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(badRequest1) + + expect(res1.status).toBe(400) + expect(res1.body).toStrictEqual({ + success: false, + version: res1.body.version, + error: WarningMessage.INVALID_INPUT, + }) - const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(unknownRequest) + const badRequest2 = '' - expect(res.status).toBe(400) - expect(res.body).toStrictEqual({ - success: false, - version: res.body.version, - error: WarningMessage.INVALID_INPUT, + const res2 = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(badRequest2) + + expect(res2.status).toBe(400) + expect(res2.body).toStrictEqual({ + success: false, + version: res2.body.version, + error: WarningMessage.INVALID_INPUT, + }) }) - }) - it('Should respond with 400 on bad encoding', async () => { - const [badRequest1, _] = await signatureRequest() - // @ts-ignore Intentionally not JSON - badRequest1.domain = 'Freddy' + it('Should respond with 400 on invalid key version', async () => { + const [badRequest, _] = await signatureRequest() - const res1 = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(badRequest1) + const res = await request(app) + .post(CombinerEndpoint.DOMAIN_SIGN) + .set(KEY_VERSION_HEADER, 'a') + .send(badRequest) - expect(res1.status).toBe(400) - expect(res1.body).toStrictEqual({ - success: false, - version: res1.body.version, - error: WarningMessage.INVALID_INPUT, + expect(res.status).toBe(400) + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: WarningMessage.INVALID_KEY_VERSION_REQUEST, + }) }) - const badRequest2 = '' + it('Should respond with 400 on unsupported key version', async () => { + const [badRequest, _] = await signatureRequest() - const res2 = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(badRequest2) + const res = await request(app) + .post(CombinerEndpoint.DOMAIN_SIGN) + .set(KEY_VERSION_HEADER, '4') + .send(badRequest) - expect(res2.status).toBe(400) - expect(res2.body).toStrictEqual({ - success: false, - version: res2.body.version, - error: WarningMessage.INVALID_INPUT, + expect(res.status).toBe(400) + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: WarningMessage.INVALID_KEY_VERSION_REQUEST, + }) }) - }) - it('Should respond with 400 on invalid key version', async () => { - const [badRequest, _] = await signatureRequest() + it('Should respond with 401 on failed auth', async () => { + // Create a manipulated request, which will have a bad signature. + const [badRequest, _] = await signatureRequest() + badRequest.domain.salt = defined('badSalt') - const res = await request(app) - .post(CombinerEndpoint.DOMAIN_SIGN) - .set(KEY_VERSION_HEADER, 'a') - .send(badRequest) + const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(badRequest) - expect(res.status).toBe(400) - expect(res.body).toStrictEqual({ - success: false, - version: res.body.version, - error: WarningMessage.INVALID_KEY_VERSION_REQUEST, + expect(res.status).toBe(401) + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: WarningMessage.UNAUTHENTICATED_USER, + }) + }) + + it('Should respond with 401 on invalid nonce', async () => { + // Request must be sent first since nonce check is >= 0 + const [req1, _] = await signatureRequest() + const res1 = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req1) + + expect(res1.status).toBe(200) + expect(res1.body).toStrictEqual({ + success: true, + version: res1.body.version, + signature: res1.body.signature, + status: { + disabled: false, + counter: 1, + timer: res1.body.status.timer, + now: res1.body.status.now, + }, + }) + const res2 = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req1) + expect(res2.status).toBe(401) + expect(res2.body).toStrictEqual({ + success: false, + version: res2.body.version, + error: WarningMessage.INVALID_NONCE, + }) }) - }) - it('Should respond with 400 on invalid key version', async () => { - const [badRequest, _] = await signatureRequest() + it('Should respond with 429 on out of quota', async () => { + const noQuotaDomain = authenticatedDomain([ + { delay: 0, resetTimer: noBool, batchSize: defined(0), repetitions: defined(0) }, + ]) + const [badRequest, _] = await signatureRequest(noQuotaDomain) - const res = await request(app) - .post(CombinerEndpoint.DOMAIN_SIGN) - .set(KEY_VERSION_HEADER, '4') - .send(badRequest) + const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(badRequest) - expect(res.status).toBe(400) - expect(res.body).toStrictEqual({ - success: false, - version: res.body.version, - error: WarningMessage.INVALID_KEY_VERSION_REQUEST, + expect(res.status).toBe(429) + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: WarningMessage.EXCEEDED_QUOTA, + }) }) - }) - it('Should respond with 401 on failed auth', async () => { - // Create a manipulated request, which will have a bad signature. - const [badRequest, _] = await signatureRequest() - badRequest.domain.salt = defined('badSalt') + it('Should respond with 429 on request too early', async () => { + // This domain won't accept requests until ~10 seconds after test execution + const noQuotaDomain = authenticatedDomain([ + { + delay: Math.floor(Date.now() / 1000) + 10, + resetTimer: noBool, + batchSize: defined(2), + repetitions: defined(1), + }, + ]) + const [badRequest, _] = await signatureRequest(noQuotaDomain) + + const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(badRequest) + + expect(res.status).toBe(429) + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: WarningMessage.EXCEEDED_QUOTA, + }) + }) - const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(badRequest) + it('Should respond with 429 when requesting a signature from a disabled domain', async () => { + const testDomain = authenticatedDomain() + const resDisable = await request(app) + .post(CombinerEndpoint.DISABLE_DOMAIN) + .send(await disableRequest(testDomain)) + expect(resDisable.status).toBe(200) + expect(resDisable.body).toStrictEqual({ + success: true, + version: resDisable.body.version, + status: { disabled: true, counter: 0, timer: 0, now: resDisable.body.status.now }, + }) + }) - expect(res.status).toBe(401) - expect(res.body).toStrictEqual({ - success: false, - version: res.body.version, - error: WarningMessage.UNAUTHENTICATED_USER, + it('Should respond with 503 on disabled api', async () => { + const configWithApiDisabled: typeof combinerConfig = JSON.parse( + JSON.stringify(combinerConfig) + ) + configWithApiDisabled.domains.enabled = false + const appWithApiDisabled = startCombiner(configWithApiDisabled) + + const [req, _] = await signatureRequest() + + const res = await request(appWithApiDisabled).post(CombinerEndpoint.DOMAIN_SIGN).send(req) + + expect(res.status).toBe(503) + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: WarningMessage.API_UNAVAILABLE, + }) }) }) + }) + + describe('when signers are not operating correctly', () => { + // In this case (1/3 signers are correct), response unblinding is guaranteed to fail + // Testing 2/3 signers is flaky since the combiner sometimes combines two + // correct signatures and returns, and sometimes combines one wrong/one correct + // since it cannot verify the sigs server-side. + describe('when 1/3 signers return correct signatures', () => { + beforeEach(async () => { + // Signer 1 & 2's v1 keys are misconfigured to point to the v3 share + const badKeyProvider1 = new MockKeyProvider( + new Map([[`${DefaultKeyName.DOMAINS}-1`, DOMAINS_THRESHOLD_DEV_PK_SHARE_1_V3]]) + ) + const badKeyProvider2 = new MockKeyProvider( + new Map([[`${DefaultKeyName.DOMAINS}-1`, DOMAINS_THRESHOLD_DEV_PK_SHARE_2_V3]]) + ) + signer1 = startSigner(signerConfig, signerDB1, badKeyProvider1).listen(3001) + signer2 = startSigner(signerConfig, signerDB2, badKeyProvider2).listen(3002) + signer3 = startSigner(signerConfig, signerDB3, keyProvider3).listen(3003) + }) - it('Should respond with 401 on invalid nonce', async () => { - // Request must be sent first since nonce check is >= 0 - const [req1, _] = await signatureRequest() - const res1 = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req1) - - expect(res1.status).toBe(200) - expect(res1.body).toStrictEqual({ - success: true, - version: res1.body.version, - signature: res1.body.signature, - status: { - disabled: false, - counter: 1, - timer: res1.body.status.timer, - now: res1.body.status.now, - }, - }) - const res2 = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req1) - expect(res2.status).toBe(401) - expect(res2.body).toStrictEqual({ - success: false, - version: res2.body.version, - error: WarningMessage.INVALID_NONCE, + describe(`${CombinerEndpoint.DOMAIN_SIGN}`, () => { + it('Should respond with 200 on valid request', async () => { + // Ensure requested keyVersion is one that signer1 does not have + const [req, poprfClient] = await signatureRequest(undefined, undefined, 1) + const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req) + + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: res.body.version, + signature: res.body.signature, + status: { + disabled: false, + counter: 1, + timer: res.body.status.timer, + now: res.body.status.now, + }, + }) + expect(() => + poprfClient.unblindResponse(Buffer.from(res.body.signature, 'base64')) + ).toThrow(/verification failed/) + }) }) }) - it('Should respond with 429 on out of quota', async () => { - const noQuotaDomain = authenticatedDomain([ - { delay: 0, resetTimer: noBool, batchSize: defined(0), repetitions: defined(0) }, - ]) - const [badRequest, _] = await signatureRequest(noQuotaDomain) + describe('when 2/3 of signers are disabled', () => { + beforeEach(async () => { + const configWithApiDisabled: SignerConfig = JSON.parse(JSON.stringify(signerConfig)) + configWithApiDisabled.api.domains.enabled = false + signer1 = startSigner(signerConfig, signerDB1, keyProvider1).listen(3001) + signer2 = startSigner(configWithApiDisabled, signerDB2, keyProvider2).listen(3002) + signer3 = startSigner(configWithApiDisabled, signerDB3, keyProvider3).listen(3003) + }) - const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(badRequest) + describe(`${CombinerEndpoint.DISABLE_DOMAIN}`, () => { + it('Should fail to reach threshold of signers on valid request', async () => { + const res = await request(app) + .post(CombinerEndpoint.DISABLE_DOMAIN) + .send(await disableRequest()) + expect(res.status).toBe(503) // majority error code in this case + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: ErrorMessage.THRESHOLD_DISABLE_DOMAIN_FAILURE, + }) + }) + }) + + describe(`${CombinerEndpoint.DOMAIN_QUOTA_STATUS}`, () => { + it('Should fail to reach threshold of signers on valid request', async () => { + const res = await request(app) + .post(CombinerEndpoint.DOMAIN_QUOTA_STATUS) + .send(await quotaRequest()) + expect(res.status).toBe(503) // majority error code in this case + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: ErrorMessage.THRESHOLD_DOMAIN_QUOTA_STATUS_FAILURE, + }) + }) + }) - expect(res.status).toBe(429) - expect(res.body).toStrictEqual({ - success: false, - version: res.body.version, - error: WarningMessage.EXCEEDED_QUOTA, + describe(`${CombinerEndpoint.DOMAIN_SIGN}`, () => { + it('Should fail to reach threshold of signers on valid request', async () => { + const [req, _] = await signatureRequest() + const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req) + expect(res.status).toBe(503) // majority error code in this case + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: ErrorMessage.NOT_ENOUGH_PARTIAL_SIGNATURES, + }) + }) }) }) - it('Should respond with 429 on request too early', async () => { - // This domain won't accept requests until ~10 seconds after test execution - const noQuotaDomain = authenticatedDomain([ - { - delay: Math.floor(Date.now() / 1000) + 10, - resetTimer: noBool, - batchSize: defined(2), - repetitions: defined(1), - }, - ]) - const [badRequest, _] = await signatureRequest(noQuotaDomain) + describe('when 1/3 of signers are disabled', () => { + beforeEach(async () => { + const configWithApiDisabled: SignerConfig = JSON.parse(JSON.stringify(signerConfig)) + configWithApiDisabled.api.domains.enabled = false + signer1 = startSigner(signerConfig, signerDB1, keyProvider1).listen(3001) + signer2 = startSigner(signerConfig, signerDB2, keyProvider2).listen(3002) + signer3 = startSigner(configWithApiDisabled, signerDB3, keyProvider3).listen(3003) + }) - const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(badRequest) + describe(`${CombinerEndpoint.DISABLE_DOMAIN}`, () => { + it('Should respond with 200 on valid request', async () => { + const res = await request(app) + .post(CombinerEndpoint.DISABLE_DOMAIN) + .send(await disableRequest()) + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: res.body.version, + status: { disabled: true, counter: 0, timer: 0, now: res.body.status.now }, + }) + }) + }) - expect(res.status).toBe(429) - expect(res.body).toStrictEqual({ - success: false, - version: res.body.version, - error: WarningMessage.EXCEEDED_QUOTA, + describe(`${CombinerEndpoint.DOMAIN_QUOTA_STATUS}`, () => { + it('Should respond with 200 on valid request', async () => { + const res = await request(app) + .post(CombinerEndpoint.DOMAIN_QUOTA_STATUS) + .send(await quotaRequest()) + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: res.body.version, + status: { disabled: false, counter: 0, timer: 0, now: res.body.status.now }, + }) + }) }) - }) - it('Should respond with 429 when requesting a signature from a disabled domain', async () => { - const testDomain = authenticatedDomain() - const resDisable = await request(app) - .post(CombinerEndpoint.DISABLE_DOMAIN) - .send(await disableRequest(testDomain)) - expect(resDisable.status).toBe(200) - expect(resDisable.body).toStrictEqual({ - success: true, - version: resDisable.body.version, - status: { disabled: true, counter: 0, timer: 0, now: resDisable.body.status.now }, - }) - - const [req, _] = await signatureRequest(testDomain) - const resSig = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req) - expect(resSig.status).toBe(429) - expect(resSig.body).toStrictEqual({ - success: false, - version: resSig.body.version, - error: WarningMessage.EXCEEDED_QUOTA, + describe(`${CombinerEndpoint.DOMAIN_SIGN}`, () => { + it('Should respond with 200 on valid request', async () => { + const [req, poprfClient] = await signatureRequest() + const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req) + + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: res.body.version, + signature: res.body.signature, + status: { + disabled: false, + counter: 1, + timer: res.body.status.timer, + now: res.body.status.now, + }, + }) + const evaluation = poprfClient.unblindResponse(Buffer.from(res.body.signature, 'base64')) + expect(evaluation.toString('base64')).toEqual(expectedEval) + }) }) }) - it('Should respond with 503 on disabled api', async () => { - const configWithApiDisabled: typeof combinerConfig = JSON.parse( - JSON.stringify(combinerConfig) - ) - configWithApiDisabled.domains.enabled = false - const appWithApiDisabled = startCombiner(configWithApiDisabled) + describe('when signers timeout', () => { + beforeEach(async () => { + const testTimeoutMS = 0 - const [req, _] = await signatureRequest() + const configWithShortTimeout: SignerConfig = JSON.parse(JSON.stringify(signerConfig)) + configWithShortTimeout.timeout = testTimeoutMS + // Test this with all signers timing out to decrease possibility of race conditions + signer1 = startSigner(configWithShortTimeout, signerDB1, keyProvider1).listen(3001) + signer2 = startSigner(configWithShortTimeout, signerDB2, keyProvider2).listen(3002) + signer3 = startSigner(configWithShortTimeout, signerDB3, keyProvider3).listen(3003) + }) - const res = await request(appWithApiDisabled).post(CombinerEndpoint.DOMAIN_SIGN).send(req) + describe(`${CombinerEndpoint.DISABLE_DOMAIN}`, () => { + it('Should fail to reach threshold of signers on valid request', async () => { + const res = await request(app) + .post(CombinerEndpoint.DISABLE_DOMAIN) + .send(await disableRequest()) + expect(res.status).toBe(500) // majority error code in this case + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: ErrorMessage.THRESHOLD_DISABLE_DOMAIN_FAILURE, + }) + }) + }) + + describe(`${CombinerEndpoint.DOMAIN_QUOTA_STATUS}`, () => { + it('Should fail to reach threshold of signers on valid request', async () => { + const res = await request(app) + .post(CombinerEndpoint.DOMAIN_QUOTA_STATUS) + .send(await quotaRequest()) + expect(res.status).toBe(500) // majority error code in this case + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: ErrorMessage.THRESHOLD_DOMAIN_QUOTA_STATUS_FAILURE, + }) + }) + }) - expect(res.status).toBe(503) - // @ts-ignore res.body.status is expected to be undefined - expect(res.body).toStrictEqual({ - success: false, - version: res.body.version, - error: WarningMessage.API_UNAVAILABLE, + describe(`${CombinerEndpoint.DOMAIN_SIGN}`, () => { + it('Should fail to reach threshold of signers on valid request', async () => { + const [req, _] = await signatureRequest() + const res = await request(app).post(CombinerEndpoint.DOMAIN_SIGN).send(req) + expect(res.status).toBe(500) // majority error code in this case + expect(res.body).toStrictEqual({ + success: false, + version: res.body.version, + error: ErrorMessage.NOT_ENOUGH_PARTIAL_SIGNATURES, + }) + }) }) }) }) diff --git a/packages/phone-number-privacy/combiner/test/integration/legacypnp.test.ts b/packages/phone-number-privacy/combiner/test/integration/legacypnp.test.ts index 414923c2ce7..8eba868383d 100644 --- a/packages/phone-number-privacy/combiner/test/integration/legacypnp.test.ts +++ b/packages/phone-number-privacy/combiner/test/integration/legacypnp.test.ts @@ -172,7 +172,7 @@ jest.mock('@celo/contractkit', () => ({ newKit: jest.fn().mockImplementation(() => mockContractKit), })) -describe('legacyPnpService', () => { +describe(`legacyPnpService: ${CombinerEndpoint.LEGACY_PNP_SIGN}`, () => { let keyProvider1: KeyProvider let keyProvider2: KeyProvider let keyProvider3: KeyProvider @@ -192,6 +192,7 @@ describe('legacyPnpService', () => { const expectedVersion = getVersion() const message = Buffer.from('test message', 'utf8') + const expectedQuota = 410 const expectedSignatures: string[] = [ 'xgFMQtcgAMHJAEX/m9B4VFopYtxqPFSw0024sWzRYvQDvnmFqhXOPdnRDfa8WCEA', 'wUuFV8yFBXGyEzKbyWjBChG6dER264nwjOsqErd/UZieVKE0oDMZcMDG+qObu4QB', @@ -210,55 +211,6 @@ describe('legacyPnpService', () => { // In current setup, the same mocked kit is used for the combiner and signers const mockKit = newKit('dummyKit') - beforeAll(async () => { - keyProvider1 = new MockKeyProvider( - new Map([ - [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, PNP_THRESHOLD_DEV_PK_SHARE_1_V1], - [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-2`, PNP_THRESHOLD_DEV_PK_SHARE_1_V2], - [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-3`, PNP_THRESHOLD_DEV_PK_SHARE_1_V3], - ]) - ) - keyProvider2 = new MockKeyProvider( - new Map([ - [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, PNP_THRESHOLD_DEV_PK_SHARE_2_V1], - [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-2`, PNP_THRESHOLD_DEV_PK_SHARE_2_V2], - [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-3`, PNP_THRESHOLD_DEV_PK_SHARE_2_V3], - ]) - ) - keyProvider3 = new MockKeyProvider( - new Map([ - [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, PNP_THRESHOLD_DEV_PK_SHARE_3_V1], - [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-2`, PNP_THRESHOLD_DEV_PK_SHARE_3_V2], - [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-3`, PNP_THRESHOLD_DEV_PK_SHARE_3_V3], - ]) - ) - - app = startCombiner(combinerConfig, mockKit) - }) - - beforeEach(async () => { - signerDB1 = await initSignerDatabase(signerConfig, signerMigrationsPath) - signerDB2 = await initSignerDatabase(signerConfig, signerMigrationsPath) - signerDB3 = await initSignerDatabase(signerConfig, signerMigrationsPath) - - // this needs to be defined here to avoid errors - userSeed = new Uint8Array(32) - for (let i = 0; i < userSeed.length - 1; i++) { - userSeed[i] = i - } - - blindedMsgResult = threshold_bls.blind(message, userSeed) - }) - - afterEach(async () => { - await signerDB1?.destroy() - await signerDB2?.destroy() - await signerDB3?.destroy() - signer1?.close() - signer2?.close() - signer3?.close() - }) - const sendLegacyPnpSignRequest = async ( req: LegacySignMessageRequest, authorization: string, @@ -307,32 +259,99 @@ describe('legacyPnpService', () => { mockGetDataEncryptionKey.mockReturnValue(DEK_PUBLIC_KEY) mockGetWalletAddress.mockReturnValue(mockAccount) } - const expectedQuota = 410 - describe('when all signers return correct signatures', () => { + beforeAll(async () => { + keyProvider1 = new MockKeyProvider( + new Map([ + [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, PNP_THRESHOLD_DEV_PK_SHARE_1_V1], + [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-2`, PNP_THRESHOLD_DEV_PK_SHARE_1_V2], + [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-3`, PNP_THRESHOLD_DEV_PK_SHARE_1_V3], + ]) + ) + keyProvider2 = new MockKeyProvider( + new Map([ + [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, PNP_THRESHOLD_DEV_PK_SHARE_2_V1], + [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-2`, PNP_THRESHOLD_DEV_PK_SHARE_2_V2], + [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-3`, PNP_THRESHOLD_DEV_PK_SHARE_2_V3], + ]) + ) + keyProvider3 = new MockKeyProvider( + new Map([ + [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, PNP_THRESHOLD_DEV_PK_SHARE_3_V1], + [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-2`, PNP_THRESHOLD_DEV_PK_SHARE_3_V2], + [`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-3`, PNP_THRESHOLD_DEV_PK_SHARE_3_V3], + ]) + ) + + app = startCombiner(combinerConfig, mockKit) + }) + + let req: LegacySignMessageRequest + beforeEach(async () => { + signerDB1 = await initSignerDatabase(signerConfig, signerMigrationsPath) + signerDB2 = await initSignerDatabase(signerConfig, signerMigrationsPath) + signerDB3 = await initSignerDatabase(signerConfig, signerMigrationsPath) + + // this needs to be defined here to avoid errors + userSeed = new Uint8Array(32) + for (let i = 0; i < userSeed.length - 1; i++) { + userSeed[i] = i + } + + blindedMsgResult = threshold_bls.blind(message, userSeed) + + req = getLegacySignRequest(blindedMsgResult) + prepMocks(true) + }) + + afterEach(async () => { + await signerDB1?.destroy() + await signerDB2?.destroy() + await signerDB3?.destroy() + signer1?.close() + signer2?.close() + signer3?.close() + }) + + describe('when signers are operating correctly', () => { beforeEach(async () => { signer1 = startSigner(signerConfig, signerDB1, keyProvider1, mockKit).listen(3001) signer2 = startSigner(signerConfig, signerDB2, keyProvider2, mockKit).listen(3002) signer3 = startSigner(signerConfig, signerDB3, keyProvider3, mockKit).listen(3003) }) - describe(`${CombinerEndpoint.LEGACY_PNP_SIGN}`, () => { - let req: LegacySignMessageRequest - - beforeEach(async () => { - req = getLegacySignRequest(blindedMsgResult) - prepMocks(true) + it('Should respond with 200 on valid request', async () => { + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await sendLegacyPnpSignRequest(req, authorization, app) + + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: expectedVersion, + signature: expectedSignature, + performedQueryCount: 1, + totalQuota: expectedQuota, + blockNumber: testBlockNumber, + warnings: [], }) + const unblindedSig = threshold_bls.unblind( + Buffer.from(res.body.signature, 'base64'), + blindedMsgResult.blindingFactor + ) - it('Should respond with 200 on valid request', async () => { + expect(Buffer.from(unblindedSig).toString('base64')).toEqual(expectedUnblindedSig) + }) + + for (let i = 1; i <= 3; i++) { + it(`Should respond with 200 on valid request with key version header ${i}`, async () => { const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await sendLegacyPnpSignRequest(req, authorization, app) + const res = await sendLegacyPnpSignRequest(req, authorization, app, i.toString()) expect(res.status).toBe(200) expect(res.body).toStrictEqual({ success: true, version: expectedVersion, - signature: expectedSignature, + signature: expectedSignatures[i - 1], performedQueryCount: 1, totalQuota: expectedQuota, blockNumber: testBlockNumber, @@ -343,126 +362,276 @@ describe('legacyPnpService', () => { blindedMsgResult.blindingFactor ) - expect(Buffer.from(unblindedSig).toString('base64')).toEqual(expectedUnblindedSig) + expect(Buffer.from(unblindedSig).toString('base64')).toEqual(expectedUnblindedSigs[i - 1]) }) + } - for (let i = 1; i <= 3; i++) { - it(`Should respond with 200 on valid request with key version header ${i}`, async () => { - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await sendLegacyPnpSignRequest(req, authorization, app, i.toString()) - - expect(res.status).toBe(200) - expect(res.body).toStrictEqual({ - success: true, - version: expectedVersion, - signature: expectedSignatures[i - 1], - performedQueryCount: 1, - totalQuota: expectedQuota, - blockNumber: testBlockNumber, - warnings: [], - }) - const unblindedSig = threshold_bls.unblind( - Buffer.from(res.body.signature, 'base64'), - blindedMsgResult.blindingFactor - ) - - expect(Buffer.from(unblindedSig).toString('base64')).toEqual(expectedUnblindedSigs[i - 1]) - }) + it('Should respond with 200 on valid request with identifier', async () => { + // Ensure that this gets passed through the combiner to the signer + req.hashedPhoneNumber = IDENTIFIER + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await sendLegacyPnpSignRequest(req, authorization, app) + + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: expectedVersion, + signature: expectedSignature, + performedQueryCount: 1, + totalQuota: 440, // Additional quota gets unlocked with an identifier + blockNumber: testBlockNumber, + warnings: [], + }) + }) + + it('Should respond with 200 on repeated valid requests', async () => { + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res1 = await sendLegacyPnpSignRequest(req, authorization, app) + + expect(res1.status).toBe(200) + expect(res1.body).toStrictEqual({ + success: true, + version: expectedVersion, + signature: expectedSignature, + performedQueryCount: 1, + totalQuota: expectedQuota, + blockNumber: testBlockNumber, + warnings: [], + }) + + // performedQueryCount should remain the same; same request should not + // consume any quota + const res2 = await sendLegacyPnpSignRequest(req, authorization, app) + expect(res2.status).toBe(200) + expect(res2.body).toStrictEqual(res1.body) + }) + + it('Should increment performedQueryCount on request from the same account with a new message', async () => { + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res1 = await sendLegacyPnpSignRequest(req, authorization, app) + + const expectedResponse: SignMessageResponseSuccess = { + success: true, + version: expectedVersion, + signature: expectedSignature, + performedQueryCount: 1, + totalQuota: expectedQuota, + blockNumber: testBlockNumber, + warnings: [], } - it('Should respond with 200 on valid request with identifier', async () => { - // Ensure that this gets passed through the combiner to the signer - req.hashedPhoneNumber = IDENTIFIER - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await sendLegacyPnpSignRequest(req, authorization, app) + expect(res1.status).toBe(200) + expect(res1.body).toStrictEqual(expectedResponse) + + // Second request for the same account but with new message + const message2 = Buffer.from('second test message', 'utf8') + const blindedMsg2 = threshold_bls.blind(message2, userSeed) + const req2 = getLegacySignRequest(blindedMsg2) + const authorization2 = getPnpRequestAuthorization(req2, PRIVATE_KEY1) + + // Expect performedQueryCount to increase + expectedResponse.performedQueryCount++ + expectedResponse.signature = + 'PWvuSYIA249x1dx+qzgl6PKSkoulXXE/P4WHJvGmtw77pCRilEWTn3xSp+6JS9+A' + const res2 = await sendLegacyPnpSignRequest(req2, authorization2, app) + expect(res2.status).toBe(200) + expect(res2.body).toStrictEqual(expectedResponse) + }) - expect(res.status).toBe(200) - expect(res.body).toStrictEqual({ - success: true, - version: expectedVersion, - signature: expectedSignature, - performedQueryCount: 1, - totalQuota: 440, // Additional quota gets unlocked with an identifier - blockNumber: testBlockNumber, - warnings: [], - }) + it('Should respond with 200 on extra request fields', async () => { + // @ts-ignore Intentionally adding an extra field to the request type + req.extraField = 'dummyString' + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await sendLegacyPnpSignRequest(req, authorization, app) + + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: expectedVersion, + signature: expectedSignature, + performedQueryCount: 1, + totalQuota: expectedQuota, + blockNumber: testBlockNumber, + warnings: [], }) + }) - it('Should respond with 200 on repeated valid requests', async () => { - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res1 = await sendLegacyPnpSignRequest(req, authorization, app) + it('Should respond with 200 when authenticated with DEK', async () => { + req.authenticationMethod = AuthenticationMethod.ENCRYPTION_KEY + const authorization = getPnpRequestAuthorization(req, DEK_PRIVATE_KEY) + const res = await sendLegacyPnpSignRequest(req, authorization, app) + + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: expectedVersion, + signature: expectedSignature, + performedQueryCount: 1, + totalQuota: expectedQuota, + blockNumber: testBlockNumber, + warnings: [], + }) + }) - expect(res1.status).toBe(200) - expect(res1.body).toStrictEqual({ - success: true, - version: expectedVersion, - signature: expectedSignature, - performedQueryCount: 1, - totalQuota: expectedQuota, - blockNumber: testBlockNumber, - warnings: [], - }) + it('Should get the same unblinded signatures from the same message (different seed)', async () => { + const authorization1 = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res1 = await sendLegacyPnpSignRequest(req, authorization1, app) + + expect(res1.status).toBe(200) + expect(res1.body).toStrictEqual({ + success: true, + version: expectedVersion, + signature: expectedSignature, + performedQueryCount: 1, + totalQuota: expectedQuota, + blockNumber: testBlockNumber, + warnings: [], + }) - // performedQueryCount should remain the same; same request should not - // consume any quota - const res2 = await sendLegacyPnpSignRequest(req, authorization, app) - expect(res2.status).toBe(200) - expect(res2.body).toStrictEqual(res1.body) + const secondUserSeed = new Uint8Array(userSeed) + secondUserSeed[0]++ + // Ensure message is identical except for message + const req2 = { ...req } + const blindedMsgResult2 = threshold_bls.blind(message, secondUserSeed) + req2.blindedQueryPhoneNumber = Buffer.from(blindedMsgResult2.message).toString('base64') + + // Sanity check + expect(req2.blindedQueryPhoneNumber).not.toEqual(req.blindedQueryPhoneNumber) + + const authorization2 = getPnpRequestAuthorization(req2, PRIVATE_KEY1) + const res2 = await sendLegacyPnpSignRequest(req2, authorization2, app) + expect(res2.status).toBe(200) + const unblindedSig1 = threshold_bls.unblind( + Buffer.from(res1.body.signature, 'base64'), + blindedMsgResult.blindingFactor + ) + const unblindedSig2 = threshold_bls.unblind( + Buffer.from(res2.body.signature, 'base64'), + blindedMsgResult2.blindingFactor + ) + expect(Buffer.from(unblindedSig1).toString('base64')).toEqual(expectedUnblindedSig) + expect(unblindedSig1).toEqual(unblindedSig2) + }) + + it('Should respond with 400 on missing request fields', async () => { + // @ts-ignore Intentionally deleting required field + delete req.account + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await sendLegacyPnpSignRequest(req, authorization, app) + + expect(res.status).toBe(400) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: WarningMessage.INVALID_INPUT, }) + }) - it('Should increment performedQueryCount on request from the same account with a new message', async () => { - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res1 = await sendLegacyPnpSignRequest(req, authorization, app) + it('Should respond with 400 on invalid key version', async () => { + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await sendLegacyPnpSignRequest(req, authorization, app, 'a') + expect(res.status).toBe(400) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: WarningMessage.INVALID_KEY_VERSION_REQUEST, + }) + }) - const expectedResponse: SignMessageResponseSuccess = { - success: true, - version: expectedVersion, - signature: expectedSignature, - performedQueryCount: 1, - totalQuota: expectedQuota, - blockNumber: testBlockNumber, - warnings: [], - } - - expect(res1.status).toBe(200) - expect(res1.body).toStrictEqual(expectedResponse) - - // Second request for the same account but with new message - const message2 = Buffer.from('second test message', 'utf8') - const blindedMsg2 = threshold_bls.blind(message2, userSeed) - const req2 = getLegacySignRequest(blindedMsg2) - const authorization2 = getPnpRequestAuthorization(req2, PRIVATE_KEY1) - - // Expect performedQueryCount to increase - expectedResponse.performedQueryCount++ - expectedResponse.signature = - 'PWvuSYIA249x1dx+qzgl6PKSkoulXXE/P4WHJvGmtw77pCRilEWTn3xSp+6JS9+A' - const res2 = await sendLegacyPnpSignRequest(req2, authorization2, app) - expect(res2.status).toBe(200) - expect(res2.body).toStrictEqual(expectedResponse) + it('Should respond with 400 on unsupported key version', async () => { + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await sendLegacyPnpSignRequest(req, authorization, app, '4') + expect(res.status).toBe(400) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: WarningMessage.INVALID_KEY_VERSION_REQUEST, }) + }) - it('Should respond with 200 on extra request fields', async () => { - // @ts-ignore Intentionally adding an extra field to the request type - req.extraField = 'dummyString' - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await sendLegacyPnpSignRequest(req, authorization, app) + it('Should respond with 400 on request with invalid identifier', async () => { + // Ensure that this gets passed through the combiner to the signer + req.hashedPhoneNumber = '+1234567890' + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await sendLegacyPnpSignRequest(req, authorization, app) + + expect(res.status).toBe(400) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: WarningMessage.INVALID_INPUT, + }) + }) - expect(res.status).toBe(200) - expect(res.body).toStrictEqual({ - success: true, - version: expectedVersion, - signature: expectedSignature, - performedQueryCount: 1, - totalQuota: expectedQuota, - blockNumber: testBlockNumber, - warnings: [], - }) + it('Should respond with 401 on failed WALLET_KEY auth', async () => { + req.account = mockAccount + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await sendLegacyPnpSignRequest(req, authorization, app) + + expect(res.status).toBe(401) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: WarningMessage.UNAUTHENTICATED_USER, + }) + }) + + it('Should respond with 401 on failed DEK auth', async () => { + req.account = mockAccount + req.authenticationMethod = AuthenticationMethod.ENCRYPTION_KEY + const differentPk = '0x00000000000000000000000000000000000000000000000000000000ddddbbbb' + const authorization = getPnpRequestAuthorization(req, differentPk) + const res = await sendLegacyPnpSignRequest(req, authorization, app) + + expect(res.status).toBe(401) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: WarningMessage.UNAUTHENTICATED_USER, }) + }) + + it('Should respond with 403 on out of quota', async () => { + prepMocks(false) + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await sendLegacyPnpSignRequest(req, authorization, app) + + expect(res.status).toBe(403) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: WarningMessage.EXCEEDED_QUOTA, + }) + }) + + it('Should respond with 503 on disabled api', async () => { + const configWithApiDisabled: typeof combinerConfig = JSON.parse( + JSON.stringify(combinerConfig) + ) + configWithApiDisabled.phoneNumberPrivacy.enabled = false + const appWithApiDisabled = startCombiner(configWithApiDisabled) + + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await sendLegacyPnpSignRequest(req, authorization, appWithApiDisabled) + + expect(res.status).toBe(503) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: WarningMessage.API_UNAVAILABLE, + }) + }) + + describe('functionality in case of errors', () => { + it('Should respond with 200 on failure to fetch DEK', async () => { + mockGetDataEncryptionKey.mockImplementation(() => { + throw new Error() + }) - it('Should respond with 200 when authenticated with DEK', async () => { + // Would fail authentication if getDataEncryptionKey succeeded + const differentPk = '0x00000000000000000000000000000000000000000000000000000000ddddbbbb' req.authenticationMethod = AuthenticationMethod.ENCRYPTION_KEY - const authorization = getPnpRequestAuthorization(req, DEK_PRIVATE_KEY) + const authorization = getPnpRequestAuthorization(req, differentPk) const res = await sendLegacyPnpSignRequest(req, authorization, app) expect(res.status).toBe(200) @@ -475,14 +644,41 @@ describe('legacyPnpService', () => { blockNumber: testBlockNumber, warnings: [], }) + const unblindedSig = threshold_bls.unblind( + Buffer.from(res.body.signature, 'base64'), + blindedMsgResult.blindingFactor + ) + expect(Buffer.from(unblindedSig).toString('base64')).toEqual(expectedUnblindedSig) + }) + }) + }) + + // For testing combiner code paths when signers do not behave as expected + describe('when signers are not operating correctly', () => { + let authorization: string + + beforeEach(() => { + authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + }) + + describe('when 2/3 signers return correct signatures', () => { + beforeEach(async () => { + const badBlsShare1 = + '000000002e50aa714ef6b865b5de89c56969ef9f8f27b6b0a6d157c9cc01c574ac9df604' + const badKeyProvider1 = new MockKeyProvider( + new Map([[`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, badBlsShare1]]) + ) + + signer1 = startSigner(signerConfig, signerDB1, badKeyProvider1, mockKit).listen(3001) + signer2 = startSigner(signerConfig, signerDB2, keyProvider2, mockKit).listen(3002) + signer3 = startSigner(signerConfig, signerDB3, keyProvider3, mockKit).listen(3003) }) - it('Should get the same unblinded signatures from the same message (different seed)', async () => { - const authorization1 = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res1 = await sendLegacyPnpSignRequest(req, authorization1, app) + it('Should respond with 200 on valid request', async () => { + const res = await sendLegacyPnpSignRequest(req, authorization, app) - expect(res1.status).toBe(200) - expect(res1.body).toStrictEqual({ + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ success: true, version: expectedVersion, signature: expectedSignature, @@ -491,200 +687,77 @@ describe('legacyPnpService', () => { blockNumber: testBlockNumber, warnings: [], }) - - const secondUserSeed = new Uint8Array(userSeed) - secondUserSeed[0]++ - // Ensure message is identical except for message - const req2 = { ...req } - const blindedMsgResult2 = threshold_bls.blind(message, secondUserSeed) - req2.blindedQueryPhoneNumber = Buffer.from(blindedMsgResult2.message).toString('base64') - - // Sanity check - expect(req2.blindedQueryPhoneNumber).not.toEqual(req.blindedQueryPhoneNumber) - - const authorization2 = getPnpRequestAuthorization(req2, PRIVATE_KEY1) - const res2 = await sendLegacyPnpSignRequest(req2, authorization2, app) - expect(res2.status).toBe(200) - const unblindedSig1 = threshold_bls.unblind( - Buffer.from(res1.body.signature, 'base64'), + const unblindedSig = threshold_bls.unblind( + Buffer.from(res.body.signature, 'base64'), blindedMsgResult.blindingFactor ) - const unblindedSig2 = threshold_bls.unblind( - Buffer.from(res2.body.signature, 'base64'), - blindedMsgResult2.blindingFactor - ) - expect(Buffer.from(unblindedSig1).toString('base64')).toEqual(expectedUnblindedSig) - expect(unblindedSig1).toEqual(unblindedSig2) - }) - - it('Should respond with 400 on missing request fields', async () => { - // @ts-ignore Intentionally deleting required field - delete req.account - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await sendLegacyPnpSignRequest(req, authorization, app) - - expect(res.status).toBe(400) - expect(res.body).toStrictEqual({ - success: false, - version: expectedVersion, - error: WarningMessage.INVALID_INPUT, - }) - }) - - it('Should respond with 400 on invalid key version', async () => { - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await sendLegacyPnpSignRequest(req, authorization, app, 'a') - expect(res.status).toBe(400) - expect(res.body).toStrictEqual({ - success: false, - version: expectedVersion, - error: WarningMessage.INVALID_KEY_VERSION_REQUEST, - }) + expect(Buffer.from(unblindedSig).toString('base64')).toEqual(expectedUnblindedSig) }) + }) - it('Should respond with 400 on invalid key version', async () => { - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await sendLegacyPnpSignRequest(req, authorization, app, '4') - expect(res.status).toBe(400) - expect(res.body).toStrictEqual({ - success: false, - version: expectedVersion, - error: WarningMessage.INVALID_KEY_VERSION_REQUEST, - }) - }) + describe('when 1/3 signers return correct signatures', () => { + beforeEach(async () => { + const badBlsShare1 = + '000000002e50aa714ef6b865b5de89c56969ef9f8f27b6b0a6d157c9cc01c574ac9df604' + const badBlsShare2 = + '01000000b8f0ef841dcf8d7bd1da5e8025e47d729eb67f513335784183b8fa227a0b9a0b' - it('Should respond with 400 on request with invalid identifier', async () => { - // Ensure that this gets passed through the combiner to the signer - req.hashedPhoneNumber = '+1234567890' - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await sendLegacyPnpSignRequest(req, authorization, app) + const badKeyProvider1 = new MockKeyProvider( + new Map([[`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, badBlsShare1]]) + ) + const badKeyProvider2 = new MockKeyProvider( + new Map([[`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, badBlsShare2]]) + ) - expect(res.status).toBe(400) - expect(res.body).toStrictEqual({ - success: false, - version: expectedVersion, - error: WarningMessage.INVALID_INPUT, - }) + signer1 = startSigner(signerConfig, signerDB1, keyProvider1, mockKit).listen(3001) + signer2 = startSigner(signerConfig, signerDB2, badKeyProvider1, mockKit).listen(3002) + signer3 = startSigner(signerConfig, signerDB3, badKeyProvider2, mockKit).listen(3003) }) - it('Should respond with 401 on failed WALLET_KEY auth', async () => { - req.account = mockAccount - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + it('Should respond with 500 even if request is valid', async () => { const res = await sendLegacyPnpSignRequest(req, authorization, app) - expect(res.status).toBe(401) + expect(res.status).toBe(500) expect(res.body).toStrictEqual({ success: false, version: expectedVersion, - error: WarningMessage.UNAUTHENTICATED_USER, + error: ErrorMessage.NOT_ENOUGH_PARTIAL_SIGNATURES, }) }) + }) - it('Should respond with 401 on failed DEK auth', async () => { - req.account = mockAccount - req.authenticationMethod = AuthenticationMethod.ENCRYPTION_KEY - const differentPk = '0x00000000000000000000000000000000000000000000000000000000ddddbbbb' - const authorization = getPnpRequestAuthorization(req, differentPk) - const res = await sendLegacyPnpSignRequest(req, authorization, app) - - expect(res.status).toBe(401) - expect(res.body).toStrictEqual({ - success: false, - version: expectedVersion, - error: WarningMessage.UNAUTHENTICATED_USER, - }) + describe('when 2/3 of signers are disabled', () => { + beforeEach(async () => { + const configWithApiDisabled: SignerConfig = JSON.parse(JSON.stringify(signerConfig)) + configWithApiDisabled.api.phoneNumberPrivacy.enabled = false + signer1 = startSigner(signerConfig, signerDB1, keyProvider1).listen(3001) + signer2 = startSigner(configWithApiDisabled, signerDB2, keyProvider2).listen(3002) + signer3 = startSigner(configWithApiDisabled, signerDB3, keyProvider3).listen(3003) }) - it('Should respond with 403 on out of quota', async () => { - prepMocks(false) - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + it('Should fail to reach threshold of signers on valid request', async () => { const res = await sendLegacyPnpSignRequest(req, authorization, app) - expect(res.status).toBe(403) - expect(res.body).toStrictEqual({ - success: false, - version: expectedVersion, - error: WarningMessage.EXCEEDED_QUOTA, - }) - }) - - it('Should respond with 503 on disabled api', async () => { - const configWithApiDisabled: typeof combinerConfig = JSON.parse( - JSON.stringify(combinerConfig) - ) - configWithApiDisabled.phoneNumberPrivacy.enabled = false - const appWithApiDisabled = startCombiner(configWithApiDisabled) - - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await sendLegacyPnpSignRequest(req, authorization, appWithApiDisabled) - - expect(res.status).toBe(503) + expect(res.status).toBe(503) // majority error code in this case expect(res.body).toStrictEqual({ success: false, version: expectedVersion, - error: WarningMessage.API_UNAVAILABLE, - }) - }) - - describe('functionality in case of errors', () => { - it('Should respond with 200 on failure to fetch DEK', async () => { - mockGetDataEncryptionKey.mockImplementation(() => { - throw new Error() - }) - - // Would fail authentication if getDataEncryptionKey succeeded - const differentPk = '0x00000000000000000000000000000000000000000000000000000000ddddbbbb' - req.authenticationMethod = AuthenticationMethod.ENCRYPTION_KEY - const authorization = getPnpRequestAuthorization(req, differentPk) - const res = await sendLegacyPnpSignRequest(req, authorization, app) - - expect(res.status).toBe(200) - expect(res.body).toStrictEqual({ - success: true, - version: expectedVersion, - signature: expectedSignature, - performedQueryCount: 1, - totalQuota: expectedQuota, - blockNumber: testBlockNumber, - warnings: [], - }) - const unblindedSig = threshold_bls.unblind( - Buffer.from(res.body.signature, 'base64'), - blindedMsgResult.blindingFactor - ) - expect(Buffer.from(unblindedSig).toString('base64')).toEqual(expectedUnblindedSig) + error: ErrorMessage.NOT_ENOUGH_PARTIAL_SIGNATURES, }) }) }) - }) - - // For testing combiner code paths when signers do not behave as expected - describe('when 2/3 signers return correct signatures', () => { - beforeEach(async () => { - const badBlsShare1 = - '000000002e50aa714ef6b865b5de89c56969ef9f8f27b6b0a6d157c9cc01c574ac9df604' - - const badKeyProvider1 = new MockKeyProvider( - new Map([[`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, badBlsShare1]]) - ) - - signer1 = startSigner(signerConfig, signerDB1, badKeyProvider1, mockKit).listen(3001) - signer2 = startSigner(signerConfig, signerDB2, keyProvider2, mockKit).listen(3002) - signer3 = startSigner(signerConfig, signerDB3, keyProvider3, mockKit).listen(3003) - }) - - describe(`${CombinerEndpoint.LEGACY_PNP_SIGN}`, () => { - let req: LegacySignMessageRequest - beforeEach(() => { - prepMocks(true) - req = getLegacySignRequest(blindedMsgResult) + describe('when 1/3 of signers are disabled', () => { + beforeEach(async () => { + const configWithApiDisabled: SignerConfig = JSON.parse(JSON.stringify(signerConfig)) + configWithApiDisabled.api.phoneNumberPrivacy.enabled = false + signer1 = startSigner(signerConfig, signerDB1, keyProvider1).listen(3001) + signer2 = startSigner(signerConfig, signerDB2, keyProvider2).listen(3002) + signer3 = startSigner(configWithApiDisabled, signerDB3, keyProvider3).listen(3003) }) it('Should respond with 200 on valid request', async () => { - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) const res = await sendLegacyPnpSignRequest(req, authorization, app) - expect(res.status).toBe(200) expect(res.body).toStrictEqual({ success: true, @@ -695,47 +768,22 @@ describe('legacyPnpService', () => { blockNumber: testBlockNumber, warnings: [], }) - const unblindedSig = threshold_bls.unblind( - Buffer.from(res.body.signature, 'base64'), - blindedMsgResult.blindingFactor - ) - expect(Buffer.from(unblindedSig).toString('base64')).toEqual(expectedUnblindedSig) }) }) - }) - - describe('when 1/3 signers return correct signatures', () => { - beforeEach(async () => { - const badBlsShare1 = - '000000002e50aa714ef6b865b5de89c56969ef9f8f27b6b0a6d157c9cc01c574ac9df604' - const badBlsShare2 = - '01000000b8f0ef841dcf8d7bd1da5e8025e47d729eb67f513335784183b8fa227a0b9a0b' - - const badKeyProvider1 = new MockKeyProvider( - new Map([[`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, badBlsShare1]]) - ) - - const badKeyProvider2 = new MockKeyProvider( - new Map([[`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, badBlsShare2]]) - ) - signer1 = startSigner(signerConfig, signerDB1, keyProvider1, mockKit).listen(3001) - signer2 = startSigner(signerConfig, signerDB2, badKeyProvider1, mockKit).listen(3002) - signer3 = startSigner(signerConfig, signerDB3, badKeyProvider2, mockKit).listen(3003) - }) - - describe(`${CombinerEndpoint.LEGACY_PNP_SIGN}`, () => { - let req: LegacySignMessageRequest - - beforeEach(() => { - req = getLegacySignRequest(blindedMsgResult) - prepMocks(true) + describe('when signers timeout', () => { + beforeEach(async () => { + const testTimeoutMS = 0 + + const configWithShortTimeout: SignerConfig = JSON.parse(JSON.stringify(signerConfig)) + configWithShortTimeout.timeout = testTimeoutMS + // Test this with all signers timing out to decrease possibility of race conditions + signer1 = startSigner(configWithShortTimeout, signerDB1, keyProvider1).listen(3001) + signer2 = startSigner(configWithShortTimeout, signerDB2, keyProvider2).listen(3002) + signer3 = startSigner(configWithShortTimeout, signerDB3, keyProvider3).listen(3003) }) - - it('Should respond with 500 even if request is valid', async () => { - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + it('Should fail to reach threshold of signers on valid request', async () => { const res = await sendLegacyPnpSignRequest(req, authorization, app) - expect(res.status).toBe(500) expect(res.body).toStrictEqual({ success: false, diff --git a/packages/phone-number-privacy/combiner/test/integration/pnp.test.ts b/packages/phone-number-privacy/combiner/test/integration/pnp.test.ts index 7aeaa5ea954..6c1b5eb26a9 100644 --- a/packages/phone-number-privacy/combiner/test/integration/pnp.test.ts +++ b/packages/phone-number-privacy/combiner/test/integration/pnp.test.ts @@ -288,17 +288,336 @@ describe('pnpService', () => { } } - // TODO(2.0.0, testing) Optionally reorganize the nesting of this file, - // since the quota endpoints don't depend on signer signature configuration, - // the sig config describes can be sub-describes under the PNP_SIGN tests. - // Part of (https://github.com/celo-org/celo-monorepo/issues/9811) - describe('when all signers return correct signatures', () => { + const useQuery = async (performedQueryCount: number, signer: Server | HttpsServer) => { + for (let i = 0; i < performedQueryCount; i++) { + const phoneNumber = '+1' + Math.floor(Math.random() * 10 ** 10) + const blindedNumber = getBlindedPhoneNumber(phoneNumber, BLINDING_FACTOR) + const req = { + account: ACCOUNT_ADDRESS1, + blindedQueryPhoneNumber: blindedNumber, + } + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + await request(signer) + .post(SignerEndpoint.PNP_SIGN) + .set('Authorization', authorization) + .send(req) + } + } + + const getCombinerQuotaResponse = async (req: PnpQuotaRequest, authorization: string) => { + const res = await request(app) + .post(CombinerEndpoint.PNP_QUOTA) + .set('Authorization', authorization) + .send(req) + return res + } + + describe('when signers are operating correctly', () => { beforeEach(async () => { signer1 = startSigner(signerConfig, signerDB1, keyProvider1, mockKit).listen(3001) signer2 = startSigner(signerConfig, signerDB2, keyProvider2, mockKit).listen(3002) signer3 = startSigner(signerConfig, signerDB3, keyProvider3, mockKit).listen(3003) }) + describe(`${CombinerEndpoint.PNP_QUOTA}`, () => { + const totalQuota = 10 + const weiTocusd = new BigNumber(1e17) + beforeAll(async () => { + mockOdisPaymentsTotalPaidCUSD.mockReturnValue(weiTocusd.multipliedBy(totalQuota)) + }) + + const queryCountParams = [ + { signerQueries: [0, 0, 0], expectedQueryCount: 0, expectedWarnings: [] }, + { + signerQueries: [1, 0, 0], + expectedQueryCount: 0, + expectedWarnings: [WarningMessage.SIGNER_RESPONSE_DISCREPANCIES], + }, // does not reach threshold + { + signerQueries: [1, 1, 0], + expectedQueryCount: 1, + expectedWarnings: [WarningMessage.SIGNER_RESPONSE_DISCREPANCIES], + }, // threshold reached + { + signerQueries: [0, 1, 1], + expectedQueryCount: 1, + expectedWarnings: [WarningMessage.SIGNER_RESPONSE_DISCREPANCIES], + }, // order of signers shouldn't matter + { + signerQueries: [1, 4, 9], + expectedQueryCount: 4, + expectedWarnings: [ + WarningMessage.SIGNER_RESPONSE_DISCREPANCIES, + WarningMessage.INCONSISTENT_SIGNER_QUERY_MEASUREMENTS, + ], + }, + ] + queryCountParams.forEach(({ signerQueries, expectedQueryCount, expectedWarnings }) => { + it(`should get ${expectedQueryCount} performedQueryCount given signer responses of ${signerQueries}`, async () => { + await useQuery(signerQueries[0], signer1) + await useQuery(signerQueries[1], signer2) + await useQuery(signerQueries[2], signer3) + + const req = { + account: ACCOUNT_ADDRESS1, + } + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await getCombinerQuotaResponse(req, authorization) + + expect(res.body).toStrictEqual({ + success: true, + version: expectedVersion, + performedQueryCount: expectedQueryCount, + totalQuota, + blockNumber: testBlockNumber, + warnings: expectedWarnings, + }) + }) + }) + + it('Should respond with 200 on valid request', async () => { + const req = { + account: ACCOUNT_ADDRESS1, + } + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await getCombinerQuotaResponse(req, authorization) + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: expectedVersion, + performedQueryCount: 0, + totalQuota, + blockNumber: testBlockNumber, + warnings: [], + }) + }) + + it('Should respond with 200 on repeated valid requests', async () => { + const req = { + account: ACCOUNT_ADDRESS1, + } + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res1 = await getCombinerQuotaResponse(req, authorization) + expect(res1.status).toBe(200) + expect(res1.body).toStrictEqual({ + success: true, + version: expectedVersion, + performedQueryCount: 0, + totalQuota, + blockNumber: testBlockNumber, + warnings: [], + }) + const res2 = await getCombinerQuotaResponse(req, authorization) + expect(res2.status).toBe(200) + expect(res2.body).toStrictEqual(res1.body) + }) + + it('Should respond with 200 on extra request fields', async () => { + const req = { + account: ACCOUNT_ADDRESS1, + extraField: 'dummy', + } + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await getCombinerQuotaResponse(req, authorization) + + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: expectedVersion, + performedQueryCount: 0, + totalQuota, + blockNumber: testBlockNumber, + warnings: [], + }) + }) + + it('Should respond with 200 when authenticated with DEK', async () => { + const req = { + account: ACCOUNT_ADDRESS1, + authenticationMethod: AuthenticationMethod.ENCRYPTION_KEY, + } + const authorization = getPnpRequestAuthorization(req, DEK_PRIVATE_KEY) + const res = await getCombinerQuotaResponse(req, authorization) + + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: expectedVersion, + performedQueryCount: 0, + totalQuota, + blockNumber: testBlockNumber, + warnings: [], + }) + }) + + it('Should respond with a warning when there are slight discrepancies in total quota', async () => { + mockOdisPaymentsTotalPaidCUSD.mockReturnValueOnce(weiTocusd.multipliedBy(totalQuota + 1)) + const req = { + account: ACCOUNT_ADDRESS1, + } + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await getCombinerQuotaResponse(req, authorization) + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: expectedVersion, + performedQueryCount: 0, + totalQuota, + blockNumber: testBlockNumber, + warnings: [ + WarningMessage.SIGNER_RESPONSE_DISCREPANCIES, + WarningMessage.INCONSISTENT_SIGNER_QUOTA_MEASUREMENTS + + ', using threshold signer as best guess', + ], + }) + }) + + it('Should respond with 500 when there are large discrepancies in total quota', async () => { + mockOdisPaymentsTotalPaidCUSD.mockReturnValueOnce(weiTocusd.multipliedBy(totalQuota + 15)) + const req = { + account: ACCOUNT_ADDRESS1, + } + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await getCombinerQuotaResponse(req, authorization) + expect(res.status).toBe(500) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: ErrorMessage.THRESHOLD_PNP_QUOTA_STATUS_FAILURE, + }) + }) + + it('Should respond with 400 on missing request fields', async () => { + // @ts-ignore Intentionally missing required fields + const req: PnpQuotaRequest = {} + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await getCombinerQuotaResponse(req, authorization) + + expect(res.status).toBe(400) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: WarningMessage.INVALID_INPUT, + }) + }) + + it('Should respond with 400 with invalid address', async () => { + const req = { + account: 'not an address', + } + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await getCombinerQuotaResponse(req, authorization) + + expect(res.status).toBe(400) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: WarningMessage.INVALID_INPUT, + }) + }) + + it('Should respond with 401 on failed WALLET_KEY auth', async () => { + // Request from one account, signed by another account + const req = { + account: ACCOUNT_ADDRESS2, + } + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await getCombinerQuotaResponse(req, authorization) + + expect(res.status).toBe(401) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: WarningMessage.UNAUTHENTICATED_USER, + }) + }) + + it('Should respond with 401 on failed DEK auth', async () => { + const req = { + account: ACCOUNT_ADDRESS2, + AuthenticationMethod: AuthenticationMethod.ENCRYPTION_KEY, + } + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await getCombinerQuotaResponse(req, authorization) + + expect(res.status).toBe(401) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: WarningMessage.UNAUTHENTICATED_USER, + }) + }) + + it('Should respond with 502 when insufficient signer responses', async () => { + await signerDB1?.destroy() + await signerDB2?.destroy() + signer1?.close() + signer2?.close() + + const req = { + account: ACCOUNT_ADDRESS1, + } + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await getCombinerQuotaResponse(req, authorization) + + expect(res.status).toBe(502) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: ErrorMessage.THRESHOLD_PNP_QUOTA_STATUS_FAILURE, + }) + }) + + it('Should respond with 503 on disabled api', async () => { + const configWithApiDisabled: typeof combinerConfig = JSON.parse( + JSON.stringify(combinerConfig) + ) + configWithApiDisabled.phoneNumberPrivacy.enabled = false + const appWithApiDisabled = startCombiner(configWithApiDisabled) + const req = { + account: ACCOUNT_ADDRESS1, + } + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await request(appWithApiDisabled) + .post(CombinerEndpoint.PNP_QUOTA) + .set('Authorization', authorization) + .send(req) + expect(res.status).toBe(503) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: WarningMessage.API_UNAVAILABLE, + }) + }) + + describe('functionality in case of errors', () => { + it('Should respond with 200 on failure to fetch DEK', async () => { + mockGetDataEncryptionKey.mockReset().mockImplementation(() => { + throw new Error() + }) + + const req = { + account: ACCOUNT_ADDRESS1, + authenticationMethod: AuthenticationMethod.ENCRYPTION_KEY, + } + + // NOT the dek private key, so authentication would fail if getDataEncryptionKey succeeded + const differentPk = '0x00000000000000000000000000000000000000000000000000000000ddddbbbb' + const authorization = getPnpRequestAuthorization(req, differentPk) + const res = await getCombinerQuotaResponse(req, authorization) + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: expectedVersion, + performedQueryCount: 0, + totalQuota, + blockNumber: testBlockNumber, + warnings: [], + }) + }) + }) + }) + describe(`${CombinerEndpoint.PNP_SIGN}`, () => { let req: SignMessageRequest @@ -635,418 +954,212 @@ describe('pnpService', () => { }) // For testing combiner code paths when signers do not behave as expected - describe('when 2/3 signers return correct signatures', () => { - beforeEach(async () => { - const badBlsShare1 = - '000000002e50aa714ef6b865b5de89c56969ef9f8f27b6b0a6d157c9cc01c574ac9df604' - - const badKeyProvider1 = new MockKeyProvider( - new Map([[`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, badBlsShare1]]) - ) - - signer1 = startSigner(signerConfig, signerDB1, badKeyProvider1, mockKit).listen(3001) - signer2 = startSigner(signerConfig, signerDB2, keyProvider2, mockKit).listen(3002) - signer3 = startSigner(signerConfig, signerDB3, keyProvider3, mockKit).listen(3003) + describe('when signers are not operating correctly', () => { + beforeEach(() => { + mockOdisPaymentsTotalPaidCUSD.mockReturnValue(onChainPaymentsDefault) }) - describe(`${CombinerEndpoint.PNP_SIGN}`, () => { - let req: SignMessageRequest - - beforeEach(() => { - mockOdisPaymentsTotalPaidCUSD.mockReturnValue(onChainPaymentsDefault) - req = getSignRequest(blindedMsgResult) - }) - - it('Should respond with 200 on valid request', async () => { - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await sendPnpSignRequest(req, authorization, app) + describe('when 2/3 signers return correct signatures', () => { + beforeEach(async () => { + const badBlsShare1 = + '000000002e50aa714ef6b865b5de89c56969ef9f8f27b6b0a6d157c9cc01c574ac9df604' - expect(res.status).toBe(200) - expect(res.body).toStrictEqual({ - success: true, - version: expectedVersion, - signature: expectedSignature, - performedQueryCount: 1, - totalQuota: expectedTotalQuota, - blockNumber: testBlockNumber, - warnings: [], - }) - const unblindedSig = threshold_bls.unblind( - Buffer.from(res.body.signature, 'base64'), - blindedMsgResult.blindingFactor + const badKeyProvider1 = new MockKeyProvider( + new Map([[`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, badBlsShare1]]) ) - expect(Buffer.from(unblindedSig).toString('base64')).toEqual(expectedUnblindedSig) + signer1 = startSigner(signerConfig, signerDB1, badKeyProvider1, mockKit).listen(3001) + signer2 = startSigner(signerConfig, signerDB2, keyProvider2, mockKit).listen(3002) + signer3 = startSigner(signerConfig, signerDB3, keyProvider3, mockKit).listen(3003) }) - }) - }) - - describe('when 1/3 signers return correct signatures', () => { - beforeEach(async () => { - const badBlsShare1 = - '000000002e50aa714ef6b865b5de89c56969ef9f8f27b6b0a6d157c9cc01c574ac9df604' - const badBlsShare2 = - '01000000b8f0ef841dcf8d7bd1da5e8025e47d729eb67f513335784183b8fa227a0b9a0b' - const badKeyProvider1 = new MockKeyProvider( - new Map([[`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, badBlsShare1]]) - ) - - const badKeyProvider2 = new MockKeyProvider( - new Map([[`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, badBlsShare2]]) - ) - - signer1 = startSigner(signerConfig, signerDB1, keyProvider1, mockKit).listen(3001) - signer2 = startSigner(signerConfig, signerDB2, badKeyProvider1, mockKit).listen(3002) - signer3 = startSigner(signerConfig, signerDB3, badKeyProvider2, mockKit).listen(3003) - }) - - describe(`${CombinerEndpoint.PNP_SIGN}`, () => { - let req: SignMessageRequest - - beforeEach(() => { - mockOdisPaymentsTotalPaidCUSD.mockReturnValue(onChainPaymentsDefault) - req = getSignRequest(blindedMsgResult) - }) - - it('Should respond with 500 even if request is valid', async () => { - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await sendPnpSignRequest(req, authorization, app) + describe(`${CombinerEndpoint.PNP_SIGN}`, () => { + it('Should respond with 200 on valid request', async () => { + const req = getSignRequest(blindedMsgResult) + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await sendPnpSignRequest(req, authorization, app) - expect(res.status).toBe(500) - expect(res.body).toStrictEqual({ - success: false, - version: expectedVersion, - error: ErrorMessage.NOT_ENOUGH_PARTIAL_SIGNATURES, + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: expectedVersion, + signature: expectedSignature, + performedQueryCount: 1, + totalQuota: expectedTotalQuota, + blockNumber: testBlockNumber, + warnings: [], + }) + const unblindedSig = threshold_bls.unblind( + Buffer.from(res.body.signature, 'base64'), + blindedMsgResult.blindingFactor + ) + expect(Buffer.from(unblindedSig).toString('base64')).toEqual(expectedUnblindedSig) }) }) }) - }) - describe(`${CombinerEndpoint.PNP_QUOTA}`, () => { - const useQuery = async (performedQueryCount: number, signer: Server | HttpsServer) => { - for (let i = 0; i < performedQueryCount; i++) { - const phoneNumber = '+1' + Math.floor(Math.random() * 10 ** 10) - const blindedNumber = getBlindedPhoneNumber(phoneNumber, BLINDING_FACTOR) - const req = { - account: ACCOUNT_ADDRESS1, - blindedQueryPhoneNumber: blindedNumber, - } - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - await request(signer) - .post(SignerEndpoint.PNP_SIGN) - .set('Authorization', authorization) - .send(req) - } - } - - const getCombinerQuotaResponse = async (req: PnpQuotaRequest, authorization: string) => { - const res = await request(app) - .post(CombinerEndpoint.PNP_QUOTA) - .set('Authorization', authorization) - .send(req) - return res - } - - const totalQuota = 10 - const weiTocusd = new BigNumber(1e17) - beforeAll(async () => { - mockOdisPaymentsTotalPaidCUSD.mockReturnValue(weiTocusd.multipliedBy(totalQuota)) - }) - - beforeEach(async () => { - signer1 = startSigner(signerConfig, signerDB1, keyProvider1, mockKit).listen(3001) - signer2 = startSigner(signerConfig, signerDB2, keyProvider2, mockKit).listen(3002) - signer3 = startSigner(signerConfig, signerDB3, keyProvider3, mockKit).listen(3003) - }) - - const queryCountParams = [ - { signerQueries: [0, 0, 0], expectedQueryCount: 0, expectedWarnings: [] }, - { - signerQueries: [1, 0, 0], - expectedQueryCount: 0, - expectedWarnings: [WarningMessage.SIGNER_RESPONSE_DISCREPANCIES], - }, // does not reach threshold - { - signerQueries: [1, 1, 0], - expectedQueryCount: 1, - expectedWarnings: [WarningMessage.SIGNER_RESPONSE_DISCREPANCIES], - }, // threshold reached - { - signerQueries: [0, 1, 1], - expectedQueryCount: 1, - expectedWarnings: [WarningMessage.SIGNER_RESPONSE_DISCREPANCIES], - }, // order of signers shouldn't matter - { - signerQueries: [1, 4, 9], - expectedQueryCount: 4, - expectedWarnings: [ - WarningMessage.SIGNER_RESPONSE_DISCREPANCIES, - WarningMessage.INCONSISTENT_SIGNER_QUERY_MEASUREMENTS, - ], - }, - ] - queryCountParams.forEach(({ signerQueries, expectedQueryCount, expectedWarnings }) => { - it(`should get ${expectedQueryCount} performedQueryCount given signer responses of ${signerQueries}`, async () => { - await useQuery(signerQueries[0], signer1) - await useQuery(signerQueries[1], signer2) - await useQuery(signerQueries[2], signer3) - - const req = { - account: ACCOUNT_ADDRESS1, - } - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await getCombinerQuotaResponse(req, authorization) - - expect(res.body).toStrictEqual({ - success: true, - version: expectedVersion, - performedQueryCount: expectedQueryCount, - totalQuota, - blockNumber: testBlockNumber, - warnings: expectedWarnings, - }) - }) - }) + describe('when 1/3 signers return correct signatures', () => { + beforeEach(async () => { + const badBlsShare1 = + '000000002e50aa714ef6b865b5de89c56969ef9f8f27b6b0a6d157c9cc01c574ac9df604' + const badBlsShare2 = + '01000000b8f0ef841dcf8d7bd1da5e8025e47d729eb67f513335784183b8fa227a0b9a0b' - it('Should respond with 200 on valid request', async () => { - const req = { - account: ACCOUNT_ADDRESS1, - } - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await getCombinerQuotaResponse(req, authorization) - expect(res.status).toBe(200) - expect(res.body).toStrictEqual({ - success: true, - version: expectedVersion, - performedQueryCount: 0, - totalQuota, - blockNumber: testBlockNumber, - warnings: [], - }) - }) + const badKeyProvider1 = new MockKeyProvider( + new Map([[`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, badBlsShare1]]) + ) - it('Should respond with 200 on repeated valid requests', async () => { - const req = { - account: ACCOUNT_ADDRESS1, - } - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res1 = await getCombinerQuotaResponse(req, authorization) - expect(res1.status).toBe(200) - expect(res1.body).toStrictEqual({ - success: true, - version: expectedVersion, - performedQueryCount: 0, - totalQuota, - blockNumber: testBlockNumber, - warnings: [], - }) - const res2 = await getCombinerQuotaResponse(req, authorization) - expect(res2.status).toBe(200) - expect(res2.body).toStrictEqual(res1.body) - }) + const badKeyProvider2 = new MockKeyProvider( + new Map([[`${DefaultKeyName.PHONE_NUMBER_PRIVACY}-1`, badBlsShare2]]) + ) - it('Should respond with 200 on extra request fields', async () => { - const req = { - account: ACCOUNT_ADDRESS1, - extraField: 'dummy', - } - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await getCombinerQuotaResponse(req, authorization) - - expect(res.status).toBe(200) - expect(res.body).toStrictEqual({ - success: true, - version: expectedVersion, - performedQueryCount: 0, - totalQuota, - blockNumber: testBlockNumber, - warnings: [], + signer1 = startSigner(signerConfig, signerDB1, keyProvider1, mockKit).listen(3001) + signer2 = startSigner(signerConfig, signerDB2, badKeyProvider1, mockKit).listen(3002) + signer3 = startSigner(signerConfig, signerDB3, badKeyProvider2, mockKit).listen(3003) }) - }) - it('Should respond with 200 when authenticated with DEK', async () => { - const req = { - account: ACCOUNT_ADDRESS1, - authenticationMethod: AuthenticationMethod.ENCRYPTION_KEY, - } - const authorization = getPnpRequestAuthorization(req, DEK_PRIVATE_KEY) - const res = await getCombinerQuotaResponse(req, authorization) - - expect(res.status).toBe(200) - expect(res.body).toStrictEqual({ - success: true, - version: expectedVersion, - performedQueryCount: 0, - totalQuota, - blockNumber: testBlockNumber, - warnings: [], - }) - }) + describe(`${CombinerEndpoint.PNP_SIGN}`, () => { + it('Should respond with 500 even if request is valid', async () => { + const req = getSignRequest(blindedMsgResult) + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await sendPnpSignRequest(req, authorization, app) - it('Should respond with a warning when there are slight discrepancies in total quota', async () => { - mockOdisPaymentsTotalPaidCUSD.mockReturnValueOnce(weiTocusd.multipliedBy(totalQuota + 1)) - const req = { - account: ACCOUNT_ADDRESS1, - } - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await getCombinerQuotaResponse(req, authorization) - expect(res.status).toBe(200) - expect(res.body).toStrictEqual({ - success: true, - version: expectedVersion, - performedQueryCount: 0, - totalQuota, - blockNumber: testBlockNumber, - warnings: [ - WarningMessage.SIGNER_RESPONSE_DISCREPANCIES, - WarningMessage.INCONSISTENT_SIGNER_QUOTA_MEASUREMENTS + - ', using threshold signer as best guess', - ], + expect(res.status).toBe(500) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: ErrorMessage.NOT_ENOUGH_PARTIAL_SIGNATURES, + }) + }) }) }) - it('Should respond with 500 when there are large discrepancies in total quota', async () => { - mockOdisPaymentsTotalPaidCUSD.mockReturnValueOnce(weiTocusd.multipliedBy(totalQuota + 15)) - const req = { - account: ACCOUNT_ADDRESS1, - } - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await getCombinerQuotaResponse(req, authorization) - expect(res.status).toBe(500) - expect(res.body).toStrictEqual({ - success: false, - version: expectedVersion, - error: ErrorMessage.THRESHOLD_PNP_QUOTA_STATUS_FAILURE, + describe('when 2/3 of signers are disabled', () => { + beforeEach(async () => { + const configWithApiDisabled: SignerConfig = JSON.parse(JSON.stringify(signerConfig)) + configWithApiDisabled.api.phoneNumberPrivacy.enabled = false + signer1 = startSigner(signerConfig, signerDB1, keyProvider1).listen(3001) + signer2 = startSigner(configWithApiDisabled, signerDB2, keyProvider2).listen(3002) + signer3 = startSigner(configWithApiDisabled, signerDB3, keyProvider3).listen(3003) }) - }) - it('Should respond with 400 on missing request fields', async () => { - const req = {} - // @ts-ignore Intentionally deleting required field - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - // @ts-ignore Intentionally deleting required field - const res = await getCombinerQuotaResponse(req, authorization) - - expect(res.status).toBe(400) - expect(res.body).toStrictEqual({ - success: false, - version: expectedVersion, - error: WarningMessage.INVALID_INPUT, + describe(`${CombinerEndpoint.PNP_QUOTA}`, () => { + it('Should fail to reach threshold of signers on valid request', async () => { + const req = { + account: ACCOUNT_ADDRESS1, + } + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await getCombinerQuotaResponse(req, authorization) + expect(res.status).toBe(503) // majority error code in this case + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: ErrorMessage.THRESHOLD_PNP_QUOTA_STATUS_FAILURE, + }) + }) }) - }) - it('Should respond with 400 with invalid address', async () => { - const req = { - account: 'not an address', - } - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await getCombinerQuotaResponse(req, authorization) + describe(`${CombinerEndpoint.PNP_SIGN}`, () => { + it('Should fail to reach threshold of signers on valid request', async () => { + const req = getSignRequest(blindedMsgResult) + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await sendPnpSignRequest(req, authorization, app) - expect(res.status).toBe(400) - expect(res.body).toStrictEqual({ - success: false, - version: expectedVersion, - error: WarningMessage.INVALID_INPUT, + expect(res.status).toBe(503) // majority error code in this case + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: ErrorMessage.NOT_ENOUGH_PARTIAL_SIGNATURES, + }) + }) }) }) - it('Should respond with 401 on failed WALLET_KEY auth', async () => { - // Request from one account, signed by another account - const req = { - account: ACCOUNT_ADDRESS2, - } - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await getCombinerQuotaResponse(req, authorization) - - expect(res.status).toBe(401) - expect(res.body).toStrictEqual({ - success: false, - version: expectedVersion, - error: WarningMessage.UNAUTHENTICATED_USER, + describe('when 1/3 of signers are disabled', () => { + beforeEach(async () => { + const configWithApiDisabled: SignerConfig = JSON.parse(JSON.stringify(signerConfig)) + configWithApiDisabled.api.phoneNumberPrivacy.enabled = false + signer1 = startSigner(signerConfig, signerDB1, keyProvider1).listen(3001) + signer2 = startSigner(signerConfig, signerDB2, keyProvider2).listen(3002) + signer3 = startSigner(configWithApiDisabled, signerDB3, keyProvider3).listen(3003) }) - }) - - it('Should respond with 401 on failed DEK auth', async () => { - const req = { - account: ACCOUNT_ADDRESS2, - AuthenticationMethod: AuthenticationMethod.ENCRYPTION_KEY, - } - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await getCombinerQuotaResponse(req, authorization) - expect(res.status).toBe(401) - expect(res.body).toStrictEqual({ - success: false, - version: expectedVersion, - error: WarningMessage.UNAUTHENTICATED_USER, + describe(`${CombinerEndpoint.PNP_QUOTA}`, () => { + it('Should respond with 200 on valid request', async () => { + const req = { + account: ACCOUNT_ADDRESS1, + } + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await getCombinerQuotaResponse(req, authorization) + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: expectedVersion, + performedQueryCount: 0, + totalQuota: expectedTotalQuota, + blockNumber: testBlockNumber, + warnings: [], + }) + }) }) - }) - - it('Should respond with 502 when insufficient signer responses', async () => { - await signerDB1?.destroy() - await signerDB2?.destroy() - signer1?.close() - signer2?.close() - const req = { - account: ACCOUNT_ADDRESS1, - } - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await getCombinerQuotaResponse(req, authorization) - - expect(res.status).toBe(502) - expect(res.body).toStrictEqual({ - success: false, - version: expectedVersion, - error: ErrorMessage.THRESHOLD_PNP_QUOTA_STATUS_FAILURE, + describe(`${CombinerEndpoint.PNP_SIGN}`, () => { + it('Should respond with 200 on valid request', async () => { + const req = getSignRequest(blindedMsgResult) + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await sendPnpSignRequest(req, authorization, app) + expect(res.status).toBe(200) + expect(res.body).toStrictEqual({ + success: true, + version: expectedVersion, + signature: expectedSignature, + performedQueryCount: 1, + totalQuota: expectedTotalQuota, + blockNumber: testBlockNumber, + warnings: [], + }) + }) }) }) - it('Should respond with 503 on disabled api', async () => { - const configWithApiDisabled: typeof combinerConfig = JSON.parse( - JSON.stringify(combinerConfig) - ) - configWithApiDisabled.phoneNumberPrivacy.enabled = false - const appWithApiDisabled = startCombiner(configWithApiDisabled) - const req = { - account: ACCOUNT_ADDRESS1, - } - const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) - const res = await request(appWithApiDisabled) - .post(CombinerEndpoint.PNP_QUOTA) - .set('Authorization', authorization) - .send(req) - expect(res.status).toBe(503) - expect(res.body).toStrictEqual({ - success: false, - version: expectedVersion, - error: WarningMessage.API_UNAVAILABLE, + describe('when signers timeout', () => { + beforeEach(async () => { + const testTimeoutMS = 0 + + const configWithShortTimeout: SignerConfig = JSON.parse(JSON.stringify(signerConfig)) + configWithShortTimeout.timeout = testTimeoutMS + // Test this with all signers timing out to decrease possibility of race conditions + signer1 = startSigner(configWithShortTimeout, signerDB1, keyProvider1).listen(3001) + signer2 = startSigner(configWithShortTimeout, signerDB2, keyProvider2).listen(3002) + signer3 = startSigner(configWithShortTimeout, signerDB3, keyProvider3).listen(3003) }) - }) - describe('functionality in case of errors', () => { - it('Should respond with 200 on failure to fetch DEK', async () => { - mockGetDataEncryptionKey.mockReset().mockImplementation(() => { - throw new Error() + describe(`${CombinerEndpoint.PNP_QUOTA}`, () => { + it('Should fail to reach threshold of signers on valid request', async () => { + const req = { + account: ACCOUNT_ADDRESS1, + } + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await getCombinerQuotaResponse(req, authorization) + expect(res.status).toBe(500) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: ErrorMessage.THRESHOLD_PNP_QUOTA_STATUS_FAILURE, + }) }) + }) - const req = { - account: ACCOUNT_ADDRESS1, - authenticationMethod: AuthenticationMethod.ENCRYPTION_KEY, - } - - // NOT the dek private key, so authentication would fail if getDataEncryptionKey succeeded - const differentPk = '0x00000000000000000000000000000000000000000000000000000000ddddbbbb' - const authorization = getPnpRequestAuthorization(req, differentPk) - const res = await getCombinerQuotaResponse(req, authorization) - expect(res.status).toBe(200) - expect(res.body).toStrictEqual({ - success: true, - version: expectedVersion, - performedQueryCount: 0, - totalQuota, - blockNumber: testBlockNumber, - warnings: [], + describe(`${CombinerEndpoint.PNP_SIGN}`, () => { + it('Should fail to reach threshold of signers on valid request', async () => { + const req = getSignRequest(blindedMsgResult) + const authorization = getPnpRequestAuthorization(req, PRIVATE_KEY1) + const res = await sendPnpSignRequest(req, authorization, app) + expect(res.status).toBe(500) + expect(res.body).toStrictEqual({ + success: false, + version: expectedVersion, + error: ErrorMessage.NOT_ENOUGH_PARTIAL_SIGNATURES, + }) }) }) }) diff --git a/packages/phone-number-privacy/signer/test/integration/domain.test.ts b/packages/phone-number-privacy/signer/test/integration/domain.test.ts index 85559199167..7d6ad2fa2a4 100644 --- a/packages/phone-number-privacy/signer/test/integration/domain.test.ts +++ b/packages/phone-number-privacy/signer/test/integration/domain.test.ts @@ -234,7 +234,7 @@ describe('domain', () => { }) it('Should respond with 400 on unknown domain', async () => { - // Create a requests with an invalid domain identifier. + // Create a request with an invalid domain identifier. const unknownRequest = await disableRequest() // @ts-ignore UnknownDomain is (intentionally) not a valid domain identifier. unknownRequest.domain.name = 'UnknownDomain' @@ -337,7 +337,7 @@ describe('domain', () => { }) it('Should respond with 500 on signer timeout', async () => { - const testTimeoutMS = 200 + const testTimeoutMS = 0 const delay = 200 const configWithShortTimeout = JSON.parse(JSON.stringify(_config)) @@ -441,7 +441,7 @@ describe('domain', () => { }) it('Should respond with 400 on unknown domain', async () => { - // Create a requests with an invalid domain identifier. + // Create a request with an invalid domain identifier. const unknownRequest = await quotaRequest() // @ts-ignore UnknownDomain is (intentionally) not a valid domain identifier. unknownRequest.domain.name = 'UnknownDomain' @@ -550,7 +550,7 @@ describe('domain', () => { }) it('Should respond with 500 on signer timeout', async () => { - const testTimeoutMS = 200 + const testTimeoutMS = 0 const delay = 200 const configWithShortTimeout = JSON.parse(JSON.stringify(_config)) @@ -756,7 +756,7 @@ describe('domain', () => { }) it('Should respond with 400 on unknown domain', async () => { - // Create a requests with an invalid domain identifier. + // Create a request with an invalid domain identifier. const [unknownRequest, _] = await signatureRequest() // @ts-ignore UnknownDomain is (intentionally) not a valid domain identifier. unknownRequest.domain.name = 'UnknownDomain' @@ -1007,7 +1007,7 @@ describe('domain', () => { it('Should respond with 500 on signer timeout', async () => { const [req, _] = await signatureRequest() - const testTimeoutMS = 200 + const testTimeoutMS = 0 const delay = 200 const spy = jest diff --git a/packages/phone-number-privacy/signer/test/integration/legacypnp.test.ts b/packages/phone-number-privacy/signer/test/integration/legacypnp.test.ts index f3d5b6c8bba..cb83628fc50 100644 --- a/packages/phone-number-privacy/signer/test/integration/legacypnp.test.ts +++ b/packages/phone-number-privacy/signer/test/integration/legacypnp.test.ts @@ -655,7 +655,7 @@ describe('legacyPNP', () => { }) it('Should respond with 500 on signer timeout', async () => { - const testTimeoutMS = 200 + const testTimeoutMS = 0 const delay = 100 const spy = jest .spyOn( @@ -1123,7 +1123,7 @@ describe('legacyPNP', () => { }) it('Should respond with 500 on unsupported key version', async () => { - const badRequest = getPnpSignRequest( + const badRequest = getLegacyPnpSignRequest( ACCOUNT_ADDRESS1, BLINDED_PHONE_NUMBER, AuthenticationMethod.WALLET_KEY @@ -1645,7 +1645,7 @@ describe('legacyPNP', () => { }) it('Should respond with 500 on signer timeout', async () => { - const testTimeoutMS = 200 + const testTimeoutMS = 0 const delay = 200 const spy = jest .spyOn( diff --git a/packages/phone-number-privacy/signer/test/integration/pnp.test.ts b/packages/phone-number-privacy/signer/test/integration/pnp.test.ts index 20478203990..b52fc27d478 100644 --- a/packages/phone-number-privacy/signer/test/integration/pnp.test.ts +++ b/packages/phone-number-privacy/signer/test/integration/pnp.test.ts @@ -422,7 +422,7 @@ describe('pnp', () => { }) it('Should respond with 500 on signer timeout', async () => { - const testTimeoutMS = 200 + const testTimeoutMS = 0 const delay = 100 const spy = jest .spyOn( @@ -645,8 +645,6 @@ describe('pnp', () => { const badRequest = getPnpSignRequest( ACCOUNT_ADDRESS1, BLINDED_PHONE_NUMBER, - // TODO(2.0.0): Investigate whether we should be testing DEK vs. WALLET_KEY based authentication - // (https://github.com/celo-org/celo-monorepo/issues/9837) AuthenticationMethod.WALLET_KEY ) // @ts-ignore Intentionally deleting required field @@ -937,7 +935,7 @@ describe('pnp', () => { }) it('Should respond with 500 on signer timeout', async () => { - const testTimeoutMS = 200 + const testTimeoutMS = 0 const delay = 200 const spy = jest .spyOn(