@remit/backend 0.0.92 → 0.0.93
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.
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, test } from "node:test";
|
|
2
|
+
import { after, before, describe, test } from "node:test";
|
|
3
3
|
import type {
|
|
4
4
|
CalendarSuggestionItem,
|
|
5
5
|
CreateFilterInput,
|
|
6
6
|
FilterItem,
|
|
7
7
|
ICalendarSuggestionRepository,
|
|
8
8
|
MessageData,
|
|
9
|
-
|
|
9
|
+
PutCalendarSuggestionInput,
|
|
10
10
|
SettleCalendarSuggestionInput,
|
|
11
11
|
} from "@remit/data-ports";
|
|
12
12
|
import {
|
|
13
13
|
CalendarInviteMethod,
|
|
14
14
|
CalendarSuggestionSource,
|
|
15
15
|
CalendarSuggestionState,
|
|
16
|
+
FilterState,
|
|
16
17
|
} from "@remit/domain-enums";
|
|
18
|
+
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
19
|
+
import type { Context } from "openapi-backend";
|
|
20
|
+
import { deriveAccountConfigId } from "../auth.js";
|
|
21
|
+
import {
|
|
22
|
+
_resetForTest,
|
|
23
|
+
type RemitClient,
|
|
24
|
+
setClient,
|
|
25
|
+
} from "../service/data-client.js";
|
|
26
|
+
import { createCalendarSqliteClient } from "./calendar-sqlite-fixture.js";
|
|
17
27
|
import {
|
|
18
28
|
assertSettleable,
|
|
29
|
+
CalendarSuggestionActionOperations,
|
|
30
|
+
CalendarSuggestionOperations,
|
|
19
31
|
type MuteSenderDeps,
|
|
20
32
|
muteSender,
|
|
21
33
|
settleSuggestion,
|
|
@@ -277,3 +289,270 @@ describe("muteSender", () => {
|
|
|
277
289
|
assert.deepEqual(created, []);
|
|
278
290
|
});
|
|
279
291
|
});
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* The suggestion wrappers driven the way an HTTP request drives them — through
|
|
295
|
+
* the registered client — against the SQLite store the self-host build ships.
|
|
296
|
+
* The unit tests above pin each half; these pin what one request does.
|
|
297
|
+
*/
|
|
298
|
+
|
|
299
|
+
type Handler = (
|
|
300
|
+
context: Context,
|
|
301
|
+
event: APIGatewayProxyEvent,
|
|
302
|
+
) => Promise<Record<string, unknown>>;
|
|
303
|
+
|
|
304
|
+
const listSuggestions =
|
|
305
|
+
CalendarSuggestionOperations.CalendarSuggestionOperations_listCalendarSuggestions as Handler;
|
|
306
|
+
const acceptSuggestion =
|
|
307
|
+
CalendarSuggestionActionOperations.CalendarSuggestionActionOperations_acceptCalendarSuggestion as Handler;
|
|
308
|
+
const dismissSuggestion =
|
|
309
|
+
CalendarSuggestionActionOperations.CalendarSuggestionActionOperations_dismissCalendarSuggestion as Handler;
|
|
310
|
+
|
|
311
|
+
interface Card {
|
|
312
|
+
suggestionId: string;
|
|
313
|
+
state: string;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let client: RemitClient;
|
|
317
|
+
let cleanup: () => void;
|
|
318
|
+
let mintedSubs = 0;
|
|
319
|
+
|
|
320
|
+
const contextOf = (request: {
|
|
321
|
+
params?: Record<string, string>;
|
|
322
|
+
query?: Record<string, unknown>;
|
|
323
|
+
requestBody?: unknown;
|
|
324
|
+
}): Context => ({ request }) as unknown as Context;
|
|
325
|
+
|
|
326
|
+
const anAccount = (): {
|
|
327
|
+
accountConfigId: string;
|
|
328
|
+
event: APIGatewayProxyEvent;
|
|
329
|
+
} => {
|
|
330
|
+
mintedSubs += 1;
|
|
331
|
+
const sub = `calendar-suggestion-sub-${mintedSubs}`;
|
|
332
|
+
return {
|
|
333
|
+
accountConfigId: deriveAccountConfigId(sub),
|
|
334
|
+
event: {
|
|
335
|
+
requestContext: { authorizer: { claims: { sub } } },
|
|
336
|
+
} as unknown as APIGatewayProxyEvent,
|
|
337
|
+
};
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const INVITATION = [
|
|
341
|
+
"BEGIN:VCALENDAR",
|
|
342
|
+
"VERSION:2.0",
|
|
343
|
+
"METHOD:REQUEST",
|
|
344
|
+
"BEGIN:VEVENT",
|
|
345
|
+
"UID:invite@example.test",
|
|
346
|
+
"DTSTART:20260901T080000Z",
|
|
347
|
+
"DTEND:20260901T090000Z",
|
|
348
|
+
"SUMMARY:Quarterly review",
|
|
349
|
+
"END:VEVENT",
|
|
350
|
+
"END:VCALENDAR",
|
|
351
|
+
"",
|
|
352
|
+
].join("\r\n");
|
|
353
|
+
|
|
354
|
+
const putSuggestion = (
|
|
355
|
+
accountConfigId: string,
|
|
356
|
+
messageId: string,
|
|
357
|
+
): Promise<CalendarSuggestionItem> =>
|
|
358
|
+
client.calendarSuggestion.put({
|
|
359
|
+
accountConfigId,
|
|
360
|
+
messageId,
|
|
361
|
+
bodyPartId: "part-1",
|
|
362
|
+
icalUid: "invite@example.test",
|
|
363
|
+
sequence: 0,
|
|
364
|
+
method: CalendarInviteMethod.Request,
|
|
365
|
+
source: CalendarSuggestionSource.IcalendarPart,
|
|
366
|
+
summary: "Quarterly review",
|
|
367
|
+
dtStart: "2026-09-01T10:00:00+02:00",
|
|
368
|
+
dtEnd: "2026-09-01T11:00:00+02:00",
|
|
369
|
+
allDay: false,
|
|
370
|
+
location: "Room 4",
|
|
371
|
+
organizer: "organizer@example.test",
|
|
372
|
+
zoneCertainty: "Explicit",
|
|
373
|
+
icalData: INVITATION,
|
|
374
|
+
} as PutCalendarSuggestionInput);
|
|
375
|
+
|
|
376
|
+
/** A message with a From address, which is all muting a sender reads. */
|
|
377
|
+
const seedMessageFrom = async (
|
|
378
|
+
messageId: string,
|
|
379
|
+
sender: string,
|
|
380
|
+
): Promise<void> => {
|
|
381
|
+
await client.envelope.createEnvelope({
|
|
382
|
+
envelopeId: "",
|
|
383
|
+
messageId,
|
|
384
|
+
dateValue: Date.parse("2026-08-30T08:00:00Z"),
|
|
385
|
+
dateRaw: "Sun, 30 Aug 2026 08:00:00 +0000",
|
|
386
|
+
subject: "Invitation: Quarterly review",
|
|
387
|
+
messageIdValue: `<${messageId}@example.test>`,
|
|
388
|
+
});
|
|
389
|
+
await client.address.createEnvelopeAddress({
|
|
390
|
+
messageId,
|
|
391
|
+
addressId: `address-${messageId}`,
|
|
392
|
+
displayName: "The organiser",
|
|
393
|
+
normalizedEmail: sender,
|
|
394
|
+
addressRole: "from",
|
|
395
|
+
addressOrder: 0,
|
|
396
|
+
});
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
before(async () => {
|
|
400
|
+
_resetForTest();
|
|
401
|
+
({ client, cleanup } = await createCalendarSqliteClient());
|
|
402
|
+
setClient(client);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
after(() => {
|
|
406
|
+
_resetForTest();
|
|
407
|
+
cleanup();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
describe("GET /calendar-suggestions", () => {
|
|
411
|
+
test("hands the pending set back one page at a time", async () => {
|
|
412
|
+
const { accountConfigId, event } = anAccount();
|
|
413
|
+
const seeded = await Promise.all(
|
|
414
|
+
Array.from({ length: 101 }, (_unused, index) =>
|
|
415
|
+
putSuggestion(accountConfigId, `msg-page-${index}`),
|
|
416
|
+
),
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
const first = (await listSuggestions(
|
|
420
|
+
contextOf({ query: { state: CalendarSuggestionState.Pending } }),
|
|
421
|
+
event,
|
|
422
|
+
)) as unknown as { items: Card[]; continuationToken?: string };
|
|
423
|
+
assert.equal(first.items.length, 100);
|
|
424
|
+
assert.ok(first.continuationToken, "a full page names where to continue");
|
|
425
|
+
|
|
426
|
+
const second = (await listSuggestions(
|
|
427
|
+
contextOf({
|
|
428
|
+
query: {
|
|
429
|
+
state: CalendarSuggestionState.Pending,
|
|
430
|
+
continuationToken: first.continuationToken,
|
|
431
|
+
},
|
|
432
|
+
}),
|
|
433
|
+
event,
|
|
434
|
+
)) as unknown as { items: Card[]; continuationToken?: string };
|
|
435
|
+
|
|
436
|
+
assert.equal(second.items.length, 1);
|
|
437
|
+
assert.equal(second.continuationToken, undefined);
|
|
438
|
+
const paged = new Set(
|
|
439
|
+
[...first.items, ...second.items].map((card) => card.suggestionId),
|
|
440
|
+
);
|
|
441
|
+
assert.equal(
|
|
442
|
+
paged.size,
|
|
443
|
+
seeded.length,
|
|
444
|
+
"the two pages cover the set once each, with no card in both",
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("answers only the state that was asked for, and keeps the raw bytes back", async () => {
|
|
449
|
+
const { accountConfigId, event } = anAccount();
|
|
450
|
+
await putSuggestion(accountConfigId, "msg-pending");
|
|
451
|
+
const dismissed = await putSuggestion(accountConfigId, "msg-dismissed");
|
|
452
|
+
await client.calendarSuggestion.settle(
|
|
453
|
+
accountConfigId,
|
|
454
|
+
dismissed.suggestionId,
|
|
455
|
+
{
|
|
456
|
+
state: CalendarSuggestionState.Dismissed,
|
|
457
|
+
acceptedCalendarObjectId: "",
|
|
458
|
+
},
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const pending = (await listSuggestions(
|
|
462
|
+
contextOf({ query: { state: CalendarSuggestionState.Pending } }),
|
|
463
|
+
event,
|
|
464
|
+
)) as unknown as { items: Card[] };
|
|
465
|
+
|
|
466
|
+
assert.deepEqual(
|
|
467
|
+
pending.items.map((card) => card.state),
|
|
468
|
+
[CalendarSuggestionState.Pending],
|
|
469
|
+
);
|
|
470
|
+
assert.equal("icalData" in (pending.items[0] ?? {}), false);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
describe("POST /calendar-suggestions/{suggestionId}/accept", () => {
|
|
475
|
+
test("answers not-found for a calendar on another account, before writing anything", async () => {
|
|
476
|
+
const stranger = anAccount();
|
|
477
|
+
const strangersCalendar = await client.calendarCollection.create({
|
|
478
|
+
accountConfigId: stranger.accountConfigId,
|
|
479
|
+
urlSegment: "default",
|
|
480
|
+
displayName: "Calendar",
|
|
481
|
+
});
|
|
482
|
+
const { accountConfigId, event } = anAccount();
|
|
483
|
+
const card = await putSuggestion(accountConfigId, "msg-cross-account");
|
|
484
|
+
|
|
485
|
+
await assert.rejects(
|
|
486
|
+
() =>
|
|
487
|
+
acceptSuggestion(
|
|
488
|
+
contextOf({
|
|
489
|
+
params: { suggestionId: card.suggestionId },
|
|
490
|
+
requestBody: { calendarId: strangersCalendar.calendarId },
|
|
491
|
+
}),
|
|
492
|
+
event,
|
|
493
|
+
),
|
|
494
|
+
(error: unknown) => (error as { statusCode?: number }).statusCode === 404,
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
assert.deepEqual(
|
|
498
|
+
await client.calendarObject.listByCalendar(strangersCalendar.calendarId),
|
|
499
|
+
[],
|
|
500
|
+
"nothing was written into the calendar the caller does not hold",
|
|
501
|
+
);
|
|
502
|
+
const untouched = await client.calendarSuggestion.get(
|
|
503
|
+
accountConfigId,
|
|
504
|
+
card.suggestionId,
|
|
505
|
+
);
|
|
506
|
+
assert.equal(untouched.state, CalendarSuggestionState.Pending);
|
|
507
|
+
assert.equal(untouched.acceptedCalendarObjectId, "");
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
describe("POST /calendar-suggestions/{suggestionId}/dismiss", () => {
|
|
512
|
+
test("settles the card and writes the sender's mute rule in one request", async () => {
|
|
513
|
+
const { accountConfigId, event } = anAccount();
|
|
514
|
+
const card = await putSuggestion(accountConfigId, "msg-mute");
|
|
515
|
+
await seedMessageFrom("msg-mute", "organizer@example.test");
|
|
516
|
+
|
|
517
|
+
const dismissed = (await dismissSuggestion(
|
|
518
|
+
contextOf({
|
|
519
|
+
params: { suggestionId: card.suggestionId },
|
|
520
|
+
requestBody: { muteSender: true },
|
|
521
|
+
}),
|
|
522
|
+
event,
|
|
523
|
+
)) as unknown as Card;
|
|
524
|
+
|
|
525
|
+
assert.equal(dismissed.state, CalendarSuggestionState.Dismissed);
|
|
526
|
+
const rules = await client.filter.listByAccountAndState(
|
|
527
|
+
accountConfigId,
|
|
528
|
+
FilterState.Active,
|
|
529
|
+
);
|
|
530
|
+
assert.equal(rules.length, 1);
|
|
531
|
+
assert.deepEqual(rules[0]?.literalClauses, [
|
|
532
|
+
{ field: "From", value: "organizer@example.test" },
|
|
533
|
+
]);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("writes no rule when the request does not ask to mute", async () => {
|
|
537
|
+
const { accountConfigId, event } = anAccount();
|
|
538
|
+
const card = await putSuggestion(accountConfigId, "msg-quiet");
|
|
539
|
+
await seedMessageFrom("msg-quiet", "organizer@example.test");
|
|
540
|
+
|
|
541
|
+
const dismissed = (await dismissSuggestion(
|
|
542
|
+
contextOf({
|
|
543
|
+
params: { suggestionId: card.suggestionId },
|
|
544
|
+
requestBody: {},
|
|
545
|
+
}),
|
|
546
|
+
event,
|
|
547
|
+
)) as unknown as Card;
|
|
548
|
+
|
|
549
|
+
assert.equal(dismissed.state, CalendarSuggestionState.Dismissed);
|
|
550
|
+
assert.deepEqual(
|
|
551
|
+
await client.filter.listByAccountAndState(
|
|
552
|
+
accountConfigId,
|
|
553
|
+
FilterState.Active,
|
|
554
|
+
),
|
|
555
|
+
[],
|
|
556
|
+
);
|
|
557
|
+
});
|
|
558
|
+
});
|