@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.
Files changed (123) hide show
  1. package/package.json +21 -6
  2. package/src/body-part-mapper.ts +51 -1
  3. package/src/body-sync-queue.test.ts +39 -99
  4. package/src/body-sync-terminal.test.ts +115 -277
  5. package/src/body-sync-terminal.ts +10 -12
  6. package/src/body-sync.test.ts +925 -0
  7. package/src/body-sync.ts +226 -200
  8. package/src/classification-backfill.test.ts +197 -527
  9. package/src/classify-message.test.ts +11 -74
  10. package/src/external-move.test.ts +102 -209
  11. package/src/external-move.ts +13 -0
  12. package/src/filter-operations.test.ts +319 -0
  13. package/src/filter-operations.ts +377 -0
  14. package/src/filter-update.test.ts +313 -0
  15. package/src/filters/filter-config.test.ts +55 -197
  16. package/src/filters/filter-config.ts +4 -39
  17. package/src/filters/match.test.ts +1 -38
  18. package/src/filters/match.ts +3 -52
  19. package/src/filters/pipeline.test.ts +158 -289
  20. package/src/filters/pipeline.ts +56 -170
  21. package/src/flag-push-terminal.ts +9 -11
  22. package/src/flag-push.test.ts +76 -372
  23. package/src/flag-queue.test.ts +197 -0
  24. package/src/folder-operations.test.ts +273 -0
  25. package/src/folder-operations.ts +217 -0
  26. package/src/forwarded-message.test.ts +122 -0
  27. package/src/imapflow-connect-retry.test.ts +76 -113
  28. package/src/imapflow-connection.test.ts +34 -0
  29. package/src/imapflow-connection.ts +19 -15
  30. package/src/index.ts +59 -12
  31. package/src/list-id-backfill.test.ts +133 -499
  32. package/src/mailbox-cursor.test.ts +81 -189
  33. package/src/mailbox-delete-intent.test.ts +78 -205
  34. package/src/mailbox-intent.ts +5 -3
  35. package/src/mailbox-management.test.ts +107 -220
  36. package/src/mailbox-management.ts +1 -2
  37. package/src/mailbox-presence.test.ts +36 -34
  38. package/src/mailbox-queue.ts +2 -20
  39. package/src/mailbox-rename-intent.test.ts +156 -325
  40. package/src/mailbox-rename-subtree.test.ts +205 -582
  41. package/src/mailbox-sync.test.ts +182 -744
  42. package/src/message-move.test.ts +726 -0
  43. package/src/message-move.ts +227 -57
  44. package/src/message-read.ts +87 -0
  45. package/src/message-save-retry.test.ts +90 -0
  46. package/src/message-sync-flags.test.ts +10 -1
  47. package/src/message-sync.test.ts +762 -0
  48. package/src/message-sync.ts +409 -239
  49. package/src/mime-walker.ts +16 -15
  50. package/src/organize-job.ts +93 -0
  51. package/src/outbox-attachment.test.ts +97 -209
  52. package/src/outbox-failed-recovery.test.ts +198 -269
  53. package/src/outbox-queue.test.ts +24 -0
  54. package/src/outbox-queue.ts +25 -8
  55. package/src/ownership.ts +140 -0
  56. package/src/placement-move-terminal.ts +9 -12
  57. package/src/placement-move.ts +33 -6
  58. package/src/placement-settled.ts +28 -4
  59. package/src/placement.test.ts +404 -0
  60. package/src/quarantine.test.ts +90 -148
  61. package/src/quarantine.ts +3 -2
  62. package/src/refusal.ts +29 -0
  63. package/src/restore-source-placement.ts +0 -4
  64. package/src/spam-report.test.ts +238 -710
  65. package/src/sync-warning.test.ts +213 -0
  66. package/src/sync-warning.ts +56 -0
  67. package/src/test-helpers/fake-imap.ts +690 -0
  68. package/src/test-helpers/test-queue.ts +56 -0
  69. package/src/test-helpers/test-store.ts +299 -0
  70. package/src/thread-root.test.ts +29 -0
  71. package/src/thread-root.ts +51 -0
  72. package/src/types/mailparser-augment.d.ts +8 -0
  73. package/src/types.ts +1 -1
  74. package/src/adversarial-mime.e2e.test.ts +0 -307
  75. package/src/body-sync-auto-read-writeonce.test.ts +0 -219
  76. package/src/body-sync-calendar-suggestion.test.ts +0 -433
  77. package/src/body-sync-category-denormalize.test.ts +0 -352
  78. package/src/body-sync-category-override.test.ts +0 -501
  79. package/src/body-sync-category-writeonce.test.ts +0 -234
  80. package/src/body-sync-filter.test.ts +0 -237
  81. package/src/body-sync-placement-flags.test.ts +0 -347
  82. package/src/body-sync-placement-writeonce.test.ts +0 -337
  83. package/src/body-sync-quarantine.test.ts +0 -276
  84. package/src/body-sync-resync.test.ts +0 -605
  85. package/src/body-sync-unclassifiable-once.test.ts +0 -260
  86. package/src/body-sync-unsubscribed-auto-read.test.ts +0 -176
  87. package/src/filters/anchor-drift.ts +0 -55
  88. package/src/filters/message-embedder.test.ts +0 -39
  89. package/src/filters/message-embedder.ts +0 -33
  90. package/src/flag-push-terminal.test.ts +0 -209
  91. package/src/flag-queue-stars.test.ts +0 -171
  92. package/src/imapflow-connection.e2e.test.ts +0 -335
  93. package/src/imapflow-connection.integ.test.ts +0 -316
  94. package/src/message-delete-junk-reconcile.test.ts +0 -117
  95. package/src/message-delete-no-trash.test.ts +0 -245
  96. package/src/message-delete-unsettled-placement.test.ts +0 -396
  97. package/src/message-move-copy.test.ts +0 -562
  98. package/src/message-move-empty-trash.test.ts +0 -326
  99. package/src/message-move-same-mailbox.test.ts +0 -139
  100. package/src/message-move-unsettled-placement.test.ts +0 -378
  101. package/src/message-sync-address-spoof.test.ts +0 -293
  102. package/src/message-sync-category.test.ts +0 -142
  103. package/src/message-sync-changedsince.test.ts +0 -840
  104. package/src/message-sync-junk-harvest.test.ts +0 -244
  105. package/src/message-sync-quarantine.test.ts +0 -416
  106. package/src/message-sync-repoint.test.ts +0 -419
  107. package/src/outbox-queue-settled-row.test.ts +0 -140
  108. package/src/pass-through-unit-of-work.ts +0 -17
  109. package/src/placement-move-terminal.test.ts +0 -300
  110. package/src/placement-move-unsettled.test.ts +0 -275
  111. package/src/placement-transition-race.test.ts +0 -482
  112. package/src/stale-message-reconcile.test.ts +0 -82
  113. package/src/stale-message-reconcile.ts +0 -55
  114. package/src/test-helpers/isolated-mailbox.ts +0 -44
  115. package/src/test-helpers/mailfuzz-connection.ts +0 -27
  116. package/test/fixtures/mime-adversarial-e2e/01-octet-stream-pdf.eml +0 -21
  117. package/test/fixtures/mime-adversarial-e2e/02-nested-related-cid-image.eml +0 -31
  118. package/test/fixtures/mime-adversarial-e2e/03-calendar-invite.eml +0 -33
  119. package/test/fixtures/mime-adversarial-e2e/04-forwarded-rfc822-with-attachments.eml +0 -42
  120. package/test/fixtures/mime-adversarial-e2e/05-tnef-winmail.eml +0 -25
  121. package/test/fixtures/mime-adversarial-e2e/06-8bit-non-ascii.eml +0 -15
  122. package/test/fixtures/mime-adversarial-e2e/07-quoted-printable-soft-breaks.eml +0 -17
  123. package/test/fixtures/mime-adversarial-e2e/08-alternative-empty-text.eml +0 -18
