@remit/backend 0.0.101 → 0.0.103
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 +1 -1
- package/src/derive/enrichThreadRows.test.ts +79 -0
- package/src/derive/enrichThreadRows.ts +23 -3
- package/src/derive/filterThreadCriteria.test.ts +9 -1
- package/src/handlers/account.ts +11 -10
- package/src/handlers/config.test.ts +79 -18
- package/src/handlers/config.ts +13 -6
- package/src/handlers/message.ts +2 -0
- package/src/service/trigger-sync.test.ts +0 -10
- package/src/service/trigger-sync.ts +1 -1
package/package.json
CHANGED
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
ThreadMessageItem,
|
|
9
9
|
} from "@remit/data-ports";
|
|
10
10
|
import { deriveAddressId } from "@remit/data-ports/id";
|
|
11
|
+
import { hasAbandonedDelete } from "@remit/data-ports/message-settlement";
|
|
12
|
+
import { MessageStatus, MessageSyncStatus } from "@remit/domain-enums";
|
|
11
13
|
import { type EnrichClient, enrichThreadRows } from "./enrichThreadRows.js";
|
|
12
14
|
|
|
13
15
|
const threadRow = (
|
|
@@ -250,3 +252,80 @@ describe("enrichThreadRows — muted", () => {
|
|
|
250
252
|
assert.equal(result?.muted, false);
|
|
251
253
|
});
|
|
252
254
|
});
|
|
255
|
+
|
|
256
|
+
describe("enrichThreadRows — the mutation pair", () => {
|
|
257
|
+
const withMessage = (message: Partial<MessageItem>): EnrichClient => ({
|
|
258
|
+
message: {
|
|
259
|
+
get: async () =>
|
|
260
|
+
[{ messageId: "msg-1", ...message }] as unknown as MessageItem[],
|
|
261
|
+
},
|
|
262
|
+
address: { getAddress: async () => [] },
|
|
263
|
+
messageLabel: { listByMessageIds: async () => [] },
|
|
264
|
+
label: { listByAccountConfig: async () => [] },
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const project = async (message: Partial<MessageItem>) => {
|
|
268
|
+
const [result] = await enrichThreadRows(
|
|
269
|
+
[threadRow("tm-1", "msg-1")],
|
|
270
|
+
withMessage(message),
|
|
271
|
+
"acc-1",
|
|
272
|
+
);
|
|
273
|
+
return result;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
test("projects the pair an abandoned delete leaves behind", async () => {
|
|
277
|
+
const result = await project({
|
|
278
|
+
status: MessageStatus.active,
|
|
279
|
+
syncStatus: MessageSyncStatus.failed,
|
|
280
|
+
});
|
|
281
|
+
assert.equal(result?.status, MessageStatus.active);
|
|
282
|
+
assert.equal(result?.syncStatus, MessageSyncStatus.failed);
|
|
283
|
+
assert.equal(
|
|
284
|
+
hasAbandonedDelete({
|
|
285
|
+
status: result?.status,
|
|
286
|
+
syncStatus: result?.syncStatus,
|
|
287
|
+
}),
|
|
288
|
+
true,
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("projects a move mid-retry without calling it a give-up", async () => {
|
|
293
|
+
const result = await project({
|
|
294
|
+
status: MessageStatus.moving,
|
|
295
|
+
syncStatus: MessageSyncStatus.failed,
|
|
296
|
+
});
|
|
297
|
+
assert.equal(result?.status, MessageStatus.moving);
|
|
298
|
+
assert.equal(result?.syncStatus, MessageSyncStatus.failed);
|
|
299
|
+
assert.equal(
|
|
300
|
+
hasAbandonedDelete({
|
|
301
|
+
status: result?.status,
|
|
302
|
+
syncStatus: result?.syncStatus,
|
|
303
|
+
}),
|
|
304
|
+
false,
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("projects an ordinary inbound row", async () => {
|
|
309
|
+
const result = await project({
|
|
310
|
+
status: MessageStatus.active,
|
|
311
|
+
syncStatus: MessageSyncStatus.pending,
|
|
312
|
+
});
|
|
313
|
+
assert.equal(result?.status, MessageStatus.active);
|
|
314
|
+
assert.equal(result?.syncStatus, MessageSyncStatus.pending);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("a row whose Message row is missing claims no mutation", async () => {
|
|
318
|
+
const [result] = await enrichThreadRows(
|
|
319
|
+
[threadRow("tm-1", "msg-1")],
|
|
320
|
+
buildClient([], []),
|
|
321
|
+
"acc-1",
|
|
322
|
+
);
|
|
323
|
+
assert.equal(
|
|
324
|
+
hasAbandonedDelete({
|
|
325
|
+
status: result?.status,
|
|
326
|
+
syncStatus: result?.syncStatus,
|
|
327
|
+
}),
|
|
328
|
+
false,
|
|
329
|
+
);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
@@ -7,7 +7,12 @@ import type {
|
|
|
7
7
|
ThreadMessageItem,
|
|
8
8
|
} from "@remit/data-ports";
|
|
9
9
|
import { deriveAddressId } from "@remit/data-ports/id";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
MessageStatus,
|
|
12
|
+
MessageSyncStatus,
|
|
13
|
+
SenderTrust,
|
|
14
|
+
StarColor,
|
|
15
|
+
} from "@remit/domain-enums";
|
|
11
16
|
import { deriveAutoMoved } from "./autoMoved.js";
|
|
12
17
|
import { deriveMuted } from "./deriveMuted.js";
|
|
13
18
|
import { deriveSenderTrust } from "./senderTrust.js";
|
|
@@ -56,6 +61,8 @@ const toResponse = (item: ThreadMessageItem): ThreadMessageResponse => ({
|
|
|
56
61
|
updatedAt: item.updatedAt,
|
|
57
62
|
senderTrust: SenderTrust.Unknown,
|
|
58
63
|
muted: false,
|
|
64
|
+
status: MessageStatus.active,
|
|
65
|
+
syncStatus: MessageSyncStatus.pending,
|
|
59
66
|
});
|
|
60
67
|
|
|
61
68
|
/**
|
|
@@ -102,8 +109,13 @@ export const planBatchFetch = (rows: ThreadMessageItem[]): BatchPlan => {
|
|
|
102
109
|
/**
|
|
103
110
|
* Enrich a page of ThreadMessage rows with `senderTrust` and `muted` (both
|
|
104
111
|
* derived from the From Address's flags map), `authenticity`, `autoMoved` and
|
|
105
|
-
* `spamReport
|
|
106
|
-
*
|
|
112
|
+
* `spamReport`, and the `status`/`syncStatus` pair an IMAP mutation leaves
|
|
113
|
+
* behind (all projected straight from the Message row — no ThreadMessage
|
|
114
|
+
* column of their own, see `deriveAutoMoved`). `toResponse` seeds that pair
|
|
115
|
+
* with the ordinary inbound values, which the projection then overwrites; a
|
|
116
|
+
* ThreadMessage row whose Message row is missing from the batch is broken in a
|
|
117
|
+
* way this read path cannot report, and claiming a mutation it has no evidence
|
|
118
|
+
* of would be worse.
|
|
107
119
|
*
|
|
108
120
|
* `category` is not enriched: it is denormalized onto the ThreadMessage row
|
|
109
121
|
* (shared with `Message.category`'s write-once value, see body-sync.ts) and
|
|
@@ -174,6 +186,12 @@ export const enrichThreadRows = async (
|
|
|
174
186
|
const spamReportByMessageId = new Map(
|
|
175
187
|
messages.map((m) => [m.messageId, m.spamReport]),
|
|
176
188
|
);
|
|
189
|
+
const settlementByMessageId = new Map(
|
|
190
|
+
messages.map((m) => [
|
|
191
|
+
m.messageId,
|
|
192
|
+
{ status: m.status, syncStatus: m.syncStatus },
|
|
193
|
+
]),
|
|
194
|
+
);
|
|
177
195
|
const trustByAddressId = new Map(
|
|
178
196
|
addresses.map((a) => [a.addressId, deriveSenderTrust(a.flags)]),
|
|
179
197
|
);
|
|
@@ -194,8 +212,10 @@ export const enrichThreadRows = async (
|
|
|
194
212
|
? (mutedByAddressId.get(addressId) ?? false)
|
|
195
213
|
: false;
|
|
196
214
|
const labels = labelsByMessageId.get(row.messageId);
|
|
215
|
+
const settlement = settlementByMessageId.get(row.messageId);
|
|
197
216
|
return {
|
|
198
217
|
...base,
|
|
218
|
+
...(settlement ?? {}),
|
|
199
219
|
...(authenticity !== undefined ? { authenticity } : {}),
|
|
200
220
|
...(autoMoved !== undefined ? { autoMoved } : {}),
|
|
201
221
|
...(labels !== undefined ? { labels } : {}),
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
3
|
import type { ThreadMessageResponse } from "@remit/api-openapi-types";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
MessageCategory,
|
|
6
|
+
MessageStatus,
|
|
7
|
+
MessageSyncStatus,
|
|
8
|
+
SenderTrust,
|
|
9
|
+
StarColor,
|
|
10
|
+
} from "@remit/domain-enums";
|
|
5
11
|
import {
|
|
6
12
|
filterByOffRowCriteria,
|
|
7
13
|
hasOffRowCriteria,
|
|
@@ -30,6 +36,8 @@ const row = (
|
|
|
30
36
|
updatedAt: 0,
|
|
31
37
|
senderTrust: SenderTrust.Unknown,
|
|
32
38
|
muted: false,
|
|
39
|
+
status: MessageStatus.active,
|
|
40
|
+
syncStatus: MessageSyncStatus.pending,
|
|
33
41
|
...overrides,
|
|
34
42
|
});
|
|
35
43
|
|
package/src/handlers/account.ts
CHANGED
|
@@ -105,14 +105,6 @@ const testOAuthConnection = async (
|
|
|
105
105
|
account: IAccountRepository,
|
|
106
106
|
secrets: SecretsService,
|
|
107
107
|
): Promise<TestConnectionResponse> => {
|
|
108
|
-
if (!existingAccount.oauthRefreshTokenHash) {
|
|
109
|
-
return {
|
|
110
|
-
imapSuccess: false,
|
|
111
|
-
imapError: "oauth_not_configured",
|
|
112
|
-
smtpSuccess: undefined,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
108
|
const tokenService = createMailOAuthService(
|
|
117
109
|
microsoftProviderConfig({
|
|
118
110
|
clientId: process.env.MSOAUTH_CLIENT_ID ?? "",
|
|
@@ -123,9 +115,9 @@ const testOAuthConnection = async (
|
|
|
123
115
|
}),
|
|
124
116
|
);
|
|
125
117
|
|
|
126
|
-
let
|
|
118
|
+
let resolution: Awaited<ReturnType<typeof resolveConnectionCredentials>>;
|
|
127
119
|
try {
|
|
128
|
-
|
|
120
|
+
resolution = await resolveConnectionCredentials(existingAccount, {
|
|
129
121
|
secrets,
|
|
130
122
|
tokenService,
|
|
131
123
|
persistRotatedToken: async (id, encryptedHash, updatedAt) => {
|
|
@@ -149,6 +141,15 @@ const testOAuthConnection = async (
|
|
|
149
141
|
throw err;
|
|
150
142
|
}
|
|
151
143
|
|
|
144
|
+
if (resolution.status === "missing") {
|
|
145
|
+
return {
|
|
146
|
+
imapSuccess: false,
|
|
147
|
+
imapError: "oauth_not_configured",
|
|
148
|
+
smtpSuccess: undefined,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const credentials = resolution.credentials;
|
|
152
|
+
|
|
152
153
|
const result: TestConnectionResponse = {
|
|
153
154
|
imapSuccess: false,
|
|
154
155
|
smtpSuccess: undefined,
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { describe, it } from "node:test";
|
|
3
3
|
import type { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
|
|
4
|
+
import { AccountAuthType } from "@remit/domain-enums";
|
|
4
5
|
import { triggerConfigLoadSyncs } from "./config.js";
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The credential a password account stores. Its content is never read here —
|
|
9
|
+
* only its presence decides whether a sync is worth enqueuing.
|
|
10
|
+
*/
|
|
11
|
+
const withPassword = (accountId: string) => ({
|
|
12
|
+
accountId,
|
|
13
|
+
passwordHash: '{"ciphertext":"x"}',
|
|
14
|
+
});
|
|
15
|
+
|
|
6
16
|
const QUEUE_URL = "http://localhost:9324/000000000000/remit-dev-mailboxes";
|
|
7
17
|
|
|
8
18
|
interface LogCall {
|
|
@@ -44,17 +54,60 @@ describe("triggerConfigLoadSyncs", () => {
|
|
|
44
54
|
const sent: SendMessageCommand[] = [];
|
|
45
55
|
const { logger, info, error } = createLoggerSpy();
|
|
46
56
|
|
|
47
|
-
await triggerConfigLoadSyncs(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
await triggerConfigLoadSyncs(
|
|
58
|
+
"config-1",
|
|
59
|
+
[withPassword("acc-a"), withPassword("acc-b")],
|
|
60
|
+
{
|
|
61
|
+
sqsClient: okSqsClient(sent),
|
|
62
|
+
queueUrl: QUEUE_URL,
|
|
63
|
+
logger,
|
|
64
|
+
},
|
|
65
|
+
);
|
|
52
66
|
|
|
53
67
|
assert.equal(sent.length, 2);
|
|
54
68
|
assert.equal(info.length, 2);
|
|
55
69
|
assert.equal(error.length, 0);
|
|
56
70
|
});
|
|
57
71
|
|
|
72
|
+
it("skips an account that stores no credential (issue #1120)", async () => {
|
|
73
|
+
const sent: SendMessageCommand[] = [];
|
|
74
|
+
const { logger, error } = createLoggerSpy();
|
|
75
|
+
|
|
76
|
+
await triggerConfigLoadSyncs(
|
|
77
|
+
"config-1",
|
|
78
|
+
[
|
|
79
|
+
{ accountId: "imported-no-password" },
|
|
80
|
+
withPassword("acc-with-password"),
|
|
81
|
+
],
|
|
82
|
+
{ sqsClient: okSqsClient(sent), queueUrl: QUEUE_URL, logger },
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
assert.equal(sent.length, 1);
|
|
86
|
+
assert.equal(error.length, 0);
|
|
87
|
+
const body = JSON.parse(String(sent[0]?.input.MessageBody)) as {
|
|
88
|
+
accountId: string;
|
|
89
|
+
};
|
|
90
|
+
assert.equal(body.accountId, "acc-with-password");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("skips an OAuth account that stores no refresh token", async () => {
|
|
94
|
+
const sent: SendMessageCommand[] = [];
|
|
95
|
+
const { logger } = createLoggerSpy();
|
|
96
|
+
|
|
97
|
+
await triggerConfigLoadSyncs(
|
|
98
|
+
"config-1",
|
|
99
|
+
[
|
|
100
|
+
{
|
|
101
|
+
accountId: "oauth-unconnected",
|
|
102
|
+
authType: AccountAuthType.OauthMicrosoft,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
{ sqsClient: okSqsClient(sent), queueUrl: QUEUE_URL, logger },
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
assert.equal(sent.length, 0);
|
|
109
|
+
});
|
|
110
|
+
|
|
58
111
|
it("does NOT reject when the SQS enqueue fails (read stays resilient)", async () => {
|
|
59
112
|
const econnrefused = Object.assign(new Error(""), {
|
|
60
113
|
name: "AggregateError",
|
|
@@ -63,7 +116,7 @@ describe("triggerConfigLoadSyncs", () => {
|
|
|
63
116
|
const { logger } = createLoggerSpy();
|
|
64
117
|
|
|
65
118
|
await assert.doesNotReject(
|
|
66
|
-
triggerConfigLoadSyncs("config-1", ["acc-a"], {
|
|
119
|
+
triggerConfigLoadSyncs("config-1", [withPassword("acc-a")], {
|
|
67
120
|
sqsClient: failingSqsClient(econnrefused),
|
|
68
121
|
queueUrl: QUEUE_URL,
|
|
69
122
|
logger,
|
|
@@ -78,7 +131,7 @@ describe("triggerConfigLoadSyncs", () => {
|
|
|
78
131
|
});
|
|
79
132
|
const { logger, error } = createLoggerSpy();
|
|
80
133
|
|
|
81
|
-
await triggerConfigLoadSyncs("config-1", ["acc-a"], {
|
|
134
|
+
await triggerConfigLoadSyncs("config-1", [withPassword("acc-a")], {
|
|
82
135
|
sqsClient: failingSqsClient(econnrefused),
|
|
83
136
|
queueUrl: QUEUE_URL,
|
|
84
137
|
logger,
|
|
@@ -109,11 +162,15 @@ describe("triggerConfigLoadSyncs", () => {
|
|
|
109
162
|
} as unknown as SQSClient;
|
|
110
163
|
const { logger, info, error } = createLoggerSpy();
|
|
111
164
|
|
|
112
|
-
await triggerConfigLoadSyncs(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
165
|
+
await triggerConfigLoadSyncs(
|
|
166
|
+
"config-1",
|
|
167
|
+
[withPassword("acc-a"), withPassword("acc-b")],
|
|
168
|
+
{
|
|
169
|
+
sqsClient,
|
|
170
|
+
queueUrl: QUEUE_URL,
|
|
171
|
+
logger,
|
|
172
|
+
},
|
|
173
|
+
);
|
|
117
174
|
|
|
118
175
|
assert.equal(error.length, 1);
|
|
119
176
|
assert.equal(info.length, 1);
|
|
@@ -127,7 +184,7 @@ describe("triggerConfigLoadSyncs", () => {
|
|
|
127
184
|
});
|
|
128
185
|
const { logger, error } = createLoggerSpy();
|
|
129
186
|
|
|
130
|
-
await triggerConfigLoadSyncs("config-1", ["acc-a"], {
|
|
187
|
+
await triggerConfigLoadSyncs("config-1", [withPassword("acc-a")], {
|
|
131
188
|
sqsClient: failingSqsClient(sdkError),
|
|
132
189
|
queueUrl: QUEUE_URL,
|
|
133
190
|
logger,
|
|
@@ -161,11 +218,15 @@ describe("triggerConfigLoadSyncs", () => {
|
|
|
161
218
|
let readResolved = false;
|
|
162
219
|
try {
|
|
163
220
|
// Fire-and-forget exactly as the getConfig handler does.
|
|
164
|
-
void triggerConfigLoadSyncs(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
221
|
+
void triggerConfigLoadSyncs(
|
|
222
|
+
"config-1",
|
|
223
|
+
[withPassword("acc-a"), withPassword("acc-b")],
|
|
224
|
+
{
|
|
225
|
+
sqsClient: failingSqsClient(econnrefused),
|
|
226
|
+
queueUrl: QUEUE_URL,
|
|
227
|
+
logger,
|
|
228
|
+
},
|
|
229
|
+
);
|
|
169
230
|
|
|
170
231
|
// A concurrent "read" sharing the event loop, like /mailboxes or /outbox.
|
|
171
232
|
await new Promise((resolve) => setImmediate(resolve)).then(() => {
|
package/src/handlers/config.ts
CHANGED
|
@@ -14,6 +14,10 @@ import type { AccountConfigItem, MailboxItem } from "@remit/data-ports";
|
|
|
14
14
|
import { ConfigNotEmptyError, NotFoundError } from "@remit/data-ports/errors";
|
|
15
15
|
import type { CanonicalMailboxRoleValue } from "@remit/data-ports/folder-role";
|
|
16
16
|
import { logger } from "@remit/logger-lambda";
|
|
17
|
+
import {
|
|
18
|
+
hasStoredCredential,
|
|
19
|
+
type StoredCredentialFields,
|
|
20
|
+
} from "@remit/mailbox-service/account-credentials";
|
|
17
21
|
import {
|
|
18
22
|
EMBEDDING_PROVIDER_OFF,
|
|
19
23
|
readEmbeddingProviderFromEnv,
|
|
@@ -77,14 +81,20 @@ const defaultConfigSyncTriggerDeps = (): ConfigSyncTriggerDeps => ({
|
|
|
77
81
|
* Each failure is logged as a distinct structured error carrying the SDK error
|
|
78
82
|
* name/code on dedicated fields plus a stable `alert: "sync_trigger_failed"`
|
|
79
83
|
* discriminator a CloudWatch metric filter / alarm can key off.
|
|
84
|
+
*
|
|
85
|
+
* An account storing no credential is not enqueued at all (issue #1120): the
|
|
86
|
+
* import wizard lands accounts without a password by design, and the client
|
|
87
|
+
* polls this read while the user is still typing one. The guard is the stored
|
|
88
|
+
* credential rather than `connectionState`, which stays `credentials_missing`
|
|
89
|
+
* until a connect succeeds — see `hasStoredCredential`.
|
|
80
90
|
*/
|
|
81
91
|
export const triggerConfigLoadSyncs = async (
|
|
82
92
|
accountConfigId: string,
|
|
83
|
-
|
|
93
|
+
accounts: ReadonlyArray<{ accountId: string } & StoredCredentialFields>,
|
|
84
94
|
deps: ConfigSyncTriggerDeps = defaultConfigSyncTriggerDeps(),
|
|
85
95
|
): Promise<void> => {
|
|
86
96
|
await Promise.all(
|
|
87
|
-
|
|
97
|
+
accounts.filter(hasStoredCredential).map(({ accountId }) =>
|
|
88
98
|
fireAndForget(
|
|
89
99
|
async () => {
|
|
90
100
|
const { eventId } = await triggerAccountSync({
|
|
@@ -224,10 +234,7 @@ export const ConfigOperations: Record<
|
|
|
224
234
|
// triggerConfigLoadSyncs swallows nothing — it logs each failure loudly
|
|
225
235
|
// with an alertable structured field — but it also never rejects, so the
|
|
226
236
|
// `void` here cannot leak an unhandled rejection into a later request.
|
|
227
|
-
void triggerConfigLoadSyncs(
|
|
228
|
-
accountConfigId,
|
|
229
|
-
activeAccounts.map((acc) => acc.accountId),
|
|
230
|
-
);
|
|
237
|
+
void triggerConfigLoadSyncs(accountConfigId, activeAccounts);
|
|
231
238
|
|
|
232
239
|
// An import that named folders IMAP had not produced yet rides the config
|
|
233
240
|
// read rather than a route of its own, so nothing has to poll for it.
|
package/src/handlers/message.ts
CHANGED
|
@@ -454,6 +454,8 @@ export const MessageOperations: Record<
|
|
|
454
454
|
internalDate: message.internalDate,
|
|
455
455
|
messageIdHeader: message.messageIdHeader,
|
|
456
456
|
authenticity: message.authenticity,
|
|
457
|
+
status: message.status,
|
|
458
|
+
syncStatus: message.syncStatus,
|
|
457
459
|
...(autoMoved ? { autoMoved } : {}),
|
|
458
460
|
...(labels.length > 0 ? { labels } : {}),
|
|
459
461
|
...(message.spamReport ? { spamReport: message.spamReport } : {}),
|
|
@@ -20,7 +20,6 @@ const parseBody = (cmd: SendMessageCommand): Record<string, unknown> => {
|
|
|
20
20
|
describe("buildSyncMailboxesCommand", () => {
|
|
21
21
|
it("sets MessageGroupId to accountId for FIFO queues", () => {
|
|
22
22
|
const cmd = buildSyncMailboxesCommand({
|
|
23
|
-
sqsClient: {} as SQSClient,
|
|
24
23
|
queueUrl: FIFO_QUEUE_URL,
|
|
25
24
|
accountId: "account-abc",
|
|
26
25
|
});
|
|
@@ -30,7 +29,6 @@ describe("buildSyncMailboxesCommand", () => {
|
|
|
30
29
|
|
|
31
30
|
it("defaults MessageDeduplicationId to the event's own id for FIFO queues", () => {
|
|
32
31
|
const cmd = buildSyncMailboxesCommand({
|
|
33
|
-
sqsClient: {} as SQSClient,
|
|
34
32
|
queueUrl: FIFO_QUEUE_URL,
|
|
35
33
|
accountId: "account-abc",
|
|
36
34
|
});
|
|
@@ -44,12 +42,10 @@ describe("buildSyncMailboxesCommand", () => {
|
|
|
44
42
|
// must each reach the queue.
|
|
45
43
|
it("gives two triggers for one account distinct dedup ids", () => {
|
|
46
44
|
const first = buildSyncMailboxesCommand({
|
|
47
|
-
sqsClient: {} as SQSClient,
|
|
48
45
|
queueUrl: FIFO_QUEUE_URL,
|
|
49
46
|
accountId: "account-abc",
|
|
50
47
|
});
|
|
51
48
|
const second = buildSyncMailboxesCommand({
|
|
52
|
-
sqsClient: {} as SQSClient,
|
|
53
49
|
queueUrl: FIFO_QUEUE_URL,
|
|
54
50
|
accountId: "account-abc",
|
|
55
51
|
});
|
|
@@ -65,7 +61,6 @@ describe("buildSyncMailboxesCommand", () => {
|
|
|
65
61
|
// skips the ones that were just enumerated.
|
|
66
62
|
it("marks an explicitly-requested trigger on the event", () => {
|
|
67
63
|
const cmd = buildSyncMailboxesCommand({
|
|
68
|
-
sqsClient: {} as SQSClient,
|
|
69
64
|
queueUrl: FIFO_QUEUE_URL,
|
|
70
65
|
accountId: "account-abc",
|
|
71
66
|
explicitRequest: true,
|
|
@@ -76,7 +71,6 @@ describe("buildSyncMailboxesCommand", () => {
|
|
|
76
71
|
|
|
77
72
|
it("leaves a side-effect trigger unmarked", () => {
|
|
78
73
|
const cmd = buildSyncMailboxesCommand({
|
|
79
|
-
sqsClient: {} as SQSClient,
|
|
80
74
|
queueUrl: FIFO_QUEUE_URL,
|
|
81
75
|
accountId: "account-abc",
|
|
82
76
|
});
|
|
@@ -86,7 +80,6 @@ describe("buildSyncMailboxesCommand", () => {
|
|
|
86
80
|
|
|
87
81
|
it("does not set FIFO params for standard queues", () => {
|
|
88
82
|
const cmd = buildSyncMailboxesCommand({
|
|
89
|
-
sqsClient: {} as SQSClient,
|
|
90
83
|
queueUrl: STANDARD_QUEUE_URL,
|
|
91
84
|
accountId: "account-abc",
|
|
92
85
|
});
|
|
@@ -97,7 +90,6 @@ describe("buildSyncMailboxesCommand", () => {
|
|
|
97
90
|
|
|
98
91
|
it("builds a SYNC_MAILBOXES event body with accountId and eventId", () => {
|
|
99
92
|
const cmd = buildSyncMailboxesCommand({
|
|
100
|
-
sqsClient: {} as SQSClient,
|
|
101
93
|
queueUrl: FIFO_QUEUE_URL,
|
|
102
94
|
accountId: "account-abc",
|
|
103
95
|
});
|
|
@@ -111,7 +103,6 @@ describe("buildSyncMailboxesCommand", () => {
|
|
|
111
103
|
|
|
112
104
|
it("targets the configured queue url", () => {
|
|
113
105
|
const cmd = buildSyncMailboxesCommand({
|
|
114
|
-
sqsClient: {} as SQSClient,
|
|
115
106
|
queueUrl: FIFO_QUEUE_URL,
|
|
116
107
|
accountId: "account-abc",
|
|
117
108
|
});
|
|
@@ -121,7 +112,6 @@ describe("buildSyncMailboxesCommand", () => {
|
|
|
121
112
|
|
|
122
113
|
it("uses the caller-supplied dedupId instead of the manual default", () => {
|
|
123
114
|
const cmd = buildSyncMailboxesCommand({
|
|
124
|
-
sqsClient: {} as SQSClient,
|
|
125
115
|
queueUrl: FIFO_QUEUE_URL,
|
|
126
116
|
accountId: "account-abc",
|
|
127
117
|
dedupId: "SYNC_MAILBOXES:scheduled:account-abc:12345",
|
|
@@ -66,7 +66,7 @@ export const buildScheduledSyncDedupId = (
|
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
export const buildSyncMailboxesCommand = (
|
|
69
|
-
input: TriggerAccountSyncInput,
|
|
69
|
+
input: Omit<TriggerAccountSyncInput, "sqsClient">,
|
|
70
70
|
): SendMessageCommand => {
|
|
71
71
|
const { queueUrl, accountId, dedupId, explicitRequest } = input;
|
|
72
72
|
const event: SyncMailboxesEvent = {
|