@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
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Issue #845 item 3. While a move is in flight the Message row names the
|
|
3
|
-
* destination folder but still carries the SOURCE folder's uid, so a delete
|
|
4
|
-
* that binds the pair as it stands sends the worker to expunge the destination
|
|
5
|
-
* folder's own message at that uid — an unrelated message, destroyed for good.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import assert from "node:assert/strict";
|
|
9
|
-
import { describe, it } from "node:test";
|
|
10
|
-
import type {
|
|
11
|
-
IAddressRepository,
|
|
12
|
-
IMailboxRepository,
|
|
13
|
-
IMailboxSpecialUseRepository,
|
|
14
|
-
IMessageRepository,
|
|
15
|
-
IThreadMessageRepository,
|
|
16
|
-
} from "@remit/data-ports";
|
|
17
|
-
import { MessagePlacementUnsettledError } from "@remit/data-ports/errors";
|
|
18
|
-
import type { RoleResolution } from "@remit/data-ports/folder-role";
|
|
19
|
-
import { MessageMoveService } from "./message-move.js";
|
|
20
|
-
import { NO_JUNK_ROLES } from "./test-helpers/folder-roles.js";
|
|
21
|
-
|
|
22
|
-
const ACCOUNT = "acc-1";
|
|
23
|
-
const ACCOUNT_CONFIG = "cfg-1";
|
|
24
|
-
const INBOX = "mbx-inbox";
|
|
25
|
-
const ARCHIVE = "mbx-archive";
|
|
26
|
-
const TRASH = "mbx-trash";
|
|
27
|
-
|
|
28
|
-
const MOVING_ID = "msg-moving";
|
|
29
|
-
const SETTLED_ID = "msg-settled";
|
|
30
|
-
const STRANDED_ID = "msg-stranded";
|
|
31
|
-
const COPY_ID = "msg-copy";
|
|
32
|
-
|
|
33
|
-
const INBOX_UID = 42;
|
|
34
|
-
const ARCHIVE_UID = 907;
|
|
35
|
-
const SETTLED_UID = 11;
|
|
36
|
-
|
|
37
|
-
type TrashMailbox = { mailboxId: string; fullPath: string };
|
|
38
|
-
|
|
39
|
-
const flaggedTrash: RoleResolution<TrashMailbox> = {
|
|
40
|
-
kind: "flagged",
|
|
41
|
-
mailbox: { mailboxId: TRASH, fullPath: "INBOX/Trash" },
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
interface CapturedEvent {
|
|
45
|
-
type: string;
|
|
46
|
-
messageId?: string;
|
|
47
|
-
operation?: string;
|
|
48
|
-
mailboxId?: string;
|
|
49
|
-
mailboxPath?: string;
|
|
50
|
-
uid?: number;
|
|
51
|
-
sourceMailboxPath?: string;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const mailboxes = [
|
|
55
|
-
{ mailboxId: INBOX, fullPath: "INBOX", accountId: ACCOUNT },
|
|
56
|
-
{ mailboxId: ARCHIVE, fullPath: "Archive", accountId: ACCOUNT },
|
|
57
|
-
{ mailboxId: TRASH, fullPath: "INBOX/Trash", accountId: ACCOUNT },
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
/** A row mid-move: Archive is already written, the uid is still INBOX's. */
|
|
61
|
-
const movingRow = () => ({
|
|
62
|
-
messageId: MOVING_ID,
|
|
63
|
-
mailboxId: ARCHIVE,
|
|
64
|
-
uid: INBOX_UID,
|
|
65
|
-
status: "moving",
|
|
66
|
-
syncStatus: "pending",
|
|
67
|
-
originalMailboxId: INBOX,
|
|
68
|
-
originalUid: INBOX_UID,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* A row a delete gave up on. R3 defines the give-up as a hand-back: the row
|
|
73
|
-
* goes back on the placement the mail server confirmed, and only then carries
|
|
74
|
-
* `abandoned`. The pair is honest, so nothing downstream refuses it — the
|
|
75
|
-
* failure is a statement the reading pane makes, never a gate.
|
|
76
|
-
*/
|
|
77
|
-
const strandedRow = () => ({
|
|
78
|
-
messageId: STRANDED_ID,
|
|
79
|
-
mailboxId: INBOX,
|
|
80
|
-
uid: INBOX_UID,
|
|
81
|
-
status: "active",
|
|
82
|
-
syncStatus: "abandoned",
|
|
83
|
-
abandonedMutation: "delete",
|
|
84
|
-
originalMailboxId: INBOX,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
/** An ordinary settled row. */
|
|
88
|
-
const settledRow = () => ({
|
|
89
|
-
messageId: SETTLED_ID,
|
|
90
|
-
mailboxId: INBOX,
|
|
91
|
-
uid: SETTLED_UID,
|
|
92
|
-
status: "active",
|
|
93
|
-
syncStatus: "synced",
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* A freshly copied row: `moving` until COPYUID lands, with no server-side uid
|
|
98
|
-
* at all and no move behind it. Its uid is not ready; it does not name anyone.
|
|
99
|
-
*/
|
|
100
|
-
const freshCopyRow = () => ({
|
|
101
|
-
messageId: COPY_ID,
|
|
102
|
-
mailboxId: ARCHIVE,
|
|
103
|
-
uid: 0,
|
|
104
|
-
status: "moving",
|
|
105
|
-
syncStatus: "pending",
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const buildWorld = (seed: Array<Record<string, unknown>>) => {
|
|
109
|
-
const patches: Array<{ messageId: string; patch: Record<string, unknown> }> =
|
|
110
|
-
[];
|
|
111
|
-
const events: CapturedEvent[] = [];
|
|
112
|
-
const rows = new Map<string, Record<string, unknown>>(
|
|
113
|
-
seed.map((row): [string, Record<string, unknown>] => [
|
|
114
|
-
String(row.messageId),
|
|
115
|
-
row,
|
|
116
|
-
]),
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
const settle = (messageId: string) => {
|
|
120
|
-
Object.assign(rows.get(messageId) ?? {}, {
|
|
121
|
-
uid: ARCHIVE_UID,
|
|
122
|
-
status: "active",
|
|
123
|
-
syncStatus: "synced",
|
|
124
|
-
});
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const messageService = {
|
|
128
|
-
get: async (ids: string | string[]) =>
|
|
129
|
-
Array.isArray(ids)
|
|
130
|
-
? ids.flatMap((id) => {
|
|
131
|
-
const row = rows.get(id);
|
|
132
|
-
return row ? [row] : [];
|
|
133
|
-
})
|
|
134
|
-
: rows.get(ids),
|
|
135
|
-
update: async (id: string, patch: Record<string, unknown>) => {
|
|
136
|
-
patches.push({ messageId: id, patch });
|
|
137
|
-
return Object.assign(rows.get(id) ?? {}, patch);
|
|
138
|
-
},
|
|
139
|
-
transitionPlacement: async (
|
|
140
|
-
id: string,
|
|
141
|
-
_expected: Record<string, unknown>,
|
|
142
|
-
patch: Record<string, unknown>,
|
|
143
|
-
) => {
|
|
144
|
-
patches.push({ messageId: id, patch });
|
|
145
|
-
return Object.assign(rows.get(id) ?? {}, patch);
|
|
146
|
-
},
|
|
147
|
-
upsert: async (row: Record<string, unknown>) => row,
|
|
148
|
-
} as unknown as IMessageRepository;
|
|
149
|
-
|
|
150
|
-
const threadMessageService = {
|
|
151
|
-
findAllByMessageId: async () => [],
|
|
152
|
-
getByMessageId: async (_cfg: string, messageId: string) => ({
|
|
153
|
-
accountConfigId: ACCOUNT_CONFIG,
|
|
154
|
-
threadMessageId: `tm-${messageId}`,
|
|
155
|
-
messageId,
|
|
156
|
-
threadId: "thr-1",
|
|
157
|
-
mailboxId: rows.get(messageId)?.mailboxId,
|
|
158
|
-
sentDate: 1,
|
|
159
|
-
isRead: false,
|
|
160
|
-
isDeleted: false,
|
|
161
|
-
hasStars: false,
|
|
162
|
-
hasAttachment: false,
|
|
163
|
-
}),
|
|
164
|
-
create: async () => {},
|
|
165
|
-
update: async () => {},
|
|
166
|
-
delete: async () => {},
|
|
167
|
-
} as unknown as IThreadMessageRepository;
|
|
168
|
-
|
|
169
|
-
const mailboxService = {
|
|
170
|
-
get: async (_accountId: string, ids: string | string[]) =>
|
|
171
|
-
Array.isArray(ids)
|
|
172
|
-
? mailboxes.filter((mailbox) => ids.includes(mailbox.mailboxId))
|
|
173
|
-
: mailboxes.find((mailbox) => mailbox.mailboxId === ids),
|
|
174
|
-
} as unknown as IMailboxRepository;
|
|
175
|
-
|
|
176
|
-
const mailboxSpecialUseService = {
|
|
177
|
-
resolveTrashRole: async () => flaggedTrash,
|
|
178
|
-
findTrashMailbox: async () => flaggedTrash.mailbox,
|
|
179
|
-
resolveJunkRolesForConfig: async () => NO_JUNK_ROLES,
|
|
180
|
-
} as unknown as IMailboxSpecialUseRepository;
|
|
181
|
-
|
|
182
|
-
const addressService = {
|
|
183
|
-
reconcileJunkOnlyForMessage: async () => {},
|
|
184
|
-
} as unknown as IAddressRepository;
|
|
185
|
-
|
|
186
|
-
const service = new MessageMoveService({
|
|
187
|
-
messageService,
|
|
188
|
-
mailboxService,
|
|
189
|
-
mailboxSpecialUseService,
|
|
190
|
-
threadMessageService,
|
|
191
|
-
addressService,
|
|
192
|
-
sqsQueueUrl: "http://localhost:9324/000000000000/remit-messages.fifo",
|
|
193
|
-
moveSettleTimeoutMs: 200,
|
|
194
|
-
moveSettlePollMs: 10,
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
const stubs = service as unknown as {
|
|
198
|
-
enqueueEventsBatch: (batch: CapturedEvent[]) => Promise<void>;
|
|
199
|
-
enqueueEvent: (event: CapturedEvent) => Promise<void>;
|
|
200
|
-
};
|
|
201
|
-
stubs.enqueueEventsBatch = async (batch) => {
|
|
202
|
-
events.push(...batch);
|
|
203
|
-
};
|
|
204
|
-
stubs.enqueueEvent = async (event) => {
|
|
205
|
-
events.push(event);
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
return { service, patches, events, rows, settle };
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
describe("a delete never binds the folder/uid pair of an in-flight move (#845.3)", () => {
|
|
212
|
-
it("binds the confirmed pair once the move settles", async () => {
|
|
213
|
-
const { service, events, settle } = buildWorld([movingRow()]);
|
|
214
|
-
|
|
215
|
-
setTimeout(() => settle(MOVING_ID), 30);
|
|
216
|
-
await service.deleteMessages(ACCOUNT_CONFIG, [MOVING_ID], ACCOUNT);
|
|
217
|
-
|
|
218
|
-
assert.equal(events.length, 1);
|
|
219
|
-
assert.equal(events[0].operation, "move_to_trash");
|
|
220
|
-
assert.equal(
|
|
221
|
-
events[0].mailboxPath,
|
|
222
|
-
"Archive",
|
|
223
|
-
"the folder the confirmed move left the message in",
|
|
224
|
-
);
|
|
225
|
-
assert.equal(
|
|
226
|
-
events[0].uid,
|
|
227
|
-
ARCHIVE_UID,
|
|
228
|
-
"and that folder's own uid, never the source folder's",
|
|
229
|
-
);
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
it("refuses the delete, and touches nothing, when the move never settles", async () => {
|
|
233
|
-
const { service, patches, events, rows } = buildWorld([movingRow()]);
|
|
234
|
-
|
|
235
|
-
await assert.rejects(
|
|
236
|
-
() => service.deleteMessages(ACCOUNT_CONFIG, [MOVING_ID], ACCOUNT),
|
|
237
|
-
(error: unknown) =>
|
|
238
|
-
error instanceof MessagePlacementUnsettledError &&
|
|
239
|
-
error.statusCode === 409 &&
|
|
240
|
-
error.publicApiError?.code === "message_placement_unsettled" &&
|
|
241
|
-
error.publicApiError?.details?.messageId === MOVING_ID &&
|
|
242
|
-
error.publicApiError?.details?.accountId === ACCOUNT &&
|
|
243
|
-
error.publicApiError?.details?.reason === "in_flight",
|
|
244
|
-
);
|
|
245
|
-
|
|
246
|
-
assert.deepEqual(events, [], "nothing was enqueued");
|
|
247
|
-
assert.deepEqual(patches, [], "and no local write was made");
|
|
248
|
-
assert.equal(rows.get(MOVING_ID)?.uid, INBOX_UID);
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it("expunges no part of a mixed batch when one row is in flight", async () => {
|
|
252
|
-
// All-or-nothing: the settled row is deletable on its own, and would be
|
|
253
|
-
// expunged first by a per-row gate. One unverifiable pair refuses the
|
|
254
|
-
// batch it arrived in.
|
|
255
|
-
const { service, patches, events } = buildWorld([
|
|
256
|
-
settledRow(),
|
|
257
|
-
movingRow(),
|
|
258
|
-
]);
|
|
259
|
-
|
|
260
|
-
await assert.rejects(
|
|
261
|
-
() =>
|
|
262
|
-
service.deleteMessages(
|
|
263
|
-
ACCOUNT_CONFIG,
|
|
264
|
-
[SETTLED_ID, MOVING_ID],
|
|
265
|
-
ACCOUNT,
|
|
266
|
-
{ permanent: true },
|
|
267
|
-
),
|
|
268
|
-
MessagePlacementUnsettledError,
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
assert.deepEqual(
|
|
272
|
-
events,
|
|
273
|
-
[],
|
|
274
|
-
"the settled row was not expunged alongside the refusal",
|
|
275
|
-
);
|
|
276
|
-
assert.deepEqual(patches, []);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it("deletes the whole batch once the in-flight row settles", async () => {
|
|
280
|
-
const { service, events, settle } = buildWorld([settledRow(), movingRow()]);
|
|
281
|
-
|
|
282
|
-
setTimeout(() => settle(MOVING_ID), 30);
|
|
283
|
-
await service.deleteMessages(
|
|
284
|
-
ACCOUNT_CONFIG,
|
|
285
|
-
[SETTLED_ID, MOVING_ID],
|
|
286
|
-
ACCOUNT,
|
|
287
|
-
{ permanent: true },
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
assert.deepEqual(
|
|
291
|
-
events.map((event) => [event.messageId, event.uid]).sort(),
|
|
292
|
-
[
|
|
293
|
-
[MOVING_ID, ARCHIVE_UID],
|
|
294
|
-
[SETTLED_ID, SETTLED_UID],
|
|
295
|
-
].sort(),
|
|
296
|
-
);
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("waits one row's ceiling for a batch, not one per row", async () => {
|
|
300
|
-
// The gate is concurrent. Three in-flight rows that never settle must
|
|
301
|
-
// refuse at roughly one ceiling — a sequential gate would spend three,
|
|
302
|
-
// and at the client's 100-id chunk cap that is a gateway timeout.
|
|
303
|
-
const rows = [movingRow(), movingRow(), movingRow()].map((row, index) => ({
|
|
304
|
-
...row,
|
|
305
|
-
messageId: `${MOVING_ID}-${index}`,
|
|
306
|
-
}));
|
|
307
|
-
const { service } = buildWorld(rows);
|
|
308
|
-
|
|
309
|
-
const startedAt = Date.now();
|
|
310
|
-
await assert.rejects(
|
|
311
|
-
() =>
|
|
312
|
-
service.deleteMessages(
|
|
313
|
-
ACCOUNT_CONFIG,
|
|
314
|
-
rows.map((row) => row.messageId),
|
|
315
|
-
ACCOUNT,
|
|
316
|
-
{ permanent: true },
|
|
317
|
-
),
|
|
318
|
-
MessagePlacementUnsettledError,
|
|
319
|
-
);
|
|
320
|
-
|
|
321
|
-
assert.ok(
|
|
322
|
-
Date.now() - startedAt < 500,
|
|
323
|
-
"three rows refused within one 200ms ceiling, not three",
|
|
324
|
-
);
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
describe("the gate refuses only a uid that names somebody else (#845.3)", () => {
|
|
329
|
-
it("deletes a freshly copied row rather than waiting on it", async () => {
|
|
330
|
-
// A copy is `moving` with no server-side uid yet. That uid is not ready,
|
|
331
|
-
// which is not the same as naming another folder's message, and the
|
|
332
|
-
// delete of a copy is an ordinary flow that must not stall or refuse.
|
|
333
|
-
const { service, events } = buildWorld([freshCopyRow()]);
|
|
334
|
-
|
|
335
|
-
const startedAt = Date.now();
|
|
336
|
-
await service.deleteMessages(ACCOUNT_CONFIG, [COPY_ID], ACCOUNT, {
|
|
337
|
-
permanent: true,
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
assert.equal(events.length, 1);
|
|
341
|
-
assert.equal(events[0].operation, "permanent_delete");
|
|
342
|
-
assert.ok(Date.now() - startedAt < 100, "and never entered the wait");
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it("acts on a row a delete gave up on, rather than refusing it", async () => {
|
|
346
|
-
// A give-up hands the row back to the placement the server confirmed
|
|
347
|
-
// before it stops (R3), so the "Delete again" the reading pane offers is
|
|
348
|
-
// an ordinary delete. Refusing it — which is what a give-up left `moving`
|
|
349
|
-
// used to earn — made that button a dead end.
|
|
350
|
-
const { service, events } = buildWorld([strandedRow()]);
|
|
351
|
-
|
|
352
|
-
const startedAt = Date.now();
|
|
353
|
-
await service.deleteMessages(ACCOUNT_CONFIG, [STRANDED_ID], ACCOUNT);
|
|
354
|
-
|
|
355
|
-
assert.equal(events.length, 1);
|
|
356
|
-
assert.ok(Date.now() - startedAt < 100, "and never entered the wait");
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
describe("a copy binds the same pair and takes the same gate (#845.3)", () => {
|
|
361
|
-
it("copies from the confirmed pair once the move settles", async () => {
|
|
362
|
-
const { service, events, settle } = buildWorld([movingRow()]);
|
|
363
|
-
|
|
364
|
-
setTimeout(() => settle(MOVING_ID), 30);
|
|
365
|
-
await service.copyMessage(ACCOUNT_CONFIG, MOVING_ID, INBOX, ACCOUNT);
|
|
366
|
-
|
|
367
|
-
assert.equal(events.length, 1);
|
|
368
|
-
assert.equal(events[0].type, "MESSAGE_COPY");
|
|
369
|
-
assert.equal(events[0].sourceMailboxPath, "Archive");
|
|
370
|
-
assert.equal(events[0].uid, ARCHIVE_UID);
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it("writes no part of a batch copy when one row is in flight", async () => {
|
|
374
|
-
// The gate runs over the whole batch before the first upsert, so a
|
|
375
|
-
// refusal cannot leave copies already committed and enqueued.
|
|
376
|
-
const { service, events } = buildWorld([settledRow(), movingRow()]);
|
|
377
|
-
|
|
378
|
-
const startedAt = Date.now();
|
|
379
|
-
await assert.rejects(
|
|
380
|
-
() =>
|
|
381
|
-
service.copyMessages(
|
|
382
|
-
ACCOUNT_CONFIG,
|
|
383
|
-
[SETTLED_ID, MOVING_ID],
|
|
384
|
-
TRASH,
|
|
385
|
-
ACCOUNT,
|
|
386
|
-
),
|
|
387
|
-
MessagePlacementUnsettledError,
|
|
388
|
-
);
|
|
389
|
-
|
|
390
|
-
assert.deepEqual(events, [], "no copy was enqueued");
|
|
391
|
-
assert.ok(
|
|
392
|
-
Date.now() - startedAt < 500,
|
|
393
|
-
"and the batch spent one ceiling, not one per row",
|
|
394
|
-
);
|
|
395
|
-
});
|
|
396
|
-
});
|