@@ -1,22 +1,12 @@
1
1
  import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type {
4
- CreateOutboxAttachmentInput,
5
- IOutboxAttachmentRepository,
6
- IOutboxMessageRepository,
7
- OutboxAttachmentCap,
8
- OutboxAttachmentItem,
9
- OutboxMessageItem,
10
- ReserveOutboxAttachmentResult,
11
- } from "@remit/data-ports";
12
- import { holdsRoom } from "@remit/data-ports";
13
- import { ForbiddenError, NotFoundError } from "@remit/data-ports/errors";
2
+ import { afterEach, describe, it } from "node:test";
3
+ import type { OutboxMessageItem } from "@remit/data-ports";
4
+ import { ForbiddenError } from "@remit/data-ports/errors";
14
5
  import {
15
6
  OutboxAttachmentRejectionReason,
16
7
  OutboxMessageStatus,
17
8
  } from "@remit/domain-enums";
18
9
  import {
19
- createMockStorageService,
20
10
  type StorageService,
21
11
  UPLOAD_URL_TTL_SECONDS,
22
12
  } from "@remit/storage-service";
@@ -25,152 +15,50 @@ import {
25
15
  OUTBOX_ATTACHMENT_MAX_TOTAL_BYTES,
26
16
  OutboxAttachmentService,
27
17
  } from "./outbox-attachment.js";
18
+ import { openStore, type Store } from "./test-helpers/test-store.js";
28
19
 
29
20
  const ACCOUNT_CONFIG_ID = "cfg-679";
30
21
  const ACCOUNT_ID = "acc-679";
