@remit/mailbox-service 0.0.85 → 0.0.86
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/body-sync-category-denormalize.test.ts +41 -36
- package/src/body-sync-category-override.test.ts +54 -10
- package/src/body-sync-quarantine.test.ts +86 -35
- package/src/body-sync-unclassifiable-once.test.ts +256 -0
- package/src/body-sync.ts +43 -140
- package/src/list-id-backfill.ts +8 -5
- package/src/message-move-copy.test.ts +39 -0
- package/src/message-move.ts +9 -0
- package/src/body-sync-backfill.test.ts +0 -292
package/package.json
CHANGED
|
@@ -3,21 +3,22 @@
|
|
|
3
3
|
* on, and its write-path defects only appear under conditions a happy-path test
|
|
4
4
|
* never creates (issue #320).
|
|
5
5
|
*
|
|
6
|
-
* 1. The
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* 1. The classifying pass wrote the Message before the ThreadMessage while the
|
|
7
|
+
* skip guard keyed off the Message. A failure between the two left the guard
|
|
8
|
+
* satisfied and the denormalized row stranded at `uncategorized` forever,
|
|
9
|
+
* because the requeued retry returned early.
|
|
10
10
|
* 2. `denormalizeCategory` resolved one arbitrary row per message through an
|
|
11
11
|
* unordered `.limit(1)`. More than one row per messageId is schema-legal —
|
|
12
12
|
* reachable through thread-root drift, not through a second mailbox, whose ids
|
|
13
13
|
* collapse to one row — and every row a message has must end up correct.
|
|
14
|
-
* 3. It also wrote only `category` on
|
|
15
|
-
*
|
|
14
|
+
* 3. It also wrote only `category` on one of its paths, so the snippet and the
|
|
15
|
+
* `listId` derived from the same bytes were dropped on the floor.
|
|
16
16
|
*
|
|
17
17
|
* None of these show up on a single row in a single successful pass.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import assert from "node:assert/strict";
|
|
21
|
+
import { Readable } from "node:stream";
|
|
21
22
|
import { describe, it } from "node:test";
|
|
22
23
|
import type {
|
|
23
24
|
IAddressRepository,
|
|
@@ -104,7 +105,6 @@ const buildHarness = (
|
|
|
104
105
|
rows: Row[],
|
|
105
106
|
options: {
|
|
106
107
|
failThreadUpdates?: number;
|
|
107
|
-
bodyStored?: boolean;
|
|
108
108
|
} = {},
|
|
109
109
|
): Harness => {
|
|
110
110
|
const messageUpdates: Array<{
|
|
@@ -120,9 +120,6 @@ const buildHarness = (
|
|
|
120
120
|
mailboxId: "mb-inbox",
|
|
121
121
|
uid: 1,
|
|
122
122
|
category: MessageCategory.uncategorized,
|
|
123
|
-
...(options.bodyStored === false
|
|
124
|
-
? {}
|
|
125
|
-
: { bodyStorageKey: "s3://bodies/m-1" }),
|
|
126
123
|
} as unknown as MessageItem;
|
|
127
124
|
|
|
128
125
|
const messageService = {
|
|
@@ -164,6 +161,7 @@ const buildHarness = (
|
|
|
164
161
|
const storageService = {
|
|
165
162
|
retrieve: async () => LINKEDIN_EML,
|
|
166
163
|
storeMessageBody: async () => ({ uri: "s3://bodies/m-1" }),
|
|
164
|
+
storeMessageBodyStream: async () => ({ uri: "s3://bodies/m-1" }),
|
|
167
165
|
storeParsedBody: async () => {},
|
|
168
166
|
listBodyParts: async () => [],
|
|
169
167
|
} as unknown as StorageService;
|
|
@@ -192,10 +190,22 @@ const buildHarness = (
|
|
|
192
190
|
};
|
|
193
191
|
};
|
|
194
192
|
|
|
195
|
-
const
|
|
196
|
-
harness.service.syncBodies(
|
|
197
|
-
|
|
198
|
-
|
|
193
|
+
const syncBatch = (harness: Harness) =>
|
|
194
|
+
harness.service.syncBodies(
|
|
195
|
+
["m-1"],
|
|
196
|
+
"acc-1",
|
|
197
|
+
"cfg-1",
|
|
198
|
+
"INBOX",
|
|
199
|
+
async () =>
|
|
200
|
+
({
|
|
201
|
+
openBox: async () => {},
|
|
202
|
+
async *fetchMessageBodies(uids: number[]) {
|
|
203
|
+
for (const uid of uids) {
|
|
204
|
+
yield { uid, source: Readable.from([LINKEDIN_EML]) };
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
}) as unknown as IImapConnection,
|
|
208
|
+
);
|
|
199
209
|
|
|
200
210
|
const syncFromImap = (harness: Harness) =>
|
|
201
211
|
harness.service.fetchAndGetBody(
|
|
@@ -210,19 +220,19 @@ const syncFromImap = (harness: Harness) =>
|
|
|
210
220
|
}) as unknown as IImapConnection,
|
|
211
221
|
);
|
|
212
222
|
|
|
213
|
-
describe("the
|
|
223
|
+
describe("the classifying pass survives an interrupted write", () => {
|
|
214
224
|
it("leaves the Message unclassified when the ThreadMessage write fails, so the retry runs again", async () => {
|
|
215
225
|
const harness = buildHarness([buildRow({ threadMessageId: "tm-1" })], {
|
|
216
226
|
failThreadUpdates: 1,
|
|
217
227
|
});
|
|
218
228
|
|
|
219
|
-
const failed = await
|
|
229
|
+
const failed = await syncBatch(harness);
|
|
220
230
|
|
|
221
231
|
assert.deepEqual(failed.failedMessageIds, ["m-1"]);
|
|
222
232
|
assert.equal(harness.message.category, MessageCategory.uncategorized);
|
|
223
233
|
assert.deepEqual(harness.messageUpdates, []);
|
|
224
234
|
|
|
225
|
-
const retried = await
|
|
235
|
+
const retried = await syncBatch(harness);
|
|
226
236
|
|
|
227
237
|
assert.deepEqual(retried.failedMessageIds, []);
|
|
228
238
|
assert.equal(harness.message.category, MessageCategory.social);
|
|
@@ -232,14 +242,14 @@ describe("the classification backfill survives an interrupted write", () => {
|
|
|
232
242
|
it("writes the Message only after the denormalized row is durable", async () => {
|
|
233
243
|
const harness = buildHarness([buildRow({ threadMessageId: "tm-1" })]);
|
|
234
244
|
|
|
235
|
-
await
|
|
245
|
+
await syncBatch(harness);
|
|
236
246
|
|
|
237
247
|
assert.deepEqual(harness.writeOrder, ["thread", "message"]);
|
|
238
248
|
});
|
|
239
249
|
});
|
|
240
250
|
|
|
241
251
|
describe("denormalization reaches every row a message has", () => {
|
|
242
|
-
it("carries the
|
|
252
|
+
it("carries the classified category onto all of a message's rows", async () => {
|
|
243
253
|
const harness = buildHarness([
|
|
244
254
|
buildRow({ threadMessageId: "tm-inbox", mailboxId: "mb-inbox" }),
|
|
245
255
|
buildRow({
|
|
@@ -249,7 +259,7 @@ describe("denormalization reaches every row a message has", () => {
|
|
|
249
259
|
}),
|
|
250
260
|
]);
|
|
251
261
|
|
|
252
|
-
await
|
|
262
|
+
await syncBatch(harness);
|
|
253
263
|
|
|
254
264
|
assert.deepEqual(
|
|
255
265
|
harness.rows.map((r) => r.category),
|
|
@@ -267,7 +277,7 @@ describe("denormalization reaches every row a message has", () => {
|
|
|
267
277
|
}),
|
|
268
278
|
]);
|
|
269
279
|
|
|
270
|
-
await
|
|
280
|
+
await syncBatch(harness);
|
|
271
281
|
|
|
272
282
|
assert.deepEqual(
|
|
273
283
|
harness.threadUpdates.map((u) => ({
|
|
@@ -286,16 +296,13 @@ describe("denormalization reaches every row a message has", () => {
|
|
|
286
296
|
);
|
|
287
297
|
});
|
|
288
298
|
|
|
289
|
-
it("fans the snippet and the list id out to every row on the
|
|
299
|
+
it("fans the snippet and the list id out to every row on the read path", async () => {
|
|
290
300
|
// A snippet and a List-Id belong to the message, not to the mailbox a row
|
|
291
301
|
// happens to name, so every row carries them.
|
|
292
|
-
const harness = buildHarness(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
],
|
|
297
|
-
{ bodyStored: false },
|
|
298
|
-
);
|
|
302
|
+
const harness = buildHarness([
|
|
303
|
+
buildRow({ threadMessageId: "tm-inbox", mailboxId: "mb-inbox" }),
|
|
304
|
+
buildRow({ threadMessageId: "tm-archive", mailboxId: "mb-archive" }),
|
|
305
|
+
]);
|
|
299
306
|
|
|
300
307
|
await syncFromImap(harness);
|
|
301
308
|
|
|
@@ -318,7 +325,7 @@ describe("denormalization reaches every row a message has", () => {
|
|
|
318
325
|
buildRow({ threadMessageId: "tm-archive", mailboxId: "mb-archive" }),
|
|
319
326
|
]);
|
|
320
327
|
|
|
321
|
-
await
|
|
328
|
+
await syncBatch(harness);
|
|
322
329
|
|
|
323
330
|
assert.deepEqual(
|
|
324
331
|
harness.threadUpdates.map((u) => u.threadMessageId),
|
|
@@ -327,15 +334,13 @@ describe("denormalization reaches every row a message has", () => {
|
|
|
327
334
|
});
|
|
328
335
|
});
|
|
329
336
|
|
|
330
|
-
describe("the
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
// returns early — and nothing but these two paths ever writes `listId`. The
|
|
334
|
-
// backfill passing only `category` left every copy's `list_id` NULL for good.
|
|
337
|
+
describe("the classifying pass writes the whole denormalized set", () => {
|
|
338
|
+
// Nothing but this pass ever derives `listId` from a message's bytes, so a
|
|
339
|
+
// write that carried only `category` left the row's `list_id` NULL for good.
|
|
335
340
|
it("carries the list id and the snippet, not just the category", async () => {
|
|
336
341
|
const harness = buildHarness([buildRow({ threadMessageId: "tm-1" })]);
|
|
337
342
|
|
|
338
|
-
await
|
|
343
|
+
await syncBatch(harness);
|
|
339
344
|
|
|
340
345
|
assert.deepEqual(harness.threadUpdates.length, 1);
|
|
341
346
|
assert.deepEqual(harness.threadUpdates[0].input, {
|
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
* `NoSuchKey` IMAP re-fetch and `syncBodies(..., force: true)` — so a
|
|
16
16
|
* regression that let the override bypass the guard shows up here, not just
|
|
17
17
|
* in an isolated unit check.
|
|
18
|
+
*
|
|
19
|
+
* The override reaches a message on the pass that examines it, and only then
|
|
20
|
+
* (issue #331). Mail already stored and examined is not revisited, whatever the
|
|
21
|
+
* user later says about the sender: an ordinary sync pass does not re-read a
|
|
22
|
+
* stored body, and the last test here pins that. Applying a reclassification
|
|
23
|
+
* backwards over mail already on disk is a deliberate operation over
|
|
24
|
+
* `Message.classificationState`, tracked in issue #415, and nothing in this
|
|
25
|
+
* file claims it happens today.
|
|
18
26
|
*/
|
|
19
27
|
|
|
20
28
|
import assert from "node:assert/strict";
|
|
@@ -278,15 +286,50 @@ describe("Address.flags.category overrides classification at sync time (issue #2
|
|
|
278
286
|
);
|
|
279
287
|
});
|
|
280
288
|
|
|
281
|
-
it("
|
|
289
|
+
it("reaches a message still awaiting examination on the batch sync path too", async () => {
|
|
290
|
+
const harness = buildHarness(
|
|
291
|
+
{ messageId: "m-1", category: MessageCategory.uncategorized },
|
|
292
|
+
overrideFlags(MessageCategory.transactional),
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const result = await harness.service.syncBodies(
|
|
296
|
+
["m-1"],
|
|
297
|
+
"acc-1",
|
|
298
|
+
"cfg-1",
|
|
299
|
+
"INBOX",
|
|
300
|
+
async () =>
|
|
301
|
+
({
|
|
302
|
+
openBox: async () => {},
|
|
303
|
+
async *fetchMessageBodies(uids: number[]) {
|
|
304
|
+
for (const uid of uids) {
|
|
305
|
+
yield { uid, source: Readable.from([LINKEDIN_EML]) };
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
}) as unknown as IImapConnection,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
assert.deepEqual(result.syncedMessageIds, ["m-1"]);
|
|
312
|
+
assert.equal(harness.messageUpdates.length, 1);
|
|
313
|
+
assert.equal(
|
|
314
|
+
harness.messageUpdates[0].input.category,
|
|
315
|
+
MessageCategory.transactional,
|
|
316
|
+
);
|
|
317
|
+
assert.equal(harness.rows[0].category, MessageCategory.transactional);
|
|
318
|
+
assert.equal(
|
|
319
|
+
harness.messageUpdates[0].input.classificationState,
|
|
320
|
+
"Examined",
|
|
321
|
+
);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("does not reach back to mail it already examined when the sender is reclassified later", async () => {
|
|
282
325
|
const harness = buildHarness(
|
|
283
326
|
{
|
|
284
327
|
messageId: "m-1",
|
|
285
328
|
bodyStorageKey: "s3://bodies/m-1",
|
|
286
329
|
category: MessageCategory.uncategorized,
|
|
287
|
-
|
|
330
|
+
classificationState: "Examined",
|
|
331
|
+
} as Partial<MessageItem> & Pick<MessageItem, "messageId">,
|
|
288
332
|
overrideFlags(MessageCategory.transactional),
|
|
289
|
-
async () => LINKEDIN_EML,
|
|
290
333
|
);
|
|
291
334
|
|
|
292
335
|
const result = await harness.service.syncBodies(
|
|
@@ -295,17 +338,18 @@ describe("Address.flags.category overrides classification at sync time (issue #2
|
|
|
295
338
|
"cfg-1",
|
|
296
339
|
"INBOX",
|
|
297
340
|
async () => {
|
|
298
|
-
throw new Error("
|
|
341
|
+
throw new Error("an examined message must not be fetched again");
|
|
299
342
|
},
|
|
300
343
|
);
|
|
301
344
|
|
|
345
|
+
// The harness's default `retrieve` throws, so a storage read would fail
|
|
346
|
+
// this outright. Issue #415 is where applying the override to this message
|
|
347
|
+
// deliberately belongs; a sync pass is not it.
|
|
302
348
|
assert.equal(result.skippedCount, 1);
|
|
303
|
-
assert.
|
|
304
|
-
assert.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
);
|
|
308
|
-
assert.equal(harness.rows[0].category, MessageCategory.transactional);
|
|
349
|
+
assert.deepEqual(result.failedMessageIds, []);
|
|
350
|
+
assert.deepEqual(harness.messageUpdates, []);
|
|
351
|
+
assert.deepEqual(harness.threadUpdates, []);
|
|
352
|
+
assert.equal(harness.message.category, MessageCategory.uncategorized);
|
|
309
353
|
});
|
|
310
354
|
|
|
311
355
|
describe("survives a re-entrant pass with a differing override already set", () => {
|
|
@@ -5,10 +5,19 @@
|
|
|
5
5
|
* and database failures — which the same per-message frame catches — keep
|
|
6
6
|
* propagating to the requeue path, because recording one as a quarantine would
|
|
7
7
|
* tell the user that mail Remit could not reach was mail Remit could not read,
|
|
8
|
-
* and let go of it.
|
|
8
|
+
* and let go of it. That discrimination is the `error instanceof BodyParseError`
|
|
9
|
+
* branch in `syncBodies`' stream loop, and these tests drive it there.
|
|
10
|
+
*
|
|
11
|
+
* The parse cases inject the `BodyParseError` at the storage write rather than
|
|
12
|
+
* feeding mailparser bytes it refuses: mailparser accepts every byte sequence a
|
|
13
|
+
* test can construct, and the branch under test asks only what type of error
|
|
14
|
+
* reached it. That only `parseMessageBody` ever constructs one — so an instance
|
|
15
|
+
* of it really is proof the message and not the infrastructure failed — is
|
|
16
|
+
* pinned in `body-parse.test.ts`.
|
|
9
17
|
*/
|
|
10
18
|
|
|
11
19
|
import assert from "node:assert/strict";
|
|
20
|
+
import { Readable } from "node:stream";
|
|
12
21
|
import { describe, it } from "node:test";
|
|
13
22
|
import type {
|
|
14
23
|
IAddressRepository,
|
|
@@ -22,16 +31,29 @@ import type {
|
|
|
22
31
|
QuarantineUpsertInput,
|
|
23
32
|
UpdateMessageInput,
|
|
24
33
|
} from "@remit/data-ports";
|
|
34
|
+
import { NotFoundError } from "@remit/data-ports/errors";
|
|
25
35
|
import { MessageCategory } from "@remit/domain-enums";
|
|
26
36
|
import type { StorageService } from "@remit/storage-service";
|
|
37
|
+
import { BodyParseError } from "./body-parse.js";
|
|
27
38
|
import { BodySyncService } from "./body-sync.js";
|
|
28
39
|
import { QuarantineService } from "./quarantine.js";
|
|
29
|
-
|
|
30
|
-
|
|
40
|
+
import type { IImapConnection } from "./types.js";
|
|
41
|
+
|
|
42
|
+
const WELL_FORMED = Buffer.from(
|
|
43
|
+
[
|
|
44
|
+
"From: someone@example.com",
|
|
45
|
+
"To: me@example.com",
|
|
46
|
+
"Subject: hello",
|
|
47
|
+
"Content-Type: text/plain",
|
|
48
|
+
"",
|
|
49
|
+
"body",
|
|
50
|
+
].join("\r\n"),
|
|
51
|
+
);
|
|
31
52
|
|
|
32
53
|
const buildHarness = (options: {
|
|
33
|
-
retrieve?: (key: string) => Promise<Buffer>;
|
|
34
54
|
existing?: QuarantineItem[];
|
|
55
|
+
storeBodyFails?: unknown;
|
|
56
|
+
storeParsedFails?: unknown;
|
|
35
57
|
upsertFails?: boolean;
|
|
36
58
|
}) => {
|
|
37
59
|
const writes: QuarantineUpsertInput[] = [];
|
|
@@ -43,8 +65,8 @@ const buildHarness = (options: {
|
|
|
43
65
|
uid: 40217,
|
|
44
66
|
rfc822Size: 2048,
|
|
45
67
|
messageIdHeader: "<abc@example.com>",
|
|
46
|
-
bodyStorageKey: "s3://bodies/m-1",
|
|
47
68
|
category: MessageCategory.uncategorized,
|
|
69
|
+
classificationState: "NotExamined",
|
|
48
70
|
} as MessageItem;
|
|
49
71
|
|
|
50
72
|
const messageService = {
|
|
@@ -55,17 +77,32 @@ const buildHarness = (options: {
|
|
|
55
77
|
} as unknown as IMessageRepository;
|
|
56
78
|
|
|
57
79
|
const storageService = {
|
|
58
|
-
retrieve:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
80
|
+
retrieve: async () => {
|
|
81
|
+
throw new Error("the sync path never reads a body back");
|
|
82
|
+
},
|
|
83
|
+
storeMessageBodyStream: async () => {
|
|
84
|
+
if (options.storeBodyFails !== undefined) throw options.storeBodyFails;
|
|
85
|
+
return { uri: "s3://bodies/m-1" };
|
|
86
|
+
},
|
|
87
|
+
storeParsedBody: async () => {
|
|
88
|
+
if (options.storeParsedFails !== undefined)
|
|
89
|
+
throw options.storeParsedFails;
|
|
90
|
+
},
|
|
91
|
+
listBodyParts: async () => [],
|
|
63
92
|
} as unknown as StorageService;
|
|
64
93
|
|
|
65
94
|
const envelopeService = {
|
|
66
95
|
getMessageData: async () => ({ bodyPart: [], bodyPartParameter: [] }),
|
|
96
|
+
listBodyParts: async () => [],
|
|
67
97
|
} as unknown as IEnvelopeRepository;
|
|
68
98
|
|
|
99
|
+
const addressService = {
|
|
100
|
+
getAddress: async () => {
|
|
101
|
+
throw new NotFoundError("Address not found");
|
|
102
|
+
},
|
|
103
|
+
incrementInboundCount: async () => {},
|
|
104
|
+
} as unknown as IAddressRepository;
|
|
105
|
+
|
|
69
106
|
const repository = {
|
|
70
107
|
listByAccountConfigId: async () => options.existing ?? [],
|
|
71
108
|
upsert: async (input: QuarantineUpsertInput) => {
|
|
@@ -91,7 +128,7 @@ const buildHarness = (options: {
|
|
|
91
128
|
],
|
|
92
129
|
update: async () => {},
|
|
93
130
|
} as unknown as IThreadMessageRepository,
|
|
94
|
-
|
|
131
|
+
addressService,
|
|
95
132
|
envelopeService,
|
|
96
133
|
{ info: () => {}, error: () => {}, debug: () => {} },
|
|
97
134
|
undefined,
|
|
@@ -114,16 +151,27 @@ const buildHarness = (options: {
|
|
|
114
151
|
return { service, writes, messageUpdates };
|
|
115
152
|
};
|
|
116
153
|
|
|
154
|
+
const connection = () =>
|
|
155
|
+
({
|
|
156
|
+
openBox: async () => {},
|
|
157
|
+
async *fetchMessageBodies(uids: number[]) {
|
|
158
|
+
for (const uid of uids) {
|
|
159
|
+
yield { uid, source: Readable.from([WELL_FORMED]) };
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
}) as unknown as IImapConnection;
|
|
163
|
+
|
|
117
164
|
const sync = (service: BodySyncService) =>
|
|
118
|
-
service.syncBodies(["m-1"], "acc-1", "cfg-1", "INBOX", async () =>
|
|
119
|
-
|
|
120
|
-
|
|
165
|
+
service.syncBodies(["m-1"], "acc-1", "cfg-1", "INBOX", async () =>
|
|
166
|
+
connection(),
|
|
167
|
+
);
|
|
121
168
|
|
|
122
|
-
|
|
123
|
-
|
|
169
|
+
const parseRefusal = () =>
|
|
170
|
+
new BodyParseError(new Error("boundary never closed"));
|
|
124
171
|
|
|
172
|
+
describe("body sync quarantines a message the parser refuses", () => {
|
|
125
173
|
it("records the failure instead of requeueing the message forever", async () => {
|
|
126
|
-
const harness = buildHarness({
|
|
174
|
+
const harness = buildHarness({ storeBodyFails: parseRefusal() });
|
|
127
175
|
|
|
128
176
|
const result = await sync(harness.service);
|
|
129
177
|
|
|
@@ -132,7 +180,7 @@ describe("body sync quarantines a message the parser refuses", () => {
|
|
|
132
180
|
});
|
|
133
181
|
|
|
134
182
|
it("names the message by uid and UIDVALIDITY, so the record is idempotent", async () => {
|
|
135
|
-
const harness = buildHarness({
|
|
183
|
+
const harness = buildHarness({ storeBodyFails: parseRefusal() });
|
|
136
184
|
|
|
137
185
|
await sync(harness.service);
|
|
138
186
|
|
|
@@ -142,7 +190,7 @@ describe("body sync quarantines a message the parser refuses", () => {
|
|
|
142
190
|
});
|
|
143
191
|
|
|
144
192
|
it("does not mark the message synced — it was set aside, not applied", async () => {
|
|
145
|
-
const harness = buildHarness({
|
|
193
|
+
const harness = buildHarness({ storeBodyFails: parseRefusal() });
|
|
146
194
|
|
|
147
195
|
const result = await sync(harness.service);
|
|
148
196
|
|
|
@@ -154,9 +202,7 @@ describe("body sync quarantines a message the parser refuses", () => {
|
|
|
154
202
|
describe("body sync leaves infrastructure failures alone", () => {
|
|
155
203
|
it("requeues a storage failure rather than calling the message unreadable", async () => {
|
|
156
204
|
const harness = buildHarness({
|
|
157
|
-
|
|
158
|
-
throw new Error("S3 503 SlowDown");
|
|
159
|
-
},
|
|
205
|
+
storeBodyFails: new Error("S3 503 SlowDown"),
|
|
160
206
|
});
|
|
161
207
|
|
|
162
208
|
const result = await sync(harness.service);
|
|
@@ -165,15 +211,13 @@ describe("body sync leaves infrastructure failures alone", () => {
|
|
|
165
211
|
assert.deepEqual(result.failedMessageIds, ["m-1"]);
|
|
166
212
|
});
|
|
167
213
|
|
|
168
|
-
it("requeues a database failure the same way", async () => {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
error.name = "ProvisionedThroughputExceededException";
|
|
173
|
-
throw error;
|
|
174
|
-
},
|
|
175
|
-
});
|
|
214
|
+
it("requeues a database failure the same way, parse already past", async () => {
|
|
215
|
+
const throughput = new Error("ProvisionedThroughputExceededException");
|
|
216
|
+
throughput.name = "ProvisionedThroughputExceededException";
|
|
217
|
+
const harness = buildHarness({ storeParsedFails: throughput });
|
|
176
218
|
|
|
219
|
+
// The body parsed cleanly and the failure came after it, so nothing here
|
|
220
|
+
// may say the message is unreadable.
|
|
177
221
|
const result = await sync(harness.service);
|
|
178
222
|
|
|
179
223
|
assert.deepEqual(harness.writes, []);
|
|
@@ -184,7 +228,7 @@ describe("body sync leaves infrastructure failures alone", () => {
|
|
|
184
228
|
describe("body sync contains a failure to write the record", () => {
|
|
185
229
|
it("requeues the message instead of aborting the batch", async () => {
|
|
186
230
|
const harness = buildHarness({
|
|
187
|
-
|
|
231
|
+
storeBodyFails: parseRefusal(),
|
|
188
232
|
upsertFails: true,
|
|
189
233
|
});
|
|
190
234
|
|
|
@@ -208,14 +252,21 @@ describe("body sync skips what is already quarantined", () => {
|
|
|
208
252
|
uid: 40217,
|
|
209
253
|
} as QuarantineItem,
|
|
210
254
|
],
|
|
211
|
-
retrieve: async () => {
|
|
212
|
-
throw new Error("a quarantined message must not be read again");
|
|
213
|
-
},
|
|
214
255
|
});
|
|
215
256
|
|
|
216
|
-
const result = await
|
|
257
|
+
const result = await harness.service.syncBodies(
|
|
258
|
+
["m-1"],
|
|
259
|
+
"acc-1",
|
|
260
|
+
"cfg-1",
|
|
261
|
+
"INBOX",
|
|
262
|
+
async () => {
|
|
263
|
+
throw new Error("a quarantined message must not be fetched again");
|
|
264
|
+
},
|
|
265
|
+
);
|
|
217
266
|
|
|
218
267
|
assert.equal(result.skippedCount, 1);
|
|
268
|
+
assert.deepEqual(result.failedMessageIds, []);
|
|
219
269
|
assert.deepEqual(harness.writes, []);
|
|
270
|
+
assert.deepEqual(harness.messageUpdates, []);
|
|
220
271
|
});
|
|
221
272
|
});
|