From 406f6748b47c7db5c4962d6815a64139a87d8f1a Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Wed, 6 Nov 2024 16:05:18 +0000 Subject: [PATCH] feat: handle some cloud server error codes This turns the server's `TOO_MANY_PROJECTS` error into the `SERVER_HAS_TOO_MANY_PROJECTS` error, and the `PROJECT_NOT_IN_ALLOWLIST` error into `PROJECT_NOT_IN_SERVER_ALLOWLIST` error. See , which adds these codes. --- src/member-api.js | 94 +++++++++++++++++++++++++++++++++------------- test-e2e/server.js | 42 +++++++++++++++++++++ 2 files changed, 110 insertions(+), 26 deletions(-) diff --git a/src/member-api.js b/src/member-api.js index 92157ab5..08b64a38 100644 --- a/src/member-api.js +++ b/src/member-api.js @@ -277,6 +277,10 @@ export class MemberApi extends TypedEmitter { * peer. For example, the project must have a name. * - `NETWORK_ERROR`: there was an issue connecting to the server. Is the * device online? Is the server online? + * - `SERVER_HAS_TOO_MANY_PROJECTS`: the server limits the number of projects + * it can have, and it's at the limit. + * - `PROJECT_NOT_IN_SERVER_ALLOWLIST`: the server only allows specific + * projects to be added and ours wasn't one of them. * - `INVALID_SERVER_RESPONSE`: we connected to the server but it returned * an unexpected response. Is the server running a compatible version of * CoMapeo Cloud? @@ -351,32 +355,7 @@ export class MemberApi extends TypedEmitter { ) } - if (response.status !== 200 && response.status !== 201) { - throw new ErrorWithCode( - 'INVALID_SERVER_RESPONSE', - `Failed to add server peer due to HTTP status code ${response.status}` - ) - } - - try { - const responseBody = await response.json() - assert( - responseBody && - typeof responseBody === 'object' && - 'data' in responseBody && - responseBody.data && - typeof responseBody.data === 'object' && - 'deviceId' in responseBody.data && - typeof responseBody.data.deviceId === 'string', - 'Response body is valid' - ) - return { serverDeviceId: responseBody.data.deviceId } - } catch (err) { - throw new ErrorWithCode( - 'INVALID_SERVER_RESPONSE', - "Failed to add server peer because we couldn't parse the response" - ) - } + return await parseAddServerResponse(response) } /** @@ -575,3 +554,66 @@ function isValidServerBaseUrl( function encodeBufferForServer(buffer) { return buffer ? b4a.toString(buffer, 'hex') : undefined } + +/** + * @param {Response} response + * @returns {Promise<{ serverDeviceId: string }>} + */ +async function parseAddServerResponse(response) { + if (response.status === 200) { + try { + const responseBody = await response.json() + assert( + responseBody && + typeof responseBody === 'object' && + 'data' in responseBody && + responseBody.data && + typeof responseBody.data === 'object' && + 'deviceId' in responseBody.data && + typeof responseBody.data.deviceId === 'string', + 'Response body is valid' + ) + return { serverDeviceId: responseBody.data.deviceId } + } catch (err) { + throw new ErrorWithCode( + 'INVALID_SERVER_RESPONSE', + "Failed to add server peer because we couldn't parse the response" + ) + } + } + + let responseBody + try { + responseBody = await response.json() + } catch (_) { + responseBody = null + } + if ( + responseBody && + typeof responseBody === 'object' && + 'error' in responseBody && + responseBody.error && + typeof responseBody.error === 'object' && + 'code' in responseBody.error + ) { + switch (responseBody.error.code) { + case 'PROJECT_NOT_IN_ALLOWLIST': + throw new ErrorWithCode( + 'PROJECT_NOT_IN_SERVER_ALLOWLIST', + "The server only allows specific projects to be added, and this isn't one of them" + ) + case 'TOO_MANY_PROJECTS': + throw new ErrorWithCode( + 'SERVER_HAS_TOO_MANY_PROJECTS', + "The server limits the number of projects it can have and it's at the limit" + ) + default: + break + } + } + + throw new ErrorWithCode( + 'INVALID_SERVER_RESPONSE', + `Failed to add server peer due to HTTP status code ${response.status}` + ) +} diff --git a/test-e2e/server.js b/test-e2e/server.js index ff3f1e18..43ed2caf 100644 --- a/test-e2e/server.js +++ b/test-e2e/server.js @@ -9,6 +9,7 @@ import { setTimeout as delay } from 'node:timers/promises' import pDefer from 'p-defer' import { pEvent } from 'p-event' import RAM from 'random-access-memory' +import { map } from 'iterpal' import { MEMBER_ROLE_ID } from '../src/roles.js' import comapeoServer from '@comapeo/cloud' import { @@ -112,6 +113,46 @@ test("fails if we can't connect to the server", async (t) => { ) }) +test( + "translates some of the server's error codes when adding one", + { concurrency: true }, + async (t) => { + const manager = createManager('device0', t) + const projectId = await manager.createProject({ name: 'foo' }) + const project = await manager.getProject(projectId) + + const serverErrorToLocalError = new Map([ + ['PROJECT_NOT_IN_ALLOWLIST', 'PROJECT_NOT_IN_SERVER_ALLOWLIST'], + ['TOO_MANY_PROJECTS', 'SERVER_HAS_TOO_MANY_PROJECTS'], + ['__TEST_UNRECOGNIZED_ERROR', 'INVALID_SERVER_RESPONSE'], + ]) + await Promise.all( + map(serverErrorToLocalError, ([serverError, expectedCode]) => + t.test(`turns a ${serverError} into ${expectedCode}`, async (t) => { + const fastify = createFastify() + fastify.put('/projects', (_req, reply) => { + reply.status(403).send({ + error: { code: serverError }, + }) + }) + const serverBaseUrl = await fastify.listen() + t.after(() => fastify.close()) + + await assert.rejects( + () => + project.$member.addServerPeer(serverBaseUrl, { + dangerouslyAllowInsecureConnections: true, + }), + { + code: expectedCode, + } + ) + }) + ) + ) + } +) + test( "fails if server doesn't return a 200", { concurrency: true }, @@ -160,6 +201,7 @@ test( '{bad_json', JSON.stringify({ data: {} }), JSON.stringify({ data: { deviceId: 123 } }), + JSON.stringify({ error: { deviceId: '123' } }), JSON.stringify({ deviceId: 'not under "data"' }), ].map((responseData) => t.test(`when returning ${responseData}`, async (t) => {