31
- const DRAFT_ID = "draft-679";
32
-
33
- /**
34
- * An in-memory stand-in for the repository, with the one property that matters:
35
- * `reserve` counts and inserts without yielding, the way a database transaction
36
- * does. Everything else may interleave freely.
37
- */
38
- const createRepository = (): IOutboxAttachmentRepository & {
39
- rows: Map<string, OutboxAttachmentItem>;
40
- } => {
41
- const rows = new Map<string, OutboxAttachmentItem>();
42
- let sequence = 0;
43
-
44
- const forDraft = (
45
- accountConfigId: string,
46
- outboxMessageId: string,
47
- ): OutboxAttachmentItem[] =>
48
- [...rows.values()].filter(
49
- (row) =>
50
- row.accountConfigId === accountConfigId &&
51
- row.outboxMessageId === outboxMessageId,
52
- );
53
22
 
54
- return {
55
- rows,
56
- reserve: async (
57
- input: CreateOutboxAttachmentInput,
58
- cap: OutboxAttachmentCap,
59
- ): Promise<ReserveOutboxAttachmentResult> => {
60
- // No await inside: the whole point of the real implementation is that
61
- // counting and inserting are one atomic step.
62
- const live = forDraft(
63
- input.accountConfigId,
64
- input.outboxMessageId,
65
- ).filter((row) => holdsRoom(row, cap.nowSeconds));
66
- const usedBytes = live.reduce((total, row) => total + row.sizeBytes, 0);
67
- if (live.length >= cap.maxCount) {
68
- return { outcome: "OverCountCap", usedBytes };
69
- }
70
- if (usedBytes + input.sizeBytes > cap.maxTotalBytes) {
71
- return { outcome: "OverByteCap", usedBytes };
72
- }
73
- sequence += 1;
74
- const item: OutboxAttachmentItem = {
75
- ...input,
76
- state: "Pending",
77
- createdAt: sequence,
78
- updatedAt: sequence,
79
- };
80
- rows.set(item.outboxAttachmentId, item);
81
- return { outcome: "Reserved", item };
82
- },
83
- get: async (accountConfigId, outboxAttachmentId) => {
84
- const row = rows.get(outboxAttachmentId);
85
- if (!row || row.accountConfigId !== accountConfigId) {
86
- throw new NotFoundError(`No outbox attachment ${outboxAttachmentId}`);
87
- }
88
- return row;
89
- },
90
- listByOutboxMessage: async (accountConfigId, outboxMessageId) =>
91
- forDraft(accountConfigId, outboxMessageId),
92
- markStored: async (accountConfigId, outboxAttachmentId, sizeBytes) => {
93
- const row = rows.get(outboxAttachmentId);
94
- if (!row || row.accountConfigId !== accountConfigId) return null;
95
- if (row.state !== "Pending") return null;
96
- const next: OutboxAttachmentItem = {
97
- ...row,
98
- state: "Stored",
99
- sizeBytes,
100
- reservationExpiresAt: 0,
101
- };
102
- rows.set(outboxAttachmentId, next);
103
- return next;
104
- },
105
- deleteLapsedReservations: async (
106
- accountConfigId: string,
107
- outboxMessageId: string,
108
- nowSeconds: number,
109
- ) => {
110
- const gone: string[] = [];
111
- for (const row of [...rows.values()]) {
112
- if (
113
- row.accountConfigId === accountConfigId &&
114
- row.outboxMessageId === outboxMessageId &&
115
- row.state === "Pending" &&
116
- row.reservationExpiresAt < nowSeconds
117
- ) {
118
- rows.delete(row.outboxAttachmentId);
119
- gone.push(row.outboxAttachmentId);
120
- }
121
- }
122
- return gone;
123
- },
124
- deleteMany: async (accountConfigId, ids) => {
125
- for (const id of ids) {
126
- if (rows.get(id)?.accountConfigId === accountConfigId) rows.delete(id);
127
- }
128
- },
129
- deleteByOutboxMessage: async (accountConfigId, outboxMessageId) => {
130
- for (const row of forDraft(accountConfigId, outboxMessageId)) {
131
- rows.delete(row.outboxAttachmentId);
132
- }
133
- },
134
- };
135
- };
23
+ let store: Store;
24
+ let draftId: string;
25
+
26
+ afterEach(() => store?.close());
136
27
 
