Skip to content

Commit

Permalink
feat: handle some cloud server error codes (#955)
Browse files Browse the repository at this point in the history
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 <digidem/comapeo-cloud#33>, which adds these
codes.
  • Loading branch information
EvanHahn authored Nov 6, 2024
1 parent 5efcdfc commit 6a3bb5b
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 26 deletions.
94 changes: 68 additions & 26 deletions src/member-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -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)
}

/**
Expand Down Expand Up @@ -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}`
)
}
42 changes: 42 additions & 0 deletions test-e2e/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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) => {
Expand Down

0 comments on commit 6a3bb5b

Please sign in to comment.