@remit/mailbox-service 0.0.110 → 0.0.112
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 +21 -6
- package/src/body-part-mapper.ts +51 -1
- package/src/body-sync-queue.test.ts +39 -99
- package/src/body-sync-terminal.test.ts +115 -277
- package/src/body-sync-terminal.ts +10 -12
- package/src/body-sync.test.ts +925 -0
- package/src/body-sync.ts +226 -200
- package/src/classification-backfill.test.ts +197 -527
- package/src/classify-message.test.ts +11 -74
- package/src/external-move.test.ts +102 -209
- package/src/external-move.ts +13 -0
- package/src/filter-operations.test.ts +319 -0
- package/src/filter-operations.ts +377 -0
- package/src/filter-update.test.ts +313 -0
- package/src/filters/filter-config.test.ts +55 -197
- package/src/filters/filter-config.ts +4 -39
- package/src/filters/match.test.ts +1 -38
- package/src/filters/match.ts +3 -52
- package/src/filters/pipeline.test.ts +158 -289
- package/src/filters/pipeline.ts +56 -170
- package/src/flag-push-terminal.ts +9 -11
- package/src/flag-push.test.ts +76 -372
- package/src/flag-queue.test.ts +197 -0
- package/src/folder-operations.test.ts +273 -0
- package/src/folder-operations.ts +217 -0
- package/src/forwarded-message.test.ts +122 -0
- package/src/imapflow-connect-retry.test.ts +76 -113
- package/src/imapflow-connection.test.ts +34 -0
- package/src/imapflow-connection.ts +19 -15
- package/src/index.ts +59 -12
- package/src/list-id-backfill.test.ts +133 -499
- package/src/mailbox-cursor.test.ts +81 -189
- package/src/mailbox-delete-intent.test.ts +78 -205
- package/src/mailbox-intent.ts +5 -3
- package/src/mailbox-management.test.ts +107 -220
- package/src/mailbox-management.ts +1 -2
- package/src/mailbox-presence.test.ts +36 -34
- package/src/mailbox-queue.ts +2 -20
- package/src/mailbox-rename-intent.test.ts +156 -325
- package/src/mailbox-rename-subtree.test.ts +205 -582
- package/src/mailbox-sync.test.ts +182 -744
- package/src/message-move.test.ts +726 -0
- package/src/message-move.ts +227 -57
- package/src/message-read.ts +87 -0
- package/src/message-save-retry.test.ts +90 -0
- package/src/message-sync-flags.test.ts +10 -1
- package/src/message-sync.test.ts +762 -0
- package/src/message-sync.ts +409 -239
- package/src/mime-walker.ts +16 -15
- package/src/organize-job.ts +93 -0
- package/src/outbox-attachment.test.ts +97 -209
- package/src/outbox-failed-recovery.test.ts +198 -269
- package/src/outbox-queue.test.ts +24 -0
- package/src/outbox-queue.ts +25 -8
- package/src/ownership.ts +140 -0
- package/src/placement-move-terminal.ts +9 -12
- package/src/placement-move.ts +33 -6
- package/src/placement-settled.ts +28 -4
- package/src/placement.test.ts +404 -0
- package/src/quarantine.test.ts +90 -148
- package/src/quarantine.ts +3 -2
- package/src/refusal.ts +29 -0
- package/src/restore-source-placement.ts +0 -4
- package/src/spam-report.test.ts +238 -710
- package/src/sync-warning.test.ts +213 -0
- package/src/sync-warning.ts +56 -0
- package/src/test-helpers/fake-imap.ts +690 -0
- package/src/test-helpers/test-queue.ts +56 -0
- package/src/test-helpers/test-store.ts +299 -0
- package/src/thread-root.test.ts +29 -0
- package/src/thread-root.ts +51 -0
- package/src/types/mailparser-augment.d.ts +8 -0
- package/src/types.ts +1 -1
- package/src/adversarial-mime.e2e.test.ts +0 -307
- package/src/body-sync-auto-read-writeonce.test.ts +0 -219
- package/src/body-sync-calendar-suggestion.test.ts +0 -433
- package/src/body-sync-category-denormalize.test.ts +0 -352
- package/src/body-sync-category-override.test.ts +0 -501
- package/src/body-sync-category-writeonce.test.ts +0 -234
- package/src/body-sync-filter.test.ts +0 -237
- package/src/body-sync-placement-flags.test.ts +0 -347
- package/src/body-sync-placement-writeonce.test.ts +0 -337
- package/src/body-sync-quarantine.test.ts +0 -276
- package/src/body-sync-resync.test.ts +0 -605
- package/src/body-sync-unclassifiable-once.test.ts +0 -260
- package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
- package/src/filters/anchor-drift.ts +0 -55
- package/src/filters/message-embedder.test.ts +0 -39
- package/src/filters/message-embedder.ts +0 -33
- package/src/flag-push-terminal.test.ts +0 -209
- package/src/flag-queue-stars.test.ts +0 -171
- package/src/imapflow-connection.e2e.test.ts +0 -335
- package/src/imapflow-connection.integ.test.ts +0 -316
- package/src/message-delete-junk-reconcile.test.ts +0 -117
- package/src/message-delete-no-trash.test.ts +0 -245
- package/src/message-delete-unsettled-placement.test.ts +0 -396
- package/src/message-move-copy.test.ts +0 -562
- package/src/message-move-empty-trash.test.ts +0 -326
- package/src/message-move-same-mailbox.test.ts +0 -139
- package/src/message-move-unsettled-placement.test.ts +0 -378
- package/src/message-sync-address-spoof.test.ts +0 -293
- package/src/message-sync-category.test.ts +0 -142
- package/src/message-sync-changedsince.test.ts +0 -840
- package/src/message-sync-junk-harvest.test.ts +0 -244
- package/src/message-sync-quarantine.test.ts +0 -416
- package/src/message-sync-repoint.test.ts +0 -419
- package/src/outbox-queue-settled-row.test.ts +0 -140
- package/src/pass-through-unit-of-work.ts +0 -17
- package/src/placement-move-terminal.test.ts +0 -300
- package/src/placement-move-unsettled.test.ts +0 -275
- package/src/placement-transition-race.test.ts +0 -482
- package/src/stale-message-reconcile.test.ts +0 -82
- package/src/stale-message-reconcile.ts +0 -55
- package/src/test-helpers/isolated-mailbox.ts +0 -44
- package/src/test-helpers/mailfuzz-connection.ts +0 -27
- package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
- package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
- package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
- package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
- package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
- package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
- package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
- package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +0 -18
package/src/flag-push.test.ts
CHANGED
|
@@ -1,408 +1,112 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it
|
|
3
|
-
import type { IMessageFlagPushRepository } from "@remit/data-ports";
|
|
2
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
4
3
|
import { MessageSystemFlag } from "@remit/domain-enums";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
_sent: sent,
|
|
21
|
-
};
|
|
4
|
+
import { FlagPushService } from "./flag-push.js";
|
|
5
|
+
import {
|
|
6
|
+
captureQueue,
|
|
7
|
+
openStore,
|
|
8
|
+
type QueueCapture,
|
|
9
|
+
type Store,
|
|
10
|
+
} from "./test-helpers/test-store.js";
|
|
11
|
+
|
|
12
|
+
const target = {
|
|
13
|
+
accountId: "acc-1",
|
|
14
|
+
accountConfigId: "cfg-1",
|
|
15
|
+
messageId: "msg-1",
|
|
16
|
+
mailboxId: "mbx-1",
|
|
17
|
+
flagName: MessageSystemFlag.Seen,
|
|
18
|
+
operation: "add" as const,
|
|
22
19
|
};
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
markerStateUpdates: Array<{
|
|
28
|
-
messageId: string;
|
|
29
|
-
flagName: string;
|
|
30
|
-
state: string;
|
|
31
|
-
}>;
|
|
32
|
-
mockSqs: ReturnType<typeof createMockSqs>;
|
|
33
|
-
logs: { info: unknown[]; error: unknown[] };
|
|
34
|
-
}
|
|
21
|
+
let store: Store;
|
|
22
|
+
let capture: QueueCapture;
|
|
23
|
+
let errors: Array<Record<string, unknown>>;
|
|
35
24
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}): Harness => {
|
|
42
|
-
const markerPuts: Array<Record<string, unknown>> = [];
|
|
43
|
-
const markerStateUpdates: Array<{
|
|
44
|
-
messageId: string;
|
|
45
|
-
flagName: string;
|
|
46
|
-
state: string;
|
|
47
|
-
}> = [];
|
|
48
|
-
const logs = { info: [] as unknown[], error: [] as unknown[] };
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
store = await openStore();
|
|
27
|
+
capture = captureQueue();
|
|
28
|
+
errors = [];
|
|
29
|
+
});
|
|
49
30
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
...input,
|
|
55
|
-
state: "pending",
|
|
56
|
-
createdAt: 1,
|
|
57
|
-
updatedAt: 1,
|
|
58
|
-
} as never;
|
|
59
|
-
}),
|
|
60
|
-
find: mock.fn(async () =>
|
|
61
|
-
opts?.findImpl ? ((await opts.findImpl()) as never) : null,
|
|
62
|
-
),
|
|
63
|
-
updateState: mock.fn(
|
|
64
|
-
async (id: string, flagName: string, state: string) => {
|
|
65
|
-
if (opts?.updateStateImpl) return opts.updateStateImpl() as never;
|
|
66
|
-
markerStateUpdates.push({ messageId: id, flagName, state });
|
|
67
|
-
return {} as never;
|
|
68
|
-
},
|
|
69
|
-
),
|
|
70
|
-
delete: mock.fn(async () => {}),
|
|
71
|
-
listByAccountId: mock.fn(async () => []),
|
|
72
|
-
listByMailboxId: mock.fn(async () => []),
|
|
73
|
-
};
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
capture.restore();
|
|
33
|
+
await store.close();
|
|
34
|
+
});
|
|
74
35
|
|
|
75
|
-
|
|
36
|
+
const queueAnswers = (answer: () => Promise<unknown>) => capture.answer(answer);
|
|
76
37
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
38
|
+
const flips = (queueUrl = "http://localhost:9324/000/remit-flags") =>
|
|
39
|
+
new FlagPushService({
|
|
40
|
+
markerService: store.flagPush,
|
|
41
|
+
sqsQueueUrl: queueUrl,
|
|
80
42
|
logger: {
|
|
81
|
-
info: (
|
|
82
|
-
|
|
83
|
-
},
|
|
84
|
-
error: (fields: Record<string, unknown>) => {
|
|
85
|
-
logs.error.push(fields);
|
|
86
|
-
},
|
|
43
|
+
info: () => {},
|
|
44
|
+
error: (fields: Record<string, unknown>) => errors.push(fields),
|
|
87
45
|
},
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const service = new FlagPushService(config);
|
|
91
|
-
// @ts-expect-error - inject mock SQS client for testing
|
|
92
|
-
service.sqs = mockSqs;
|
|
93
|
-
|
|
94
|
-
return { service, markerPuts, markerStateUpdates, mockSqs, logs };
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
describe("FlagPushService.flip", () => {
|
|
98
|
-
it("persists a pending marker before enqueueing the wake-up hint", async () => {
|
|
99
|
-
const { service, markerPuts } = createHarness();
|
|
100
|
-
|
|
101
|
-
await service.flip({
|
|
102
|
-
accountId,
|
|
103
|
-
accountConfigId,
|
|
104
|
-
messageId,
|
|
105
|
-
mailboxId,
|
|
106
|
-
flagName: MessageSystemFlag.Seen,
|
|
107
|
-
operation: "add",
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
assert.equal(markerPuts.length, 1);
|
|
111
|
-
assert.deepEqual(markerPuts[0], {
|
|
112
|
-
messageId,
|
|
113
|
-
flagName: MessageSystemFlag.Seen,
|
|
114
|
-
accountId,
|
|
115
|
-
accountConfigId,
|
|
116
|
-
mailboxId,
|
|
117
|
-
operation: "add",
|
|
118
|
-
});
|
|
119
46
|
});
|
|
120
47
|
|
|
121
|
-
|
|
122
|
-
const { service, markerStateUpdates } = createHarness();
|
|
123
|
-
|
|
124
|
-
await service.flip({
|
|
125
|
-
accountId,
|
|
126
|
-
accountConfigId,
|
|
127
|
-
messageId,
|
|
128
|
-
mailboxId,
|
|
129
|
-
flagName: MessageSystemFlag.Flagged,
|
|
130
|
-
operation: "remove",
|
|
131
|
-
});
|
|
48
|
+
const marker = () => store.flagPush.find(target.messageId, target.flagName);
|
|
132
49
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
});
|
|
50
|
+
describe("FlagPushService.flip", () => {
|
|
51
|
+
it("records the owed push and marks it queued once the hint is sent", async () => {
|
|
52
|
+
queueAnswers(async () => ({}));
|
|
137
53
|
|
|
138
|
-
|
|
139
|
-
const { service } = createHarness({
|
|
140
|
-
sqsImpl: () => {
|
|
141
|
-
throw Object.assign(new Error(""), {
|
|
142
|
-
name: "AggregateError",
|
|
143
|
-
code: "ECONNREFUSED",
|
|
144
|
-
});
|
|
145
|
-
},
|
|
146
|
-
});
|
|
54
|
+
await flips().flip(target);
|
|
147
55
|
|
|
148
|
-
await
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
messageId,
|
|
153
|
-
mailboxId,
|
|
154
|
-
flagName: MessageSystemFlag.Seen,
|
|
155
|
-
operation: "add",
|
|
156
|
-
}),
|
|
157
|
-
);
|
|
56
|
+
const recorded = await marker();
|
|
57
|
+
assert.equal(recorded?.operation, "add");
|
|
58
|
+
assert.equal(recorded?.mailboxId, target.mailboxId);
|
|
59
|
+
assert.equal(recorded?.state, "queued");
|
|
158
60
|
});
|
|
159
61
|
|
|
160
|
-
it("
|
|
161
|
-
|
|
162
|
-
sqsImpl: () => {
|
|
163
|
-
throw new Error("ECONNREFUSED");
|
|
164
|
-
},
|
|
165
|
-
});
|
|
62
|
+
it("names the message and flag in the hint, never the operation or a uid", async () => {
|
|
63
|
+
queueAnswers(async () => ({}));
|
|
166
64
|
|
|
167
|
-
await
|
|
168
|
-
accountId,
|
|
169
|
-
accountConfigId,
|
|
170
|
-
messageId,
|
|
171
|
-
mailboxId,
|
|
172
|
-
flagName: MessageSystemFlag.Seen,
|
|
173
|
-
operation: "add",
|
|
174
|
-
});
|
|
65
|
+
await flips().flip(target);
|
|
175
66
|
|
|
176
|
-
|
|
67
|
+
const event = JSON.parse(capture.inputs()[0].MessageBody ?? "{}");
|
|
68
|
+
assert.equal(event.type, "FLAG_PUSH");
|
|
69
|
+
assert.equal(event.messageId, target.messageId);
|
|
70
|
+
assert.equal(event.flagName, target.flagName);
|
|
71
|
+
assert.equal("operation" in event, false);
|
|
72
|
+
assert.equal("uid" in event, false);
|
|
177
73
|
});
|
|
178
74
|
|
|
179
|
-
it("
|
|
180
|
-
|
|
181
|
-
sqsImpl: () => {
|
|
182
|
-
throw Object.assign(new Error(""), {
|
|
183
|
-
name: "AggregateError",
|
|
184
|
-
code: "ECONNREFUSED",
|
|
185
|
-
});
|
|
186
|
-
},
|
|
187
|
-
});
|
|
75
|
+
it("groups a FIFO hint by account and leaves a standard queue ungrouped", async () => {
|
|
76
|
+
queueAnswers(async () => ({}));
|
|
188
77
|
|
|
189
|
-
await
|
|
190
|
-
|
|
191
|
-
accountConfigId,
|
|
192
|
-
messageId,
|
|
193
|
-
mailboxId,
|
|
194
|
-
flagName: MessageSystemFlag.Seen,
|
|
195
|
-
operation: "add",
|
|
196
|
-
});
|
|
78
|
+
await flips("http://localhost:9324/000/remit-flags.fifo").flip(target);
|
|
79
|
+
await flips("http://localhost:9324/000/remit-flags").flip(target);
|
|
197
80
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
(entry as { alert?: string }).alert === "flag_push_hint_enqueue_failed",
|
|
201
|
-
);
|
|
202
|
-
assert.ok(
|
|
203
|
-
alerted,
|
|
204
|
-
"expected an alertable flag_push_hint_enqueue_failed log",
|
|
205
|
-
);
|
|
206
|
-
assert.equal((alerted as { errorCode?: string }).errorCode, "ECONNREFUSED");
|
|
81
|
+
assert.equal(capture.inputs()[0].MessageGroupId, target.accountId);
|
|
82
|
+
assert.equal(capture.inputs()[1].MessageGroupId, undefined);
|
|
207
83
|
});
|
|
208
84
|
|
|
209
|
-
it("
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
};
|
|
214
|
-
process.on("unhandledRejection", onUnhandled);
|
|
215
|
-
|
|
216
|
-
const { service } = createHarness({
|
|
217
|
-
sqsImpl: () =>
|
|
218
|
-
new Promise((_resolve, reject) => {
|
|
219
|
-
setImmediate(() =>
|
|
220
|
-
reject(
|
|
221
|
-
Object.assign(new Error(""), {
|
|
222
|
-
name: "AggregateError",
|
|
223
|
-
code: "ECONNREFUSED",
|
|
224
|
-
}),
|
|
225
|
-
),
|
|
226
|
-
);
|
|
227
|
-
}),
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
try {
|
|
231
|
-
void service.flip({
|
|
232
|
-
accountId,
|
|
233
|
-
accountConfigId,
|
|
234
|
-
messageId,
|
|
235
|
-
mailboxId,
|
|
236
|
-
flagName: MessageSystemFlag.Seen,
|
|
237
|
-
operation: "add",
|
|
85
|
+
it("keeps the push owed and alarms, without rejecting, when the queue is down", async () => {
|
|
86
|
+
queueAnswers(async () => {
|
|
87
|
+
throw Object.assign(new Error("connect ECONNREFUSED"), {
|
|
88
|
+
code: "ECONNREFUSED",
|
|
238
89
|
});
|
|
239
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
240
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
241
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
242
|
-
} finally {
|
|
243
|
-
process.off("unhandledRejection", onUnhandled);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
assert.equal(leaked.length, 0);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it("sets MessageGroupId=accountId for FIFO queue URLs", async () => {
|
|
250
|
-
const { service, mockSqs } = createHarness({
|
|
251
|
-
queueUrl:
|
|
252
|
-
"https://sqs.eu-west-1.amazonaws.com/123456789012/remit-dev-mailboxes.fifo",
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
await service.flip({
|
|
256
|
-
accountId,
|
|
257
|
-
accountConfigId,
|
|
258
|
-
messageId,
|
|
259
|
-
mailboxId,
|
|
260
|
-
flagName: MessageSystemFlag.Seen,
|
|
261
|
-
operation: "add",
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
assert.equal(mockSqs._sent.length, 1);
|
|
265
|
-
const sent = mockSqs._sent[0] as { MessageGroupId?: string };
|
|
266
|
-
assert.equal(sent.MessageGroupId, accountId);
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it("omits FIFO params for standard queue URLs", async () => {
|
|
270
|
-
const { service, mockSqs } = createHarness({
|
|
271
|
-
queueUrl:
|
|
272
|
-
"https://sqs.eu-west-1.amazonaws.com/123456789012/remit-dev-mailboxes",
|
|
273
90
|
});
|
|
274
91
|
|
|
275
|
-
await
|
|
276
|
-
accountId,
|
|
277
|
-
accountConfigId,
|
|
278
|
-
messageId,
|
|
279
|
-
mailboxId,
|
|
280
|
-
flagName: MessageSystemFlag.Seen,
|
|
281
|
-
operation: "add",
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
assert.equal(mockSqs._sent.length, 1);
|
|
285
|
-
const sent = mockSqs._sent[0] as { MessageGroupId?: string };
|
|
286
|
-
assert.equal(sent.MessageGroupId, undefined);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("event body carries only messageId + flagName — never the operation or a UID", async () => {
|
|
290
|
-
const { service, mockSqs } = createHarness();
|
|
92
|
+
await flips().flip(target);
|
|
291
93
|
|
|
292
|
-
await
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
flagName: MessageSystemFlag.Flagged,
|
|
298
|
-
operation: "remove",
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
const sent = mockSqs._sent[0] as { MessageBody: string };
|
|
302
|
-
const event = JSON.parse(sent.MessageBody);
|
|
303
|
-
assert.equal(event.type, "FLAG_PUSH");
|
|
304
|
-
assert.equal(event.messageId, messageId);
|
|
305
|
-
assert.equal(event.flagName, MessageSystemFlag.Flagged);
|
|
306
|
-
assert.equal(event.accountId, accountId);
|
|
307
|
-
assert.equal(event.accountConfigId, accountConfigId);
|
|
308
|
-
assert.equal("operation" in event, false);
|
|
309
|
-
assert.equal("uid" in event, false);
|
|
94
|
+
assert.equal((await marker())?.state, "pending");
|
|
95
|
+
const alarm = errors.find(
|
|
96
|
+
(entry) => entry.alert === "flag_push_hint_enqueue_failed",
|
|
97
|
+
);
|
|
98
|
+
assert.equal(alarm?.errorCode, "ECONNREFUSED");
|
|
310
99
|
});
|
|
311
100
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
throw new Error(
|
|
317
|
-
"Cannot update state on a MessageFlagPush that does not exist: msg-1/\\Seen",
|
|
318
|
-
);
|
|
319
|
-
},
|
|
320
|
-
findImpl: () => Promise.resolve(null),
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
await service.flip({
|
|
324
|
-
accountId,
|
|
325
|
-
accountConfigId,
|
|
326
|
-
messageId,
|
|
327
|
-
mailboxId,
|
|
328
|
-
flagName: MessageSystemFlag.Seen,
|
|
329
|
-
operation: "add",
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
assert.equal(
|
|
333
|
-
logs.error.length,
|
|
334
|
-
0,
|
|
335
|
-
"the enqueue succeeded; a fast-path race must never be logged as flag_push_hint_enqueue_failed",
|
|
336
|
-
);
|
|
337
|
-
assert.ok(
|
|
338
|
-
logs.info.length > 0,
|
|
339
|
-
"the race is still visible as routine info, not silent",
|
|
340
|
-
);
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
it("still logs the flag_push_hint_enqueue_failed alarm when updateState fails for a REAL reason (marker still present)", async () => {
|
|
344
|
-
const { service, logs } = createHarness({
|
|
345
|
-
updateStateImpl: () => {
|
|
346
|
-
throw new Error("ProvisionedThroughputExceededException");
|
|
347
|
-
},
|
|
348
|
-
findImpl: () =>
|
|
349
|
-
Promise.resolve({
|
|
350
|
-
messageId,
|
|
351
|
-
flagName: MessageSystemFlag.Seen,
|
|
352
|
-
state: "pending",
|
|
353
|
-
}),
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
await service.flip({
|
|
357
|
-
accountId,
|
|
358
|
-
accountConfigId,
|
|
359
|
-
messageId,
|
|
360
|
-
mailboxId,
|
|
361
|
-
flagName: MessageSystemFlag.Seen,
|
|
362
|
-
operation: "add",
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
const alerted = logs.error.find(
|
|
366
|
-
(entry) =>
|
|
367
|
-
(entry as { alert?: string }).alert ===
|
|
368
|
-
"flag_push_hint_enqueue_failed",
|
|
369
|
-
);
|
|
370
|
-
assert.ok(
|
|
371
|
-
alerted,
|
|
372
|
-
"a genuine failure behind the race-check must still alarm",
|
|
373
|
-
);
|
|
101
|
+
it("raises no alarm when the worker drained the push before it was marked queued", async () => {
|
|
102
|
+
queueAnswers(async () => {
|
|
103
|
+
await store.flagPush.delete(target.messageId, target.flagName);
|
|
104
|
+
return {};
|
|
374
105
|
});
|
|
375
106
|
|
|
376
|
-
|
|
377
|
-
const { service, logs } = createHarness({
|
|
378
|
-
updateStateImpl: () => {
|
|
379
|
-
throw new Error(
|
|
380
|
-
"Cannot update state on a MessageFlagPush that does not exist: msg-1/\\Seen",
|
|
381
|
-
);
|
|
382
|
-
},
|
|
383
|
-
findImpl: () => {
|
|
384
|
-
throw new Error("ECONNREFUSED");
|
|
385
|
-
},
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
await service.flip({
|
|
389
|
-
accountId,
|
|
390
|
-
accountConfigId,
|
|
391
|
-
messageId,
|
|
392
|
-
mailboxId,
|
|
393
|
-
flagName: MessageSystemFlag.Seen,
|
|
394
|
-
operation: "add",
|
|
395
|
-
});
|
|
107
|
+
await flips().flip(target);
|
|
396
108
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
(entry as { alert?: string }).alert ===
|
|
400
|
-
"flag_push_hint_enqueue_failed",
|
|
401
|
-
);
|
|
402
|
-
assert.ok(
|
|
403
|
-
alerted,
|
|
404
|
-
"a re-check failure is a real infra problem — must alarm",
|
|
405
|
-
);
|
|
406
|
-
});
|
|
109
|
+
assert.equal(await marker(), null);
|
|
110
|
+
assert.deepEqual(errors, []);
|
|
407
111
|
});
|
|
408
112
|
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
3
|
+
import type { AccountItem, MailboxItem } from "@remit/data-ports";
|
|
4
|
+
import { MessageSystemFlag } from "@remit/domain-enums";
|
|
5
|
+
import { FlagPushService } from "./flag-push.js";
|
|
6
|
+
import { resolveExhaustedFlagPushFailure } from "./flag-push-terminal.js";
|
|
7
|
+
import { FlagQueueService, type UpdateFlagsInput } from "./flag-queue.js";
|
|
8
|
+
import {
|
|
9
|
+
eml,
|
|
10
|
+
type FakeImapConnection,
|
|
11
|
+
FakeImapServer,
|
|
12
|
+
} from "./test-helpers/fake-imap.js";
|
|
13
|
+
import {
|
|
14
|
+
captureQueue,
|
|
15
|
+
openStore,
|
|
16
|
+
type QueueCapture,
|
|
17
|
+
type Store,
|
|
18
|
+
seedAccount,
|
|
19
|
+
seedMailbox,
|
|
20
|
+
seedMessage,
|
|
21
|
+
silentLog,
|
|
22
|
+
} from "./test-helpers/test-store.js";
|
|
23
|
+
|
|
24
|
+
let store: Store;
|
|
25
|
+
let capture: QueueCapture;
|
|
26
|
+
let account: AccountItem;
|
|
27
|
+
let inbox: MailboxItem;
|
|
28
|
+
let messageId: string;
|
|
29
|
+
|
|
30
|
+
beforeEach(async () => {
|
|
31
|
+
capture = captureQueue();
|
|
32
|
+
store = await openStore();
|
|
33
|
+
account = await seedAccount(store);
|
|
34
|
+
inbox = await seedMailbox(store, account, "INBOX");
|
|
35
|
+
messageId = (await seedMessage(store, account, inbox, 1)).message.messageId;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(async () => {
|
|
39
|
+
capture.restore();
|
|
40
|
+
await store.close();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const flags = () =>
|
|
44
|
+
new FlagQueueService({
|
|
45
|
+
messageFlagService: store.messageFlag,
|
|
46
|
+
messageService: store.message,
|
|
47
|
+
threadMessageService: store.threadMessage,
|
|
48
|
+
flagPushService: new FlagPushService({
|
|
49
|
+
markerService: store.flagPush,
|
|
50
|
+
sqsQueueUrl: "https://sqs.test/flags",
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const update = (input: UpdateFlagsInput) =>
|
|
55
|
+
flags().updateFlags(
|
|
56
|
+
account.accountConfigId,
|
|
57
|
+
messageId,
|
|
58
|
+
account.accountId,
|
|
59
|
+
input,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const listing = async () =>
|
|
63
|
+
(
|
|
64
|
+
await store.threadMessage.findAllByMessageId(
|
|
65
|
+
account.accountConfigId,
|
|
66
|
+
messageId,
|
|
67
|
+
)
|
|
68
|
+
)[0];
|
|
69
|
+
|
|
70
|
+
const owedFlagged = async () =>
|
|
71
|
+
(await store.flagPush.find(messageId, MessageSystemFlag.Flagged))?.operation;
|
|
72
|
+
|
|
73
|
+
describe("updateFlags keeps the star and its colour in step", () => {
|
|
74
|
+
for (const [input, hasStars, star, owed] of [
|
|
75
|
+
[{ isStarred: true }, true, "yellow", "add"],
|
|
76
|
+
[{ isStarred: true, starColor: "red" }, true, "red", "add"],
|
|
77
|
+
[{ starColor: "blue" }, true, "blue", "add"],
|
|
78
|
+
[{ isStarred: true, starColor: "none" }, true, "none", "add"],
|
|
79
|
+
] as const) {
|
|
80
|
+
it(`${JSON.stringify(input)} stars as ${star} and owes the server \\Flagged`, async () => {
|
|
81
|
+
await update(input);
|
|
82
|
+
|
|
83
|
+
const row = await listing();
|
|
84
|
+
assert.equal(row.hasStars, hasStars);
|
|
85
|
+
assert.equal(row.star, star);
|
|
86
|
+
assert.equal(await owedFlagged(), owed);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const input of [{ isStarred: false }, { starColor: "none" }] as const) {
|
|
91
|
+
it(`${JSON.stringify(input)} unstars a starred message`, async () => {
|
|
92
|
+
await update({ isStarred: true });
|
|
93
|
+
|
|
94
|
+
await update(input);
|
|
95
|
+
|
|
96
|
+
const row = await listing();
|
|
97
|
+
assert.equal(row.hasStars, false);
|
|
98
|
+
assert.equal(row.star, "none");
|
|
99
|
+
assert.equal(await owedFlagged(), "remove");
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
it("marks read once and treats a repeat as no change", async () => {
|
|
104
|
+
await update({ isRead: true });
|
|
105
|
+
const first = await store.flagPush.find(messageId, MessageSystemFlag.Seen);
|
|
106
|
+
|
|
107
|
+
const result = await update({ isRead: true });
|
|
108
|
+
|
|
109
|
+
assert.equal(result.isRead, true);
|
|
110
|
+
assert.equal((await listing()).isRead, true);
|
|
111
|
+
assert.deepEqual(
|
|
112
|
+
await store.flagPush.find(messageId, MessageSystemFlag.Seen),
|
|
113
|
+
first,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("a flag push that exhausted its retries", () => {
|
|
119
|
+
const resolve = async (
|
|
120
|
+
connect: () => FakeImapConnection,
|
|
121
|
+
afterRead: () => Promise<unknown> = async () => {},
|
|
122
|
+
) => {
|
|
123
|
+
await update({ isRead: true });
|
|
124
|
+
const read = await store.message.get(messageId);
|
|
125
|
+
await afterRead();
|
|
126
|
+
return resolveExhaustedFlagPushFailure(
|
|
127
|
+
{
|
|
128
|
+
messageService: store.message,
|
|
129
|
+
markerService: store.flagPush,
|
|
130
|
+
log: silentLog,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
placement: {
|
|
134
|
+
status: read.status,
|
|
135
|
+
mailboxId: read.mailboxId,
|
|
136
|
+
uid: read.uid,
|
|
137
|
+
},
|
|
138
|
+
accountId: account.accountId,
|
|
139
|
+
accountConfigId: account.accountConfigId,
|
|
140
|
+
messageId,
|
|
141
|
+
flagName: MessageSystemFlag.Seen,
|
|
142
|
+
uid: 1,
|
|
143
|
+
mailboxPath: "INBOX",
|
|
144
|
+
getConnection: async () => connect(),
|
|
145
|
+
},
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
it("drops the push and the stale rows of a message the server no longer holds", async () => {
|
|
150
|
+
const imap = new FakeImapServer();
|
|
151
|
+
imap.addFolder("INBOX");
|
|
152
|
+
|
|
153
|
+
const { outcome } = await resolve(() => imap.connection());
|
|
154
|
+
|
|
155
|
+
assert.equal(outcome, "reconciled");
|
|
156
|
+
assert.equal(
|
|
157
|
+
await store.flagPush.find(messageId, MessageSystemFlag.Seen),
|
|
158
|
+
null,
|
|
159
|
+
);
|
|
160
|
+
await assert.rejects(store.message.get(messageId));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("leaves a row the user moved after the push read it", async () => {
|
|
164
|
+
const imap = new FakeImapServer();
|
|
165
|
+
imap.addFolder("INBOX");
|
|
166
|
+
const archive = await seedMailbox(store, account, "Archive");
|
|
167
|
+
|
|
168
|
+
const { outcome } = await resolve(
|
|
169
|
+
() => imap.connection(),
|
|
170
|
+
() =>
|
|
171
|
+
store.message.transitionPlacement(
|
|
172
|
+
messageId,
|
|
173
|
+
{ mailboxId: inbox.mailboxId },
|
|
174
|
+
{ mailboxId: archive.mailboxId, uid: 7 },
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
assert.equal(outcome, "reconciled");
|
|
179
|
+
const moved = await store.message.get(messageId);
|
|
180
|
+
assert.equal(moved.mailboxId, archive.mailboxId);
|
|
181
|
+
assert.equal(moved.uid, 7);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("keeps the push and the rows of a message still there, dropped FETCH row or not", async () => {
|
|
185
|
+
const imap = new FakeImapServer();
|
|
186
|
+
imap.addFolder("INBOX");
|
|
187
|
+
imap.deliver("INBOX", eml({}));
|
|
188
|
+
const connection = imap.connection();
|
|
189
|
+
connection.fetchMessages = async () => [];
|
|
190
|
+
|
|
191
|
+
const { outcome } = await resolve(() => connection);
|
|
192
|
+
|
|
193
|
+
assert.equal(outcome, "broken");
|
|
194
|
+
assert.ok(await store.flagPush.find(messageId, MessageSystemFlag.Seen));
|
|
195
|
+
assert.ok(await store.message.get(messageId));
|
|
196
|
+
});
|
|
197
|
+
});
|