137
- const build = (
28
+ const build = async (
138
29
  status: OutboxMessageItem["status"] = OutboxMessageStatus.draft,
139
30
  now?: () => number,
140
31
  ) => {
141
- const storage = createMockStorageService();
142
- const repository = createRepository();
143
- const outboxMessageService = {
144
- get: async (
145
- accountConfigId: string,
146
- _id: string,
147
- mode?: "read" | "act",
148
- ) => {
149
- if (accountConfigId !== ACCOUNT_CONFIG_ID) {
150
- assert.equal(mode, "act");
151
- throw new ForbiddenError("not yours");
152
- }
153
- return {
154
- outboxMessageId: DRAFT_ID,
155
- accountId: ACCOUNT_ID,
156
- accountConfigId,
157
- status,
158
- } as OutboxMessageItem;
159
- },
160
- } as unknown as IOutboxMessageRepository;
161
-
32
+ store = await openStore();
33
+ const storage = store.storage();
34
+ const draft = await store.outboxMessage.create({
35
+ accountId: ACCOUNT_ID,
36
+ accountConfigId: ACCOUNT_CONFIG_ID,
37
+ fromAddress: "me@example.com",
38
+ toAddresses: ["them@example.com"],
39
+ messageIdValue: "<draft@example.com>",
40
+ status,
41
+ });
42
+ draftId = draft.outboxMessageId;
162
43
  return {
163
44
  service: new OutboxAttachmentService({
164
- outboxMessageService,
165
- outboxAttachmentService: repository,
45
+ outboxMessageService: store.outboxMessage,
46
+ outboxAttachmentService: store.outboxAttachment,
166
47
  storage,
167
48
  now,
168
49
  }),
169
50
  storage,
170
- repository,
171
51
  };
172
52
  };
173
53
 
54
+ const rows = () =>
55
+ store.outboxAttachment.listByOutboxMessage(ACCOUNT_CONFIG_ID, draftId);
56
+
57
+ const rowOf = (outboxAttachmentId: string) =>
58
+ store.outboxAttachment.get(ACCOUNT_CONFIG_ID, outboxAttachmentId);
59
+
60
+ const ids = async () => (await rows()).map((row) => row.outboxAttachmentId);
61
+
174
62
  const mint = (
175
63
  service: OutboxAttachmentService,
176
64
  overrides: Partial<{
@@ -182,7 +70,7 @@ const mint = (
182
70
  ) =>
183
71
  service.mint({
184
72
  accountConfigId: overrides.accountConfigId ?? ACCOUNT_CONFIG_ID,
185
- outboxMessageId: DRAFT_ID,
73
+ outboxMessageId: draftId,
186
74
  filename: overrides.filename ?? "notes.txt",
187
75
  contentType: overrides.contentType ?? "text/plain",
188
76
  sizeBytes: overrides.sizeBytes ?? 10,
@@ -196,14 +84,14 @@ const uploadFor = (
196
84
  storage.storeOutboxAttachment({
197
85
  accountConfigId: ACCOUNT_CONFIG_ID,
198
86
  accountId: ACCOUNT_ID,
199
- outboxMessageId: DRAFT_ID,
87
+ outboxMessageId: draftId,
200
88
  outboxAttachmentId,
201
89
  content: Buffer.alloc(sizeBytes),
202
90
  });
203
91
 
204
92
  describe("reserving room on a draft", () => {
205
93
  it("writes a Pending row and hands back a URL bound to its size", async () => {
206
- const { service, repository } = build();
94
+ const { service } = await build();
207
95
 
208
96
  const result = await mint(service, {
209
97
  filename: "invoice.pdf",
@@ -217,7 +105,7 @@ describe("reserving room on a draft", () => {
217
105
  assert.equal(result.reservation.contentType, "application/pdf");
218
106
  assert.match(result.reservation.uploadUrl, /max=2048/);
219
107
 
220
- const row = repository.rows.get(result.reservation.outboxAttachmentId);
108
+ const row = await rowOf(result.reservation.outboxAttachmentId);
221
109
  assert.equal(row?.state, "Pending");
222
110
  assert.equal(row?.sizeBytes, 2048);
223
111
  // The key on the row is the one the URL addresses — one identity, not two.
@@ -225,7 +113,7 @@ describe("reserving room on a draft", () => {
225
113
  });
226
114
 
227
115
  it("names the content-id a body can refer to the file by", async () => {
228
- const { service } = build();
116
+ const { service } = await build();
229
117
 
230
118
  const result = await mint(service, {
231
119
  filename: "shot.png",
@@ -242,7 +130,7 @@ describe("reserving room on a draft", () => {
242
130
  });
243
131
 
244
132
  it("refuses a declared size over the cap", async () => {
245
- const { service } = build();
133
+ const { service } = await build();
246
134
 
247
135
  const result = await mint(service, {
248
136
  filename: "huge.bin",
@@ -258,7 +146,7 @@ describe("reserving room on a draft", () => {
258
146
  });
259
147
 
260
148
  it("counts a reservation nobody has uploaded against yet", async () => {
261
- const { service } = build();
149
+ const { service } = await build();
262
150
  await mint(service, {
263
151
  sizeBytes: OUTBOX_ATTACHMENT_MAX_TOTAL_BYTES - 1024,
264
152
  });
@@ -278,7 +166,7 @@ describe("reserving room on a draft", () => {
278
166
  });
279
167
 
280
168
  it("refuses once the draft holds the most files a message can", async () => {
281
- const { service } = build();
169
+ const { service } = await build();
282
170
  for (let index = 0; index < OUTBOX_ATTACHMENT_MAX_COUNT; index += 1) {
283
171
  await mint(service, { sizeBytes: 1 });
284
172
  }
@@ -294,7 +182,7 @@ describe("reserving room on a draft", () => {
294
182
  });
295
183
 
296
184
  it("refuses a file declared as empty, and a filename that sanitizes away", async () => {
297
- const { service } = build();
185
+ const { service } = await build();
298
186
 
299
187
  const empty = await mint(service, { sizeBytes: 0 });
300
188
  assert.equal(
@@ -310,7 +198,7 @@ describe("reserving room on a draft", () => {
310
198
  });
311
199
 
312
200
  it("records a filename stripped of its path and a media type it cannot read", async () => {
313
- const { service } = build();
201
+ const { service } = await build();
314
202
 
315
203
  const result = await mint(service, {
316
204
  filename: "../../etc/passwd",
@@ -325,7 +213,10 @@ describe("reserving room on a draft", () => {
325
213
 
326
214
  it("stops holding room once the reservation lapses", async () => {
327
215
  const clock = { seconds: 1_000_000 };
328
- const { service } = build(OutboxMessageStatus.draft, () => clock.seconds);
216
+ const { service } = await build(
217
+ OutboxMessageStatus.draft,
218
+ () => clock.seconds,
219
+ );
329
220
  await mint(service, {
330
221
  sizeBytes: OUTBOX_ATTACHMENT_MAX_TOTAL_BYTES - 1024,
331
222
  });
@@ -341,17 +232,17 @@ describe("reserving room on a draft", () => {
341
232
  });
342
233
 
343
234
  it("denies a mint against a draft owned by someone else", async () => {
344
- const { service, repository } = build();
235
+ const { service } = await build();
345
236
 
346
237
  await assert.rejects(
347
238
  () => mint(service, { accountConfigId: "cfg-stranger" }),
348
239
  ForbiddenError,
349
240
  );
350
- assert.equal(repository.rows.size, 0);
241
+ assert.equal((await rows()).length, 0);
351
242
  });
352
243
 
353
244
  it("refuses to reserve on a message that has left draft", async () => {
354
- const { service } = build(OutboxMessageStatus.queued);
245
+ const { service } = await build(OutboxMessageStatus.queued);
355
246
 
356
247
  await assert.rejects(() => mint(service), {
357
248
  name: "ConflictError",
@@ -368,7 +259,7 @@ describe("the cap under concurrency", () => {
368
259
  * if `reserve` ever grew an await between its count and its insert.
369
260
  */
370
261
  it("holds the byte cap when files are dropped in together", async () => {
371
- const { service } = build();
262
+ const { service } = await build();
372
263
  const fiveMegabytes = 5 * 1024 * 1024;
373
264
 
374
265
  const results = await Promise.all(
@@ -390,7 +281,7 @@ describe("the cap under concurrency", () => {
390
281
  });
391
282
 
392
283
  it("holds the file-count ceiling when files are dropped in together", async () => {
393
- const { service, repository } = build();
284
+ const { service } = await build();
394
285
 
395
286
  const results = await Promise.all(
396
287
  Array.from({ length: OUTBOX_ATTACHMENT_MAX_COUNT + 5 }, () =>
@@ -402,13 +293,13 @@ describe("the cap under concurrency", () => {
402
293
  results.filter((result) => result.outcome === "Minted").length,
403
294
  OUTBOX_ATTACHMENT_MAX_COUNT,
404
295
  );
405
- assert.equal(repository.rows.size, OUTBOX_ATTACHMENT_MAX_COUNT);
296
+ assert.equal((await rows()).length, OUTBOX_ATTACHMENT_MAX_COUNT);
406
297
  });
407
298
  });
408
299
 
409
300
  describe("completing an attachment", () => {
410
301
  it("believes storage about the size and moves the row to Stored", async () => {
411
- const { service, storage, repository } = build();
302
+ const { service, storage } = await build();
412
303
  const minted = await mint(service, { sizeBytes: 512 });
413
304
  assert.equal(minted.outcome, "Minted");
414
305
  if (minted.outcome !== "Minted") return;
@@ -416,7 +307,7 @@ describe("completing an attachment", () => {
416
307
 
417
308
  const completed = await service.complete({
418
309
  accountConfigId: ACCOUNT_CONFIG_ID,
419
- outboxMessageId: DRAFT_ID,
310
+ outboxMessageId: draftId,
420
311
  outboxAttachmentId: minted.reservation.outboxAttachmentId,
421
312
  });
422
313
 
@@ -425,14 +316,14 @@ describe("completing an attachment", () => {
425
316
  assert.equal(completed.attachment.sizeBytes, 512);
426
317
  assert.equal(completed.attachment.state, "Stored");
427
318
  assert.equal(
428
- repository.rows.get(minted.reservation.outboxAttachmentId)
319
+ (await rowOf(minted.reservation.outboxAttachmentId))
429
320
  ?.reservationExpiresAt,
430
321
  0,
431
322
  );
432
323
  });
433
324
 
434
325
  it("answers a repeated completion the same way, not with an error", async () => {
435
- const { service, storage } = build();
326
+ const { service, storage } = await build();
436
327
  const minted = await mint(service, { sizeBytes: 64 });
437
328
  assert.equal(minted.outcome, "Minted");
438
329
  if (minted.outcome !== "Minted") return;
@@ -440,7 +331,7 @@ describe("completing an attachment", () => {
440
331
 
441
332
  const input = {
442
333
  accountConfigId: ACCOUNT_CONFIG_ID,
443
- outboxMessageId: DRAFT_ID,
334
+ outboxMessageId: draftId,
444
335
  outboxAttachmentId: minted.reservation.outboxAttachmentId,
445
336
  };
446
337
  const first = await service.complete(input);
@@ -455,14 +346,14 @@ describe("completing an attachment", () => {
455
346
  });
456
347
 
457
348
  it("never completes an attachment whose object is absent", async () => {
458
- const { service } = build();
349
+ const { service } = await build();
459
350
  const minted = await mint(service, { sizeBytes: 512 });
460
351
  assert.equal(minted.outcome, "Minted");
461
352
  if (minted.outcome !== "Minted") return;
462
353
 
463
354
  const completed = await service.complete({
464
355
  accountConfigId: ACCOUNT_CONFIG_ID,
465
- outboxMessageId: DRAFT_ID,
356
+ outboxMessageId: draftId,
466
357
  outboxAttachmentId: minted.reservation.outboxAttachmentId,
467
358
  });
468
359
 
@@ -473,7 +364,7 @@ describe("completing an attachment", () => {
473
364
  });
474
365
 
475
366
  it("refuses a wrong-sized object, removes it, and gives the room back", async () => {
476
- const { service, storage, repository } = build();
367
+ const { service, storage } = await build();
477
368
  const minted = await mint(service, { sizeBytes: 512 });
478
369
  assert.equal(minted.outcome, "Minted");
479
370
  if (minted.outcome !== "Minted") return;
@@ -481,7 +372,7 @@ describe("completing an attachment", () => {
481
372
 
482
373
  const completed = await service.complete({
483
374
  accountConfigId: ACCOUNT_CONFIG_ID,
484
- outboxMessageId: DRAFT_ID,
375
+ outboxMessageId: draftId,
485
376
  outboxAttachmentId: minted.reservation.outboxAttachmentId,
486
377
  });
487
378
 
@@ -493,12 +384,12 @@ describe("completing an attachment", () => {
493
384
  );
494
385
  // The room the reservation held is reported as released, not as still held.
495
386
  assert.equal(completed.rejection.usedBytes, 0);
496
- assert.equal(repository.rows.size, 0);
387
+ assert.equal((await rows()).length, 0);
497
388
  assert.equal(
498
389
  await storage.statOutboxAttachment(
499
390
  ACCOUNT_CONFIG_ID,
500
391
  ACCOUNT_ID,
501
- DRAFT_ID,
392
+ draftId,
502
393
  minted.reservation.outboxAttachmentId,
503
394
  ),
504
395
  null,
@@ -507,7 +398,7 @@ describe("completing an attachment", () => {
507
398
 
508
399
  it("refuses a completion once the reservation has lapsed", async () => {
509
400
  const clock = { seconds: 3_000_000 };
510
- const { service, storage } = build(
401
+ const { service, storage } = await build(
511
402
  OutboxMessageStatus.draft,
512
403
  () => clock.seconds,
513
404
  );
@@ -520,7 +411,7 @@ describe("completing an attachment", () => {
520
411
 
521
412
  const completed = await service.complete({
522
413
  accountConfigId: ACCOUNT_CONFIG_ID,
523
- outboxMessageId: DRAFT_ID,
414
+ outboxMessageId: draftId,
524
415
  outboxAttachmentId: minted.reservation.outboxAttachmentId,
525
416
  });
526
417
 
@@ -533,7 +424,7 @@ describe("completing an attachment", () => {
533
424
 
534
425
  describe("removing and discarding", () => {
535
426
  it("retainOnly drops the rows and the bytes it was not told to keep", async () => {
536
- const { service, storage, repository } = build();
427
+ const { service, storage } = await build();
537
428
  const kept = await mint(service, { filename: "keep.txt", sizeBytes: 8 });
538
429
  const dropped = await mint(service, { filename: "drop.txt", sizeBytes: 8 });
539
430
  assert.equal(kept.outcome, "Minted");
@@ -541,19 +432,16 @@ describe("removing and discarding", () => {
541
432
  if (kept.outcome !== "Minted" || dropped.outcome !== "Minted") return;
542
433
  await uploadFor(storage, dropped.reservation.outboxAttachmentId, 8);
543
434
 
544
- await service.retainOnly(ACCOUNT_CONFIG_ID, ACCOUNT_ID, DRAFT_ID, [
435
+ await service.retainOnly(ACCOUNT_CONFIG_ID, ACCOUNT_ID, draftId, [
545
436
  kept.reservation.outboxAttachmentId,
546
437
  ]);
547
438
 
548
- assert.deepEqual(
549
- [...repository.rows.keys()],
550
- [kept.reservation.outboxAttachmentId],
551
- );
439
+ assert.deepEqual(await ids(), [kept.reservation.outboxAttachmentId]);
552
440
  assert.equal(
553
441
  await storage.statOutboxAttachment(
554
442
  ACCOUNT_CONFIG_ID,
555
443
  ACCOUNT_ID,
556
- DRAFT_ID,
444
+ draftId,
557
445
  dropped.reservation.outboxAttachmentId,
558
446
  ),
559
447
  null,
@@ -561,20 +449,20 @@ describe("removing and discarding", () => {
561
449
  });
562
450
 
563
451
  it("discardAll takes every row and every object", async () => {
564
- const { service, storage, repository } = build();
452
+ const { service, storage } = await build();
565
453
  const minted = await mint(service, { sizeBytes: 8 });
566
454
  assert.equal(minted.outcome, "Minted");
567
455
  if (minted.outcome !== "Minted") return;
568
456
  await uploadFor(storage, minted.reservation.outboxAttachmentId, 8);
569
457
 
570
- await service.discardAll(ACCOUNT_CONFIG_ID, ACCOUNT_ID, DRAFT_ID);
458
+ await service.discardAll(ACCOUNT_CONFIG_ID, ACCOUNT_ID, draftId);
571
459
 
572
- assert.equal(repository.rows.size, 0);
460
+ assert.equal((await rows()).length, 0);
573
461
  assert.deepEqual(
574
462
  await storage.listOutboxAttachments(
575
463
  ACCOUNT_CONFIG_ID,
576
464
  ACCOUNT_ID,
577
- DRAFT_ID,
465
+ draftId,
578
466
  ),
579
467
  [],
580
468
  );
@@ -594,21 +482,21 @@ describe("retainOnly, which is what attachmentIds drives", () => {
594
482
  };
595
483
 
596
484
  it("keeps everything named and removes everything else, bytes included", async () => {
597
- const { service, storage, repository } = build();
485
+ const { service, storage } = await build();
598
486
  const [keep, drop] = await mintTwo(service);
599
487
  await uploadFor(storage, keep.outboxAttachmentId, 8);
600
488
  await uploadFor(storage, drop.outboxAttachmentId, 8);
601
489
 
602
- await service.retainOnly(ACCOUNT_CONFIG_ID, ACCOUNT_ID, DRAFT_ID, [
490
+ await service.retainOnly(ACCOUNT_CONFIG_ID, ACCOUNT_ID, draftId, [
603
491
  keep.outboxAttachmentId,
604
492
  ]);
605
493
 
606
- assert.deepEqual([...repository.rows.keys()], [keep.outboxAttachmentId]);
494
+ assert.deepEqual(await ids(), [keep.outboxAttachmentId]);
607
495
  assert.ok(
608
496
  await storage.statOutboxAttachment(
609
497
  ACCOUNT_CONFIG_ID,
610
498
  ACCOUNT_ID,
611
- DRAFT_ID,
499
+ draftId,
612
500
  keep.outboxAttachmentId,
613
501
  ),
614
502
  );
@@ -616,7 +504,7 @@ describe("retainOnly, which is what attachmentIds drives", () => {
616
504
  await storage.statOutboxAttachment(
617
505
  ACCOUNT_CONFIG_ID,
618
506
  ACCOUNT_ID,
619
- DRAFT_ID,
507
+ draftId,
620
508
  drop.outboxAttachmentId,
621
509
  ),
622
510
  null,
@@ -624,53 +512,53 @@ describe("retainOnly, which is what attachmentIds drives", () => {
624
512
  });
625
513
 
626
514
  it("an empty list is a real instruction: everything goes", async () => {
627
- const { service, storage, repository } = build();
515
+ const { service, storage } = await build();
628
516
  const [first] = await mintTwo(service);
629
517
  await uploadFor(storage, first.outboxAttachmentId, 8);
630
518
 
631
- await service.retainOnly(ACCOUNT_CONFIG_ID, ACCOUNT_ID, DRAFT_ID, []);
519
+ await service.retainOnly(ACCOUNT_CONFIG_ID, ACCOUNT_ID, draftId, []);
632
520
 
633
- assert.equal(repository.rows.size, 0);
521
+ assert.equal((await rows()).length, 0);
634
522
  assert.deepEqual(
635
523
  await storage.listOutboxAttachments(
636
524
  ACCOUNT_CONFIG_ID,
637
525
  ACCOUNT_ID,
638
- DRAFT_ID,
526
+ draftId,
639
527
  ),
640
528
  [],
641
529
  );
642
530
  });
643
531
 
644
532
  it("naming every id changes nothing", async () => {
645
- const { service, repository } = build();
533
+ const { service } = await build();
646
534
  const [first, second] = await mintTwo(service);
647
535
 
648
- await service.retainOnly(ACCOUNT_CONFIG_ID, ACCOUNT_ID, DRAFT_ID, [
536
+ await service.retainOnly(ACCOUNT_CONFIG_ID, ACCOUNT_ID, draftId, [
649
537
  first.outboxAttachmentId,
650
538
  second.outboxAttachmentId,
651
539
  ]);
652
540
 
653
- assert.equal(repository.rows.size, 2);
541
+ assert.equal((await rows()).length, 2);
654
542
  });
655
543
 
656
544
  it("an id the draft never held is a no-op, not a removal of the rest", async () => {
657
- const { service, repository } = build();
545
+ const { service } = await build();
658
546
  const [first, second] = await mintTwo(service);
659
547
 
660
- await service.retainOnly(ACCOUNT_CONFIG_ID, ACCOUNT_ID, DRAFT_ID, [
548
+ await service.retainOnly(ACCOUNT_CONFIG_ID, ACCOUNT_ID, draftId, [
661
549
  first.outboxAttachmentId,
662
550
  second.outboxAttachmentId,
663
551
  "never-existed",
664
552
  ]);
665
553
 
666
- assert.equal(repository.rows.size, 2);
554
+ assert.equal((await rows()).length, 2);
667
555
  });
668
556
  });
669
557
 
670
558
  describe("a reservation that lapses without ever completing", () => {
671
559
  it("is reaped, so the sweep can collect the bytes it was vouching for", async () => {
672
560
  const clock = { seconds: 5_000_000 };
673
- const { service, storage, repository } = build(
561
+ const { service, storage } = await build(
674
562
  OutboxMessageStatus.draft,
675
563
  () => clock.seconds,
676
564
  );
@@ -684,29 +572,29 @@ describe("a reservation that lapses without ever completing", () => {
684
572
 
685
573
  // Before the reap the row still names the object, which is enough for the
686
574
  // sweep to leave it alone forever.
687
- const live = await service.reapAndListLive(ACCOUNT_CONFIG_ID, DRAFT_ID);
575
+ const live = await service.reapAndListLive(ACCOUNT_CONFIG_ID, draftId);
688
576
 
689
577
  assert.deepEqual(live, []);
690
- assert.equal(repository.rows.size, 0);
578
+ assert.equal((await rows()).length, 0);
691
579
  });
692
580
 
693
581
  it("is not shown to the composer as a file the draft holds", async () => {
694
582
  const clock = { seconds: 6_000_000 };
695
- const { service } = build(OutboxMessageStatus.draft, () => clock.seconds);
583
+ const { service } = await build(
584
+ OutboxMessageStatus.draft,
585
+ () => clock.seconds,
586
+ );
696
587
  await mint(service, { sizeBytes: 32 });
697
588
 
698
- assert.equal(
699
- (await service.listFor(ACCOUNT_CONFIG_ID, DRAFT_ID)).length,
700
- 1,
701
- );
589
+ assert.equal((await service.listFor(ACCOUNT_CONFIG_ID, draftId)).length, 1);
702
590
 
703
591
  clock.seconds += UPLOAD_URL_TTL_SECONDS + 1;
704
592
 
705
- assert.deepEqual(await service.listFor(ACCOUNT_CONFIG_ID, DRAFT_ID), []);
593
+ assert.deepEqual(await service.listFor(ACCOUNT_CONFIG_ID, draftId), []);
706
594
  });
707
595
 
708
596
  it("a confirmed attachment cannot be overwritten through its old URL", async () => {
709
- const { service, storage } = build();
597
+ const { service, storage } = await build();
710
598
  const minted = await mint(service, { sizeBytes: 16 });
711
599
  assert.equal(minted.outcome, "Minted");
712
600
  if (minted.outcome !== "Minted") return;
@@ -722,7 +610,7 @@ describe("a reservation that lapses without ever completing", () => {
722
610
 
723
611
  await service.complete({
724
612
  accountConfigId: ACCOUNT_CONFIG_ID,
725
- outboxMessageId: DRAFT_ID,
613
+ outboxMessageId: draftId,
726
614
  outboxAttachmentId: minted.reservation.outboxAttachmentId,
727
615
  });
728
616