@remit/backend 0.0.72 → 0.0.73
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/error.test.ts +114 -0
- package/src/error.ts +15 -2
- package/src/handlers/mailbox.ts +1 -1
package/package.json
CHANGED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import type { FolderRoleConflict } from "@remit/api-openapi-types";
|
|
4
|
+
import {
|
|
5
|
+
ClientError,
|
|
6
|
+
FolderRoleUnresolvedError,
|
|
7
|
+
ForbiddenError,
|
|
8
|
+
UnhandledError,
|
|
9
|
+
} from "@remit/data-ports/errors";
|
|
10
|
+
import { NO_TRASH_FOLDER_REASON } from "@remit/data-ports/folder-role";
|
|
11
|
+
import { CanonicalMailboxRole } from "@remit/domain-enums";
|
|
12
|
+
import { NoTrashMailboxError } from "@remit/mailbox-service";
|
|
13
|
+
import { handleError } from "./error.js";
|
|
14
|
+
|
|
15
|
+
const parseBody = (body: string): Record<string, unknown> =>
|
|
16
|
+
JSON.parse(body) as Record<string, unknown>;
|
|
17
|
+
|
|
18
|
+
describe("handleError coded refusals", () => {
|
|
19
|
+
it("puts the code and every detail of a folder-role 409 on the wire", async () => {
|
|
20
|
+
const response = await handleError(
|
|
21
|
+
new FolderRoleUnresolvedError(
|
|
22
|
+
"The folder you chose for Trash is gone.",
|
|
23
|
+
CanonicalMailboxRole.Trash,
|
|
24
|
+
"stale",
|
|
25
|
+
"account-7",
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
assert.equal(response.statusCode, 409);
|
|
30
|
+
assert.deepEqual(parseBody(response.body), {
|
|
31
|
+
message: "The folder you chose for Trash is gone.",
|
|
32
|
+
code: "folder_role_unresolved",
|
|
33
|
+
details: { role: "Trash", reason: "stale", accountId: "account-7" },
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("carries NoTrashMailboxError as the Trash role with reason none", async () => {
|
|
38
|
+
const response = await handleError(new NoTrashMailboxError("account-7"));
|
|
39
|
+
|
|
40
|
+
assert.equal(response.statusCode, 409);
|
|
41
|
+
assert.deepEqual(parseBody(response.body), {
|
|
42
|
+
message: NO_TRASH_FOLDER_REASON,
|
|
43
|
+
code: "folder_role_unresolved",
|
|
44
|
+
details: { role: "Trash", reason: "none", accountId: "account-7" },
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("keeps an unhandled 500 to a message — no stack, no cause, no code", async () => {
|
|
49
|
+
const response = await handleError(
|
|
50
|
+
new UnhandledError("Something went wrong", new Error("connection reset")),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
assert.equal(response.statusCode, 500);
|
|
54
|
+
assert.deepEqual(parseBody(response.body), {
|
|
55
|
+
message: "Something went wrong",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("strips a code a 5xx should never have carried", async () => {
|
|
60
|
+
const error = new UnhandledError("Something went wrong");
|
|
61
|
+
error.publicApiError = {
|
|
62
|
+
code: "folder_role_unresolved",
|
|
63
|
+
details: { role: "Trash", reason: "none", accountId: "account-7" },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const response = await handleError(error);
|
|
67
|
+
|
|
68
|
+
assert.equal(response.statusCode, 500);
|
|
69
|
+
assert.deepEqual(parseBody(response.body), {
|
|
70
|
+
message: "Something went wrong",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("answers an unauthenticated request with a 401 and an error body", async () => {
|
|
75
|
+
const response = await handleError(new ClientError("Session expired"));
|
|
76
|
+
|
|
77
|
+
assert.equal(response.statusCode, 401);
|
|
78
|
+
assert.deepEqual(parseBody(response.body), { message: "Session expired" });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("answers a forbidden request with a 403 and an error body", async () => {
|
|
82
|
+
const response = await handleError(new ForbiddenError("Not your account"));
|
|
83
|
+
|
|
84
|
+
assert.equal(response.statusCode, 403);
|
|
85
|
+
assert.deepEqual(parseBody(response.body), { message: "Not your account" });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// The contract and the emitter cannot drift apart: this asserts the response
|
|
89
|
+
// `handleError` actually builds against the generated model. Nesting the body
|
|
90
|
+
// under `error` again would leave `code` off `keyof FolderRoleConflict` and
|
|
91
|
+
// fail to compile here, long before a client reads a 409 it cannot parse.
|
|
92
|
+
it("matches the generated FolderRoleConflict model field for field", async () => {
|
|
93
|
+
const response = await handleError(
|
|
94
|
+
new FolderRoleUnresolvedError(
|
|
95
|
+
"Nobody has confirmed a Trash folder.",
|
|
96
|
+
CanonicalMailboxRole.Trash,
|
|
97
|
+
"unconfirmed",
|
|
98
|
+
"account-7",
|
|
99
|
+
),
|
|
100
|
+
);
|
|
101
|
+
const body: Omit<FolderRoleConflict, "statusCode"> = JSON.parse(
|
|
102
|
+
response.body,
|
|
103
|
+
);
|
|
104
|
+
const declaredFields: ReadonlyArray<keyof typeof body> = [
|
|
105
|
+
"code",
|
|
106
|
+
"details",
|
|
107
|
+
"message",
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
assert.equal(response.statusCode, 409);
|
|
111
|
+
assert.deepEqual(Object.keys(body).sort(), [...declaredFields].sort());
|
|
112
|
+
assert.equal(body.code, "folder_role_unresolved");
|
|
113
|
+
});
|
|
114
|
+
});
|
package/src/error.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPublicApiError } from "@remit/data-ports/errors";
|
|
1
2
|
import { logger } from "@remit/logger-lambda";
|
|
2
3
|
import type { APIGatewayProxyResult } from "aws-lambda";
|
|
3
4
|
import { formatResponse } from "./response.js";
|
|
@@ -15,9 +16,21 @@ export const handleError = async (
|
|
|
15
16
|
},
|
|
16
17
|
"Error with statusCode",
|
|
17
18
|
);
|
|
19
|
+
// Only an error that opted in gets a coded body, and only below 500:
|
|
20
|
+
// a coded 5xx is a mistake at the throw site, not a contract, and the
|
|
21
|
+
// status bound keeps it from reaching a client rather than trusting
|
|
22
|
+
// every future thrower to leave `publicApiError` alone.
|
|
23
|
+
const statusCode =
|
|
24
|
+
typeof error.statusCode === "number" ? error.statusCode : 500;
|
|
25
|
+
const publicApiError =
|
|
26
|
+
statusCode < 500 &&
|
|
27
|
+
"publicApiError" in error &&
|
|
28
|
+
isPublicApiError(error.publicApiError)
|
|
29
|
+
? error.publicApiError
|
|
30
|
+
: undefined;
|
|
18
31
|
return formatResponse(
|
|
19
|
-
{ message: error.message },
|
|
20
|
-
|
|
32
|
+
{ message: error.message, ...publicApiError },
|
|
33
|
+
statusCode,
|
|
21
34
|
);
|
|
22
35
|
}
|
|
23
36
|
|
package/src/handlers/mailbox.ts
CHANGED
|
@@ -421,7 +421,7 @@ export const TrashOperations: Record<
|
|
|
421
421
|
await client.mailboxSpecialUse.findConfirmedTrashMailbox(accountId);
|
|
422
422
|
|
|
423
423
|
if (!trashMailbox) {
|
|
424
|
-
throw new NoTrashMailboxError();
|
|
424
|
+
throw new NoTrashMailboxError(accountId);
|
|
425
425
|
}
|
|
426
426
|
|
|
427
427
|
const messages = await client.message.listAllByMailbox(
|