@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,22 +1,12 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import type {
|
|
4
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
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,
|
|
458
|
+
await service.discardAll(ACCOUNT_CONFIG_ID, ACCOUNT_ID, draftId);
|
|
571
459
|
|
|
572
|
-
assert.equal(
|
|
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
|
-
|
|
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
|
|
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,
|
|
490
|
+
await service.retainOnly(ACCOUNT_CONFIG_ID, ACCOUNT_ID, draftId, [
|
|
603
491
|
keep.outboxAttachmentId,
|
|
604
492
|
]);
|
|
605
493
|
|
|
606
|
-
assert.deepEqual(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
519
|
+
await service.retainOnly(ACCOUNT_CONFIG_ID, ACCOUNT_ID, draftId, []);
|
|
632
520
|
|
|
633
|
-
assert.equal(
|
|
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
|
-
|
|
526
|
+
draftId,
|
|
639
527
|
),
|
|
640
528
|
[],
|
|
641
529
|
);
|
|
642
530
|
});
|
|
643
531
|
|
|
644
532
|
it("naming every id changes nothing", async () => {
|
|
645
|
-
const { service
|
|
533
|
+
const { service } = await build();
|
|
646
534
|
const [first, second] = await mintTwo(service);
|
|
647
535
|
|
|
648
|
-
await service.retainOnly(ACCOUNT_CONFIG_ID, ACCOUNT_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(
|
|
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
|
|
545
|
+
const { service } = await build();
|
|
658
546
|
const [first, second] = await mintTwo(service);
|
|
659
547
|
|
|
660
|
-
await service.retainOnly(ACCOUNT_CONFIG_ID, ACCOUNT_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(
|
|
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
|
|
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,
|
|
575
|
+
const live = await service.reapAndListLive(ACCOUNT_CONFIG_ID, draftId);
|
|
688
576
|
|
|
689
577
|
assert.deepEqual(live, []);
|
|
690
|
-
assert.equal(
|
|
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(
|
|
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,
|
|
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:
|
|
613
|
+
outboxMessageId: draftId,
|
|
726
614
|
outboxAttachmentId: minted.reservation.outboxAttachmentId,
|
|
727
615
|
});
|
|
728
616
|
|