@remit/imap-worker 0.0.86 → 0.0.87
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/README.md +3 -1
- package/package.json +10 -2
- package/src/account-check.ts +1 -1
- package/src/body-sync-gate.test.ts +1 -1
- package/src/body-sync-gate.ts +1 -1
- package/src/e2e-processor-shim.ts +6 -21
- package/src/emit.ts +1 -1
- package/src/failure-kind.ts +1 -1
- package/src/handlers/append-sent-message.sqlite.test.ts +176 -0
- package/src/handlers/append-sent-message.ts +3 -24
- package/src/handlers/delete-account-objects.test.ts +1 -1
- package/src/handlers/delete-account-objects.ts +1 -1
- package/src/handlers/empty-trash.sqlite.test.ts +209 -0
- package/src/handlers/empty-trash.ts +3 -24
- package/src/handlers/flag-push.ts +11 -18
- package/src/handlers/mailbox-management.sqlite.test.ts +265 -0
- package/src/handlers/mailbox-management.ts +12 -48
- package/src/handlers/message-copy.sqlite.test.ts +298 -0
- package/src/handlers/message-copy.ts +31 -49
- package/src/handlers/message-delete-terminal.ts +12 -10
- package/src/handlers/message-delete.sqlite.test.ts +291 -0
- package/src/handlers/message-delete.ts +26 -32
- package/src/handlers/message-move-terminal.ts +12 -10
- package/src/handlers/message-move.sqlite.test.ts +271 -0
- package/src/handlers/message-move.test.ts +76 -863
- package/src/handlers/message-move.ts +49 -39
- package/src/handlers/placement-move-push.test.ts +96 -329
- package/src/handlers/placement-move-push.ts +75 -29
- package/src/handlers/placement-push.sqlite.test.ts +298 -0
- package/src/handlers/probe-message-id.sqlite.test.ts +197 -0
- package/src/handlers/probe-message-id.ts +15 -0
- package/src/handlers/sync-mailboxes.test.ts +0 -1
- package/src/handlers/sync-mailboxes.ts +1 -1
- package/src/handlers/sync-message-body.sqlite.test.ts +314 -0
- package/src/handlers/sync-message-body.test.ts +3 -299
- package/src/handlers/sync-message-body.ts +21 -11
- package/src/handlers/sync-messages.sqlite.test.ts +183 -0
- package/src/handlers/sync-messages.ts +27 -33
- package/src/index.ts +5 -3
- package/src/mail-sync-selection.sqlite.test.ts +16 -18
- package/src/poller.ts +5 -4
- package/src/processor.test.ts +1 -1
- package/src/processor.ts +1 -1
- package/src/scheduler/attachment-sweep.sqlite.test.ts +74 -0
- package/src/scheduler/attachment-sweep.ts +1 -1
- package/src/scheduler/{calendar-subscriptions.test.ts → calendar-subscriptions.sqlite.test.ts} +43 -55
- package/src/scheduler/calendar-subscriptions.ts +6 -6
- package/src/scheduler/half-deleted-accounts.sqlite.test.ts +203 -0
- package/src/scheduler/handler.ts +12 -1
- package/src/scheduler/loop.test.ts +2 -0
- package/src/scheduler/run-tick.sqlite.test.ts +192 -0
- package/src/scheduler/run-tick.ts +39 -1
- package/src/scheduler/runner.ts +21 -1
- package/src/scheduler/stranded-placement-redrive.ts +265 -0
- package/src/stranded-placement.sqlite.test.ts +795 -0
- package/src/test-helpers/test-world.ts +398 -0
- package/src/thread-join.sqlite.test.ts +115 -0
- package/src/with-oauth-lifecycle-deps.sqlite.test.ts +138 -14
- package/src/with-oauth-lifecycle.ts +1 -1
- package/src/handlers/append-sent-message.test.ts +0 -529
- package/src/handlers/empty-trash.test.ts +0 -728
- package/src/handlers/flag-push-placement-race.test.ts +0 -385
- package/src/handlers/flag-push.test.ts +0 -325
- package/src/handlers/mailbox-management.test.ts +0 -850
- package/src/handlers/message-copy.test.ts +0 -654
- package/src/handlers/message-delete.test.ts +0 -1869
- package/src/handlers/message-move-terminal.test.ts +0 -412
- package/src/handlers/sync-messages.test.ts +0 -569
- package/src/scheduler/attachment-sweep.test.ts +0 -169
- package/src/scheduler/run-tick.test.ts +0 -478
- package/src/with-oauth-lifecycle.test.ts +0 -318
|
@@ -1,529 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { beforeEach, describe, it } from "node:test";
|
|
3
|
-
import { noopLogger } from "@remit/logger-lambda/noop-logger";
|
|
4
|
-
import { OutboxAttachmentUnavailableError } from "@remit/mailbox-service/outbox-attachment-content";
|
|
5
|
-
import type { AppendSentMessageEvent } from "../events.js";
|
|
6
|
-
import {
|
|
7
|
-
APPEND_SENT_MAX_ATTEMPTS,
|
|
8
|
-
type AppendSentMessageDeps,
|
|
9
|
-
handleAppendSentMessage,
|
|
10
|
-
} from "./append-sent-message.js";
|
|
11
|
-
|
|
12
|
-
interface Call {
|
|
13
|
-
method: string;
|
|
14
|
-
args: unknown[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface Harness {
|
|
18
|
-
calls: Call[];
|
|
19
|
-
account: {
|
|
20
|
-
accountId: string;
|
|
21
|
-
accountConfigId: string;
|
|
22
|
-
deletedAt?: number;
|
|
23
|
-
};
|
|
24
|
-
outbox: Record<string, unknown>;
|
|
25
|
-
sentMailbox: { mailboxId: string; fullPath: string } | null;
|
|
26
|
-
append: (
|
|
27
|
-
path: string,
|
|
28
|
-
raw: Buffer,
|
|
29
|
-
flags: string[],
|
|
30
|
-
) => Promise<{ uid: number; uidValidity: number }>;
|
|
31
|
-
/** Stand in for a terminal auth failure: withOAuthLifecycle ACKs the record
|
|
32
|
-
* without ever running the work. */
|
|
33
|
-
ackWithoutWork: boolean;
|
|
34
|
-
disconnectCount: number;
|
|
35
|
-
attachments: Array<{
|
|
36
|
-
filename: string;
|
|
37
|
-
contentType: string;
|
|
38
|
-
content: Buffer;
|
|
39
|
-
}>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let h: Harness;
|
|
43
|
-
|
|
44
|
-
const record =
|
|
45
|
-
(method: string) =>
|
|
46
|
-
async (...args: unknown[]) => {
|
|
47
|
-
h.calls.push({ method, args });
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const fresh = (): Harness => ({
|
|
51
|
-
calls: [],
|
|
52
|
-
account: { accountId: "acc-1", accountConfigId: "cfg-1" },
|
|
53
|
-
outbox: {
|
|
54
|
-
status: "sent",
|
|
55
|
-
fromName: "Alice",
|
|
56
|
-
fromAddress: "alice@example.com",
|
|
57
|
-
toAddresses: ["bob@example.com"],
|
|
58
|
-
ccAddresses: ["carol@example.com"],
|
|
59
|
-
subject: "Quarterly numbers",
|
|
60
|
-
textBody: "See attached.",
|
|
61
|
-
messageIdValue: "generated-id@example.com",
|
|
62
|
-
references: ["parent@example.com"],
|
|
63
|
-
inReplyTo: "parent@example.com",
|
|
64
|
-
sentAt: 1700000000000,
|
|
65
|
-
appendedUid: 0,
|
|
66
|
-
},
|
|
67
|
-
sentMailbox: { mailboxId: "sent-mbx", fullPath: "INBOX/Sent" },
|
|
68
|
-
append: async (path, raw, flags) => {
|
|
69
|
-
h.calls.push({ method: "connection.append", args: [path, raw, flags] });
|
|
70
|
-
return { uid: 55, uidValidity: 7 };
|
|
71
|
-
},
|
|
72
|
-
ackWithoutWork: false,
|
|
73
|
-
disconnectCount: 0,
|
|
74
|
-
attachments: [],
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const deps = (): AppendSentMessageDeps =>
|
|
78
|
-
({
|
|
79
|
-
getClient: async () => ({
|
|
80
|
-
account: {
|
|
81
|
-
get: async (accountId: string) => {
|
|
82
|
-
h.calls.push({ method: "account.get", args: [accountId] });
|
|
83
|
-
return h.account;
|
|
84
|
-
},
|
|
85
|
-
},
|
|
86
|
-
outboxMessage: {
|
|
87
|
-
get: async () => h.outbox,
|
|
88
|
-
update: record("outboxMessage.update"),
|
|
89
|
-
delete: record("outboxMessage.delete"),
|
|
90
|
-
},
|
|
91
|
-
outboxAttachment: {
|
|
92
|
-
discardAll: record("outboxAttachment.discardAll"),
|
|
93
|
-
contentsFor: async (...args: unknown[]) => {
|
|
94
|
-
h.calls.push({ method: "outboxAttachment.contentsFor", args });
|
|
95
|
-
return h.attachments;
|
|
96
|
-
},
|
|
97
|
-
},
|
|
98
|
-
mailboxSpecialUse: {
|
|
99
|
-
findSentMailbox: async () => h.sentMailbox,
|
|
100
|
-
},
|
|
101
|
-
secrets: {},
|
|
102
|
-
}),
|
|
103
|
-
buildLifecycleDeps: () => ({}),
|
|
104
|
-
withOAuthLifecycle: async (
|
|
105
|
-
_deps: unknown,
|
|
106
|
-
_account: unknown,
|
|
107
|
-
_log: unknown,
|
|
108
|
-
cb: (credentials: unknown) => Promise<void>,
|
|
109
|
-
) => (h.ackWithoutWork ? undefined : cb({})),
|
|
110
|
-
createConnectionScope: () => ({
|
|
111
|
-
getConnection: async () => ({
|
|
112
|
-
append: (path: string, raw: Buffer, flags: string[]) =>
|
|
113
|
-
h.append(path, raw, flags),
|
|
114
|
-
}),
|
|
115
|
-
disconnect: async () => {
|
|
116
|
-
h.disconnectCount += 1;
|
|
117
|
-
},
|
|
118
|
-
}),
|
|
119
|
-
}) as unknown as AppendSentMessageDeps;
|
|
120
|
-
|
|
121
|
-
const event: AppendSentMessageEvent = {
|
|
122
|
-
type: "APPEND_SENT_MESSAGE",
|
|
123
|
-
accountId: "acc-1",
|
|
124
|
-
outboxMessageId: "out-1",
|
|
125
|
-
} as AppendSentMessageEvent;
|
|
126
|
-
|
|
127
|
-
const called = (method: string): Call[] =>
|
|
128
|
-
h.calls.filter((c) => c.method === method);
|
|
129
|
-
|
|
130
|
-
const patches = (): unknown[] =>
|
|
131
|
-
called("outboxMessage.update").map((c) => c.args[2]);
|
|
132
|
-
|
|
133
|
-
type BackendClient = Awaited<ReturnType<AppendSentMessageDeps["getClient"]>>;
|
|
134
|
-
|
|
135
|
-
const depsWithFailingDelete = (): AppendSentMessageDeps => {
|
|
136
|
-
const base = deps();
|
|
137
|
-
return {
|
|
138
|
-
...base,
|
|
139
|
-
getClient: async (): Promise<BackendClient> => {
|
|
140
|
-
const client = await base.getClient();
|
|
141
|
-
return {
|
|
142
|
-
...client,
|
|
143
|
-
outboxMessage: {
|
|
144
|
-
...client.outboxMessage,
|
|
145
|
-
delete: async (): Promise<void> => {
|
|
146
|
-
throw new Error("storage down");
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
const depsWithFailingUpdate = (): AppendSentMessageDeps => {
|
|
155
|
-
const base = deps();
|
|
156
|
-
return {
|
|
157
|
-
...base,
|
|
158
|
-
getClient: async (): Promise<BackendClient> => {
|
|
159
|
-
const client = await base.getClient();
|
|
160
|
-
return {
|
|
161
|
-
...client,
|
|
162
|
-
outboxMessage: {
|
|
163
|
-
...client.outboxMessage,
|
|
164
|
-
update: async (): Promise<never> => {
|
|
165
|
-
throw new Error("storage down");
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
};
|
|
169
|
-
},
|
|
170
|
-
};
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
describe("handleAppendSentMessage", () => {
|
|
174
|
-
beforeEach(() => {
|
|
175
|
-
h = fresh();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("appends a seen RFC822 copy to the Sent folder and drops the outbox row", async () => {
|
|
179
|
-
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
180
|
-
|
|
181
|
-
const append = called("connection.append")[0];
|
|
182
|
-
assert.equal(append?.args[0], "INBOX/Sent");
|
|
183
|
-
assert.deepEqual(append?.args[2], ["\\Seen"]);
|
|
184
|
-
assert.deepEqual(called("outboxMessage.delete")[0]?.args, [
|
|
185
|
-
"cfg-1",
|
|
186
|
-
"out-1",
|
|
187
|
-
]);
|
|
188
|
-
assert.equal(h.disconnectCount, 1);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("takes the draft's stored attachments with the row (#679)", async () => {
|
|
192
|
-
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
193
|
-
|
|
194
|
-
// The row is the only reference to those objects. Sweeping has to happen
|
|
195
|
-
// here too, not only on a discard, or a sent message leaves its files
|
|
196
|
-
// behind with nothing able to reach them.
|
|
197
|
-
assert.deepEqual(called("outboxAttachment.discardAll")[0]?.args, [
|
|
198
|
-
"cfg-1",
|
|
199
|
-
"acc-1",
|
|
200
|
-
"out-1",
|
|
201
|
-
]);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("files the Sent copy with the same files the wire copy carried", async () => {
|
|
205
|
-
h.attachments = [
|
|
206
|
-
{
|
|
207
|
-
filename: "numbers.csv",
|
|
208
|
-
contentType: "text/csv",
|
|
209
|
-
content: Buffer.from("q1,q2\n1,2\n"),
|
|
210
|
-
},
|
|
211
|
-
];
|
|
212
|
-
|
|
213
|
-
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
214
|
-
|
|
215
|
-
assert.deepEqual(called("outboxAttachment.contentsFor")[0]?.args, [
|
|
216
|
-
"cfg-1",
|
|
217
|
-
"acc-1",
|
|
218
|
-
"out-1",
|
|
219
|
-
]);
|
|
220
|
-
const raw = String(called("connection.append")[0]?.args[1] as Buffer);
|
|
221
|
-
assert.match(raw, /^Content-Type: multipart\/mixed;/m);
|
|
222
|
-
assert.match(
|
|
223
|
-
raw,
|
|
224
|
-
/^Content-Disposition: attachment; filename=numbers\.csv$/m,
|
|
225
|
-
);
|
|
226
|
-
assert.match(raw, /^Content-Type: text\/csv; name=numbers\.csv$/m);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
const unreadable = new OutboxAttachmentUnavailableError(
|
|
230
|
-
"numbers.csv",
|
|
231
|
-
"nothing is stored at key/numbers",
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
const depsWithUnreadableFile = (): AppendSentMessageDeps => {
|
|
235
|
-
const base = deps();
|
|
236
|
-
return {
|
|
237
|
-
...base,
|
|
238
|
-
getClient: async (): Promise<BackendClient> => {
|
|
239
|
-
const client = await base.getClient();
|
|
240
|
-
client.outboxAttachment.contentsFor = async () => {
|
|
241
|
-
throw unreadable;
|
|
242
|
-
};
|
|
243
|
-
return client;
|
|
244
|
-
},
|
|
245
|
-
};
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
it("retries a copy whose files could not be read, below the budget", async () => {
|
|
249
|
-
const failing = depsWithUnreadableFile();
|
|
250
|
-
|
|
251
|
-
await assert.rejects(
|
|
252
|
-
() => handleAppendSentMessage(event, noopLogger, 1, failing),
|
|
253
|
-
unreadable,
|
|
254
|
-
);
|
|
255
|
-
assert.equal(called("connection.append").length, 0);
|
|
256
|
-
assert.deepEqual(patches(), [], "the row is left for the retry");
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it("settles a delivered message whose files cannot be read as sent, not filed, at the budget", async () => {
|
|
260
|
-
const failing = depsWithUnreadableFile();
|
|
261
|
-
|
|
262
|
-
await handleAppendSentMessage(
|
|
263
|
-
event,
|
|
264
|
-
noopLogger,
|
|
265
|
-
APPEND_SENT_MAX_ATTEMPTS,
|
|
266
|
-
failing,
|
|
267
|
-
);
|
|
268
|
-
assert.deepEqual(patches(), [
|
|
269
|
-
{
|
|
270
|
-
status: "unfiled",
|
|
271
|
-
lastError:
|
|
272
|
-
'Sent, but not filed: the copy for INBOX/Sent could not be built because "numbers.csv" could not be read: nothing is stored at key/numbers.',
|
|
273
|
-
},
|
|
274
|
-
]);
|
|
275
|
-
assert.equal(called("connection.append").length, 0);
|
|
276
|
-
assert.equal(
|
|
277
|
-
called("outboxMessage.delete").length,
|
|
278
|
-
0,
|
|
279
|
-
"the row stays in the Outbox with its reason",
|
|
280
|
-
);
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it("builds the message from the outbox row's own headers", async () => {
|
|
284
|
-
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
285
|
-
|
|
286
|
-
const raw = String(called("connection.append")[0]?.args[1] as Buffer);
|
|
287
|
-
assert.match(raw, /^From: Alice <alice@example\.com>$/m);
|
|
288
|
-
assert.match(raw, /^To: bob@example\.com$/m);
|
|
289
|
-
assert.match(raw, /^Cc: carol@example\.com$/m);
|
|
290
|
-
assert.match(raw, /^Subject: Quarterly numbers$/m);
|
|
291
|
-
assert.match(raw, /^Message-ID: <generated-id@example\.com>$/m);
|
|
292
|
-
assert.match(raw, /^In-Reply-To: <parent@example\.com>$/m);
|
|
293
|
-
assert.ok(raw.includes("See attached."));
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it("uses a bare address when the outbox row carries no display name", async () => {
|
|
297
|
-
h.outbox = { ...h.outbox, fromName: undefined };
|
|
298
|
-
|
|
299
|
-
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
300
|
-
|
|
301
|
-
const raw = String(called("connection.append")[0]?.args[1] as Buffer);
|
|
302
|
-
assert.match(raw, /^From: alice@example\.com$/m);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it("settles the row as unfiled when the account has no Sent folder at all", async () => {
|
|
306
|
-
h.sentMailbox = null;
|
|
307
|
-
|
|
308
|
-
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
309
|
-
|
|
310
|
-
assert.equal(called("connection.append").length, 0);
|
|
311
|
-
assert.equal(called("outboxMessage.delete").length, 0);
|
|
312
|
-
|
|
313
|
-
// The message was delivered over SMTP. Leaving the row at `sent` hides it
|
|
314
|
-
// from the Outbox list and it exists in no server folder either, so the
|
|
315
|
-
// user loses it entirely.
|
|
316
|
-
const update = called("outboxMessage.update")[0];
|
|
317
|
-
assert.deepEqual(update?.args.slice(0, 2), ["cfg-1", "out-1"]);
|
|
318
|
-
const patch = update?.args[2] as { status: string; lastError: string };
|
|
319
|
-
assert.equal(patch.status, "unfiled");
|
|
320
|
-
assert.match(patch.lastError, /no folder appointed to the Sent role/);
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it("skips the append while the outbox row is not yet sent", async () => {
|
|
324
|
-
h.outbox = { ...h.outbox, status: "pending" };
|
|
325
|
-
|
|
326
|
-
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
327
|
-
|
|
328
|
-
assert.equal(called("connection.append").length, 0);
|
|
329
|
-
assert.equal(called("outboxMessage.delete").length, 0);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it("returns early without touching the outbox when the account is soft-deleted", async () => {
|
|
333
|
-
h.account = { ...h.account, deletedAt: Date.now() };
|
|
334
|
-
|
|
335
|
-
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
336
|
-
|
|
337
|
-
assert.equal(called("connection.append").length, 0);
|
|
338
|
-
assert.equal(called("outboxMessage.delete").length, 0);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
it("rethrows a failed APPEND while the queue still has attempts left", async () => {
|
|
342
|
-
h.append = async () => {
|
|
343
|
-
throw new Error("server exploded");
|
|
344
|
-
};
|
|
345
|
-
|
|
346
|
-
await assert.rejects(
|
|
347
|
-
handleAppendSentMessage(event, noopLogger, 1, deps()),
|
|
348
|
-
/server exploded/,
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
// Nothing settled: the row stays `sent` and the redelivered event tries
|
|
352
|
-
// the APPEND again.
|
|
353
|
-
assert.equal(called("outboxMessage.delete").length, 0);
|
|
354
|
-
assert.equal(called("outboxMessage.update").length, 0);
|
|
355
|
-
assert.equal(h.disconnectCount, 1);
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
it("settles the row as unfiled on the last attempt instead of dead-lettering it", async () => {
|
|
359
|
-
h.append = async () => {
|
|
360
|
-
throw new Error("server exploded");
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
await handleAppendSentMessage(
|
|
364
|
-
event,
|
|
365
|
-
noopLogger,
|
|
366
|
-
APPEND_SENT_MAX_ATTEMPTS,
|
|
367
|
-
deps(),
|
|
368
|
-
);
|
|
369
|
-
|
|
370
|
-
// A DLQ'd APPEND is how the message went missing: the row would keep the
|
|
371
|
-
// `sent` status the Outbox list hides, and no Sent folder holds it.
|
|
372
|
-
assert.equal(called("outboxMessage.delete").length, 0);
|
|
373
|
-
const patch = called("outboxMessage.update")[0]?.args[2] as {
|
|
374
|
-
status: string;
|
|
375
|
-
lastError: string;
|
|
376
|
-
};
|
|
377
|
-
assert.equal(patch.status, "unfiled");
|
|
378
|
-
assert.match(patch.lastError, /server exploded/);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it("settles the row as unfiled when a terminal auth failure acks the record", async () => {
|
|
382
|
-
h.ackWithoutWork = true;
|
|
383
|
-
|
|
384
|
-
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
385
|
-
|
|
386
|
-
const patch = called("outboxMessage.update")[0]?.args[2] as {
|
|
387
|
-
status: string;
|
|
388
|
-
lastError: string;
|
|
389
|
-
};
|
|
390
|
-
assert.equal(patch.status, "unfiled");
|
|
391
|
-
assert.match(patch.lastError, /signed in again/);
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
it("leaves the row alone when the APPEND landed but the delete did not", async () => {
|
|
395
|
-
await assert.rejects(
|
|
396
|
-
handleAppendSentMessage(event, noopLogger, 1, depsWithFailingDelete()),
|
|
397
|
-
/storage down/,
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
// The copy is in Sent, so the message is findable — settling it as unfiled
|
|
401
|
-
// would say the opposite.
|
|
402
|
-
assert.deepEqual(patches(), [{ appendedUid: 55 }]);
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
it("stops redelivering a landed APPEND at the budget instead of filing another copy", async () => {
|
|
406
|
-
await handleAppendSentMessage(
|
|
407
|
-
event,
|
|
408
|
-
noopLogger,
|
|
409
|
-
APPEND_SENT_MAX_ATTEMPTS,
|
|
410
|
-
depsWithFailingDelete(),
|
|
411
|
-
);
|
|
412
|
-
|
|
413
|
-
assert.equal(called("connection.append").length, 1);
|
|
414
|
-
assert.deepEqual(patches(), [{ appendedUid: 55 }]);
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
it("records the uid the APPEND produced before it deletes the row", async () => {
|
|
418
|
-
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
419
|
-
|
|
420
|
-
// Order is the whole of it: a uid written after the delete is a uid a
|
|
421
|
-
// redelivery never sees, and the redelivery is what files the second copy.
|
|
422
|
-
const order = h.calls.map((c) => c.method);
|
|
423
|
-
assert.deepEqual(patches(), [{ appendedUid: 55 }]);
|
|
424
|
-
assert.ok(
|
|
425
|
-
order.indexOf("outboxMessage.update") <
|
|
426
|
-
order.indexOf("outboxMessage.delete"),
|
|
427
|
-
);
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
it("records a copy the server filed but named no uid for", async () => {
|
|
431
|
-
// UIDPLUS is an extension. Without it the APPEND succeeds and reports
|
|
432
|
-
// nothing, and "filed" still has to be told apart from "not filed".
|
|
433
|
-
h.append = async () => ({ uid: 0, uidValidity: 0 });
|
|
434
|
-
|
|
435
|
-
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
436
|
-
|
|
437
|
-
assert.deepEqual(patches(), [{ appendedUid: -1 }]);
|
|
438
|
-
assert.equal(called("outboxMessage.delete").length, 1);
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
it("files nothing a second time when a redelivery finds a recorded uid (#858)", async () => {
|
|
442
|
-
h.outbox = { ...h.outbox, appendedUid: 55 };
|
|
443
|
-
|
|
444
|
-
await handleAppendSentMessage(event, noopLogger, 1, deps());
|
|
445
|
-
|
|
446
|
-
// The copy is already in the user's Sent folder. All this redelivery owes
|
|
447
|
-
// is the delete the last attempt could not make.
|
|
448
|
-
assert.equal(called("connection.append").length, 0);
|
|
449
|
-
assert.equal(h.disconnectCount, 0);
|
|
450
|
-
assert.deepEqual(called("outboxMessage.delete")[0]?.args, [
|
|
451
|
-
"cfg-1",
|
|
452
|
-
"out-1",
|
|
453
|
-
]);
|
|
454
|
-
assert.deepEqual(called("outboxAttachment.discardAll")[0]?.args, [
|
|
455
|
-
"cfg-1",
|
|
456
|
-
"acc-1",
|
|
457
|
-
"out-1",
|
|
458
|
-
]);
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
it("retries the delete a redelivery owes while the queue has attempts left", async () => {
|
|
462
|
-
h.outbox = { ...h.outbox, appendedUid: 55 };
|
|
463
|
-
|
|
464
|
-
await assert.rejects(
|
|
465
|
-
handleAppendSentMessage(event, noopLogger, 1, depsWithFailingDelete()),
|
|
466
|
-
/storage down/,
|
|
467
|
-
);
|
|
468
|
-
|
|
469
|
-
assert.equal(called("connection.append").length, 0);
|
|
470
|
-
assert.deepEqual(patches(), []);
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
it("keeps the row when the uid cannot be recorded", async () => {
|
|
474
|
-
await assert.rejects(
|
|
475
|
-
handleAppendSentMessage(event, noopLogger, 1, depsWithFailingUpdate()),
|
|
476
|
-
/storage down/,
|
|
477
|
-
);
|
|
478
|
-
|
|
479
|
-
// The other half of "written before the delete, never after". A row
|
|
480
|
-
// deleted without its uid is a row a redelivery reads as never filed.
|
|
481
|
-
assert.equal(called("outboxMessage.delete").length, 0);
|
|
482
|
-
assert.equal(called("outboxAttachment.discardAll").length, 0);
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
it("acks an event whose outbox row is already gone", async () => {
|
|
486
|
-
// Its own last attempt deleted it, or the boot repair dropped it. Throwing
|
|
487
|
-
// the same NotFoundError on every redelivery only dead-letters the record.
|
|
488
|
-
const notFound = Object.assign(new Error("gone"), {
|
|
489
|
-
name: "NotFoundError",
|
|
490
|
-
});
|
|
491
|
-
const base = deps();
|
|
492
|
-
const failing: AppendSentMessageDeps = {
|
|
493
|
-
...base,
|
|
494
|
-
getClient: async (): Promise<BackendClient> => {
|
|
495
|
-
const client = await base.getClient();
|
|
496
|
-
return {
|
|
497
|
-
...client,
|
|
498
|
-
outboxMessage: {
|
|
499
|
-
...client.outboxMessage,
|
|
500
|
-
get: async () => {
|
|
501
|
-
throw notFound;
|
|
502
|
-
},
|
|
503
|
-
},
|
|
504
|
-
} as BackendClient;
|
|
505
|
-
},
|
|
506
|
-
};
|
|
507
|
-
|
|
508
|
-
await handleAppendSentMessage(event, noopLogger, 1, failing);
|
|
509
|
-
|
|
510
|
-
assert.equal(called("connection.append").length, 0);
|
|
511
|
-
assert.deepEqual(patches(), []);
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
it("gives up on that delete at the budget rather than dead-lettering it", async () => {
|
|
515
|
-
h.outbox = { ...h.outbox, appendedUid: 55 };
|
|
516
|
-
|
|
517
|
-
await handleAppendSentMessage(
|
|
518
|
-
event,
|
|
519
|
-
noopLogger,
|
|
520
|
-
APPEND_SENT_MAX_ATTEMPTS,
|
|
521
|
-
depsWithFailingDelete(),
|
|
522
|
-
);
|
|
523
|
-
|
|
524
|
-
// The row stays `sent` and hidden, which the boot-time repair drops on the
|
|
525
|
-
// strength of the same recorded uid. Settling it as unfiled would tell the
|
|
526
|
-
// user a message sitting in Sent was never filed.
|
|
527
|
-
assert.deepEqual(patches(), []);
|
|
528
|
-
});
|
|
529
|
-
});
|