@remit/backend 0.0.143 → 0.0.145
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/dist/dev-server/content-handler.d.ts +38 -0
- package/dist/dev-server/content-handler.js +46 -0
- package/dist/src/api.d.ts +5 -0
- package/dist/src/api.js +73 -0
- package/dist/src/auth.d.ts +24 -0
- package/dist/src/auth.js +81 -0
- package/dist/src/config/api-config.d.ts +20 -0
- package/dist/src/config/api-config.js +1 -0
- package/dist/src/config/from-env.d.ts +2 -0
- package/dist/src/config/from-env.js +65 -0
- package/dist/src/config/msoauth-secrets-manager.d.ts +2 -0
- package/dist/src/config/msoauth-secrets-manager.js +28 -0
- package/dist/src/config/msoauth.d.ts +19 -0
- package/dist/src/config/msoauth.js +6 -0
- package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
- package/dist/src/derive/autoMoved.js +49 -0
- package/dist/src/derive/contentSignature.d.ts +40 -0
- package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +23 -77
- package/dist/src/derive/contentUrl.d.ts +33 -0
- package/dist/src/derive/contentUrl.js +27 -0
- package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
- package/dist/src/derive/deriveMuted.js +7 -0
- package/dist/src/derive/enrichThreadRows.d.ts +118 -0
- package/dist/src/derive/enrichThreadRows.js +151 -0
- package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
- package/dist/src/derive/filterThreadCriteria.js +24 -0
- package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
- package/dist/src/derive/pendingMoveCounts.js +53 -0
- package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
- package/dist/src/derive/senderTrust.js +19 -0
- package/dist/src/error.d.ts +21 -0
- package/dist/src/error.js +85 -0
- package/dist/src/handlers/account-guards.d.ts +36 -0
- package/dist/src/handlers/account-guards.js +107 -0
- package/dist/src/handlers/account-oauth.d.ts +15 -0
- package/dist/src/handlers/account-oauth.js +334 -0
- package/dist/src/handlers/account-overrides.d.ts +53 -0
- package/dist/src/handlers/account-overrides.js +233 -0
- package/dist/src/handlers/account-signature.d.ts +28 -0
- package/dist/src/handlers/account-signature.js +85 -0
- package/dist/src/handlers/account.d.ts +7 -0
- package/dist/src/handlers/account.js +337 -0
- package/dist/src/handlers/address.d.ts +35 -0
- package/dist/src/handlers/address.js +133 -0
- package/dist/src/handlers/calendar-event.d.ts +70 -0
- package/dist/src/handlers/calendar-event.js +417 -0
- package/dist/src/handlers/calendar-feed.d.ts +47 -0
- package/dist/src/handlers/calendar-feed.js +184 -0
- package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
- package/dist/src/handlers/calendar-suggestion.js +204 -0
- package/dist/src/handlers/calendar.d.ts +116 -0
- package/dist/src/handlers/calendar.js +323 -0
- package/dist/src/handlers/config.d.ts +43 -0
- package/dist/src/handlers/config.js +164 -0
- package/dist/src/handlers/ensure-account-config.d.ts +9 -0
- package/dist/src/handlers/ensure-account-config.js +27 -0
- package/dist/src/handlers/filter.d.ts +3 -0
- package/dist/src/handlers/filter.js +88 -0
- package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
- package/dist/src/handlers/folder-role-appointments.js +168 -0
- package/dist/src/handlers/folder-role-labels.d.ts +26 -0
- package/dist/src/handlers/folder-role-labels.js +40 -0
- package/dist/src/handlers/folder-role.d.ts +9 -0
- package/dist/src/handlers/folder-role.js +37 -0
- package/dist/src/handlers/index.d.ts +88 -0
- package/dist/src/handlers/index.js +62 -0
- package/dist/src/handlers/label.d.ts +43 -0
- package/dist/src/handlers/label.js +107 -0
- package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
- package/dist/src/handlers/mailbox-bindings.js +61 -0
- package/dist/src/handlers/mailbox.d.ts +23 -0
- package/dist/src/handlers/mailbox.js +211 -0
- package/dist/src/handlers/me.d.ts +2 -0
- package/dist/src/handlers/me.js +117 -0
- package/dist/src/handlers/message.d.ts +82 -0
- package/dist/src/handlers/message.js +595 -0
- package/dist/src/handlers/organize.d.ts +10 -0
- package/dist/src/handlers/organize.js +103 -0
- package/dist/src/handlers/outbox-attachment.d.ts +25 -0
- package/dist/src/handlers/outbox-attachment.js +77 -0
- package/dist/src/handlers/outbox.d.ts +4 -0
- package/dist/src/handlers/outbox.js +136 -0
- package/dist/src/handlers/sync-progress.d.ts +46 -0
- package/dist/src/handlers/sync-progress.js +63 -0
- package/dist/src/handlers/sync.d.ts +28 -0
- package/dist/src/handlers/sync.js +73 -0
- package/dist/src/handlers/system-update.d.ts +2 -0
- package/dist/src/handlers/system-update.js +159 -0
- package/dist/src/handlers/thread.d.ts +148 -0
- package/dist/src/handlers/thread.js +228 -0
- package/dist/src/handlers/unified-threads.d.ts +247 -0
- package/dist/src/handlers/unified-threads.js +406 -0
- package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
- package/dist/src/handlers/vip-suggestions.js +16 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +53 -0
- package/{src/json.ts → dist/src/json.d.ts} +1 -4
- package/dist/src/json.js +10 -0
- package/dist/src/jwt-auth.d.ts +15 -0
- package/dist/src/jwt-auth.js +45 -0
- package/dist/src/lambda.d.ts +13 -0
- package/dist/src/lambda.js +33 -0
- package/dist/src/request.d.ts +9 -0
- package/dist/src/request.js +17 -0
- package/dist/src/response.d.ts +44 -0
- package/dist/src/response.js +80 -0
- package/dist/src/serve.d.ts +31 -0
- package/dist/src/serve.js +117 -0
- package/dist/src/service/account-deletion.d.ts +20 -0
- package/dist/src/service/account-deletion.js +33 -0
- package/dist/src/service/compose-sqlite.d.ts +3 -0
- package/dist/src/service/compose-sqlite.js +13 -0
- package/dist/src/service/create-remit-client.d.ts +109 -0
- package/dist/src/service/create-remit-client.js +188 -0
- package/dist/src/service/data-client.d.ts +12 -0
- package/dist/src/service/data-client.js +42 -0
- package/dist/src/service/export-identity.d.ts +4 -0
- package/dist/src/service/export-identity.js +33 -0
- package/dist/src/service/fire-and-forget.d.ts +54 -0
- package/dist/src/service/fire-and-forget.js +37 -0
- package/dist/src/service/organize.d.ts +181 -0
- package/dist/src/service/organize.js +367 -0
- package/dist/src/service/queues.d.ts +12 -0
- package/dist/src/service/queues.js +1 -0
- package/dist/src/service/refusal.d.ts +4 -0
- package/dist/src/service/refusal.js +20 -0
- package/dist/src/service/sender-category.d.ts +51 -0
- package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
- package/dist/src/service/shared-deps.d.ts +6 -0
- package/dist/src/service/shared-deps.js +24 -0
- package/dist/src/service/trigger-sync.d.ts +57 -0
- package/dist/src/service/trigger-sync.js +31 -0
- package/dist/src/types.d.ts +72 -0
- package/dist/src/types.js +1 -0
- package/package.json +125 -98
- package/.env.test +0 -4
- package/dev-server/content-auth.test.ts +0 -113
- package/dev-server/content-auth.ts +0 -54
- package/dev-server/content-handler.test.ts +0 -105
- package/dev-server/content-handler.ts +0 -79
- package/dev-server/content-path.test.ts +0 -37
- package/dev-server/content-path.ts +0 -27
- package/dev-server/cors.test.ts +0 -48
- package/dev-server/cors.ts +0 -25
- package/dev-server/keep-alive.test.ts +0 -24
- package/dev-server/lambda-helpers.ts +0 -69
- package/dev-server/log-output.test.ts +0 -173
- package/dev-server/oauth-callback-route.test.ts +0 -66
- package/dev-server/relational-health.test.ts +0 -83
- package/dev-server/relational-health.ts +0 -39
- package/dev-server/server.ts +0 -384
- package/dev-server/sync-age.test.ts +0 -91
- package/dev-server/sync-age.ts +0 -55
- package/dev-server/upload-handler.ts +0 -132
- package/dev-server/upload-route.test.ts +0 -196
- package/scripts/backfill-classification.ts +0 -91
- package/scripts/backfill-list-id.ts +0 -83
- package/scripts/config-save.ts +0 -128
- package/src/auth.test.ts +0 -12
- package/src/auth.ts +0 -92
- package/src/config/msoauth.test.ts +0 -48
- package/src/config/msoauth.ts +0 -48
- package/src/data-backend.test.ts +0 -23
- package/src/data-backend.ts +0 -14
- package/src/derive/autoMoved.test.ts +0 -104
- package/src/derive/contentSignature.test.ts +0 -161
- package/src/derive/contentUrl.test.ts +0 -172
- package/src/derive/contentUrl.ts +0 -86
- package/src/derive/deriveMuted.test.ts +0 -34
- package/src/derive/enrichThreadRows.test.ts +0 -360
- package/src/derive/enrichThreadRows.ts +0 -234
- package/src/derive/filterThreadCriteria.test.ts +0 -126
- package/src/derive/filterThreadCriteria.ts +0 -51
- package/src/derive/senderTrust.test.ts +0 -67
- package/src/error.test.ts +0 -201
- package/src/error.ts +0 -123
- package/src/handlers/account-guards.test.ts +0 -67
- package/src/handlers/account-guards.ts +0 -169
- package/src/handlers/account-oauth-callback.test.ts +0 -155
- package/src/handlers/account-oauth-consent.test.ts +0 -336
- package/src/handlers/account-oauth-start.test.ts +0 -191
- package/src/handlers/account-oauth.test.ts +0 -439
- package/src/handlers/account-oauth.ts +0 -635
- package/src/handlers/account-overrides.test.ts +0 -88
- package/src/handlers/account-overrides.ts +0 -323
- package/src/handlers/account-ownership.ts +0 -25
- package/src/handlers/account-signature.ts +0 -117
- package/src/handlers/account-synced-services.test.ts +0 -245
- package/src/handlers/account.ts +0 -552
- package/src/handlers/address.test.ts +0 -381
- package/src/handlers/address.ts +0 -200
- package/src/handlers/calendar-event.test.ts +0 -685
- package/src/handlers/calendar-event.ts +0 -738
- package/src/handlers/calendar-feed-validators.test.ts +0 -228
- package/src/handlers/calendar-feed.test.ts +0 -439
- package/src/handlers/calendar-feed.ts +0 -291
- package/src/handlers/calendar-sqlite-fixture.ts +0 -36
- package/src/handlers/calendar-suggestion.test.ts +0 -1269
- package/src/handlers/calendar-suggestion.ts +0 -425
- package/src/handlers/calendar.test.ts +0 -1080
- package/src/handlers/calendar.ts +0 -517
- package/src/handlers/config.test.ts +0 -245
- package/src/handlers/config.ts +0 -332
- package/src/handlers/ensure-account-config.ts +0 -30
- package/src/handlers/filter.test.ts +0 -644
- package/src/handlers/filter.ts +0 -518
- package/src/handlers/folder-role-appointments.test.ts +0 -438
- package/src/handlers/folder-role-appointments.ts +0 -249
- package/src/handlers/folder-role-labels.ts +0 -63
- package/src/handlers/folder-role.test.ts +0 -209
- package/src/handlers/folder-role.ts +0 -80
- package/src/handlers/index.ts +0 -83
- package/src/handlers/label.test.ts +0 -112
- package/src/handlers/label.ts +0 -210
- package/src/handlers/mailbox-bindings.ts +0 -111
- package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
- package/src/handlers/mailbox-settled-target.test.ts +0 -251
- package/src/handlers/mailbox.test.ts +0 -280
- package/src/handlers/mailbox.ts +0 -581
- package/src/handlers/me.test.ts +0 -81
- package/src/handlers/me.ts +0 -203
- package/src/handlers/message-read-backfill.test.ts +0 -103
- package/src/handlers/message.test.ts +0 -131
- package/src/handlers/message.ts +0 -1131
- package/src/handlers/organize.test.ts +0 -277
- package/src/handlers/organize.ts +0 -197
- package/src/handlers/outbox-attachment.test.ts +0 -765
- package/src/handlers/outbox-attachment.ts +0 -126
- package/src/handlers/outbox-list.test.ts +0 -179
- package/src/handlers/outbox.test.ts +0 -772
- package/src/handlers/outbox.ts +0 -258
- package/src/handlers/search.test.ts +0 -161
- package/src/handlers/search.ts +0 -105
- package/src/handlers/sync-progress.test.ts +0 -205
- package/src/handlers/sync-progress.ts +0 -99
- package/src/handlers/sync.test.ts +0 -168
- package/src/handlers/sync.ts +0 -118
- package/src/handlers/system-update.test.ts +0 -497
- package/src/handlers/system-update.ts +0 -230
- package/src/handlers/thread.test.ts +0 -396
- package/src/handlers/thread.ts +0 -427
- package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
- package/src/handlers/unified-threads.test.ts +0 -785
- package/src/handlers/unified-threads.ts +0 -669
- package/src/index.test.ts +0 -83
- package/src/index.ts +0 -228
- package/src/jwt-auth.test.ts +0 -152
- package/src/jwt-auth.ts +0 -132
- package/src/log-redaction.test.ts +0 -144
- package/src/openapi-spec.test.ts +0 -118
- package/src/request-context.test.ts +0 -61
- package/src/request-context.ts +0 -33
- package/src/request-pipeline.test.ts +0 -88
- package/src/request.ts +0 -10
- package/src/response.test.ts +0 -130
- package/src/response.ts +0 -144
- package/src/service/compose-sqlite.ts +0 -98
- package/src/service/config-import.ts +0 -39
- package/src/service/create-remit-client.ts +0 -522
- package/src/service/data-client.test.ts +0 -97
- package/src/service/data-client.ts +0 -58
- package/src/service/export-identity.ts +0 -39
- package/src/service/filter.ts +0 -40
- package/src/service/fire-and-forget.test.ts +0 -126
- package/src/service/fire-and-forget.ts +0 -79
- package/src/service/localhost-env-config.test.ts +0 -36
- package/src/service/organize-queue.ts +0 -98
- package/src/service/organize.test.ts +0 -1451
- package/src/service/organize.ts +0 -1025
- package/src/service/read-path-filter-config.test.ts +0 -243
- package/src/service/semantic-capability.test.ts +0 -78
- package/src/service/semantic-capability.ts +0 -95
- package/src/service/sender-category.test.ts +0 -235
- package/src/service/sqs.ts +0 -4
- package/src/service/trigger-sync.test.ts +0 -205
- package/src/service/trigger-sync.ts +0 -100
- package/src/stdout-capture-fixture.ts +0 -40
- package/src/types.ts +0 -271
- package/tsconfig.json +0 -7
|
@@ -1,1080 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { after, before, describe, it } from "node:test";
|
|
3
|
-
import type { CalendarFeedFetcher } from "@remit/calendar-service";
|
|
4
|
-
import type {
|
|
5
|
-
CalendarCollectionItem,
|
|
6
|
-
CalendarEventIndexItem,
|
|
7
|
-
CalendarObjectItem,
|
|
8
|
-
} from "@remit/data-ports";
|
|
9
|
-
import { CalendarSource, RecurrenceScope } from "@remit/domain-enums";
|
|
10
|
-
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
11
|
-
import type { Context } from "openapi-backend";
|
|
12
|
-
import { deriveAccountConfigId } from "../auth.js";
|
|
13
|
-
import {
|
|
14
|
-
_resetForTest,
|
|
15
|
-
type RemitClient,
|
|
16
|
-
setClient,
|
|
17
|
-
} from "../service/data-client.js";
|
|
18
|
-
import {
|
|
19
|
-
type CalendarDeps,
|
|
20
|
-
CalendarDetailOperations,
|
|
21
|
-
CalendarOperations,
|
|
22
|
-
calendarDepsOf,
|
|
23
|
-
createCalendarFor,
|
|
24
|
-
deleteCalendarFor,
|
|
25
|
-
listCalendarsFor,
|
|
26
|
-
readCollectionTimezone,
|
|
27
|
-
updateCalendarFor,
|
|
28
|
-
} from "./calendar.js";
|
|
29
|
-
import {
|
|
30
|
-
type CalendarEventDeps,
|
|
31
|
-
createCalendarEventFor,
|
|
32
|
-
deleteCalendarEventFor,
|
|
33
|
-
etagMatches,
|
|
34
|
-
pickEventUpdate,
|
|
35
|
-
readScope,
|
|
36
|
-
readWindow,
|
|
37
|
-
updateCalendarEventFor,
|
|
38
|
-
} from "./calendar-event.js";
|
|
39
|
-
import { createCalendarSqliteClient } from "./calendar-sqlite-fixture.js";
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The calendar handlers against the store the self-host build ships.
|
|
43
|
-
*
|
|
44
|
-
* This file used to run on a set of in-memory port implementations that claimed
|
|
45
|
-
* to behave exactly as sqlite. Nothing checked the claim, so it was worth
|
|
46
|
-
* nothing: `putCalendarObject` projects, expands and bumps the sequence, and a
|
|
47
|
-
* memory twin of that is a second implementation of the write path that can
|
|
48
|
-
* drift from the real one silently. Every test here now writes through the
|
|
49
|
-
* drizzle repositories, so the projection, the expansion and the transaction
|
|
50
|
-
* are the shipped ones.
|
|
51
|
-
*
|
|
52
|
-
* One database serves the file. Every calendar row is scoped by account config
|
|
53
|
-
* and `calendarId` is derived from it, so a test that mints its own account
|
|
54
|
-
* sees only what it wrote.
|
|
55
|
-
*/
|
|
56
|
-
|
|
57
|
-
let client: RemitClient;
|
|
58
|
-
let cleanup: () => void;
|
|
59
|
-
let mintedAccounts = 0;
|
|
60
|
-
|
|
61
|
-
/** One caller's calendars, and the two ways the handlers reach them. */
|
|
62
|
-
class CalendarAccount {
|
|
63
|
-
readonly accountConfigId: string;
|
|
64
|
-
|
|
65
|
-
constructor(readonly sub: string) {
|
|
66
|
-
this.accountConfigId = deriveAccountConfigId(sub);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** The request an authenticated caller of this account arrives on. */
|
|
70
|
-
request(): APIGatewayProxyEvent {
|
|
71
|
-
return {
|
|
72
|
-
requestContext: { authorizer: { claims: { sub: this.sub } } },
|
|
73
|
-
} as unknown as APIGatewayProxyEvent;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
deps(): CalendarDeps {
|
|
77
|
-
return calendarDepsOf(client);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** The same deps with id minting and the clock pinned. */
|
|
81
|
-
eventDeps(): CalendarEventDeps {
|
|
82
|
-
let minted = 0;
|
|
83
|
-
return {
|
|
84
|
-
...this.deps(),
|
|
85
|
-
newId: () => {
|
|
86
|
-
minted += 1;
|
|
87
|
-
return `${this.accountConfigId}-minted-${minted}`;
|
|
88
|
-
},
|
|
89
|
-
now: () => new Date("2026-08-29T00:00:00Z"),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
collections(): Promise<CalendarCollectionItem[]> {
|
|
94
|
-
return client.calendarCollection.listByAccountConfig(this.accountConfigId);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async collection(calendarId: string): Promise<CalendarCollectionItem | null> {
|
|
98
|
-
const held = await this.collections();
|
|
99
|
-
return (
|
|
100
|
-
held.find((collection) => collection.calendarId === calendarId) ?? null
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async objects(): Promise<CalendarObjectItem[]> {
|
|
105
|
-
const held = await this.collections();
|
|
106
|
-
const objects: CalendarObjectItem[] = [];
|
|
107
|
-
for (const collection of held) {
|
|
108
|
-
objects.push(
|
|
109
|
-
...(await client.calendarObject.listByCalendar(collection.calendarId)),
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
return objects;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
object(
|
|
116
|
-
calendarId: string,
|
|
117
|
-
calendarObjectId: string,
|
|
118
|
-
): Promise<CalendarObjectItem | null> {
|
|
119
|
-
return client.calendarObject.find(calendarId, calendarObjectId);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
occurrences(object: {
|
|
123
|
-
calendarId: string;
|
|
124
|
-
calendarObjectId: string;
|
|
125
|
-
}): Promise<CalendarEventIndexItem[]> {
|
|
126
|
-
return client.calendarEventIndex.listForObject(
|
|
127
|
-
object.calendarId,
|
|
128
|
-
object.calendarObjectId,
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const anAccount = (): CalendarAccount => {
|
|
134
|
-
mintedAccounts += 1;
|
|
135
|
-
return new CalendarAccount(`calendar-sub-${mintedAccounts}`);
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const seedWeekly = async (
|
|
139
|
-
deps: CalendarEventDeps,
|
|
140
|
-
accountConfigId: string,
|
|
141
|
-
calendarId: string,
|
|
142
|
-
recurrenceRule = "FREQ=WEEKLY;COUNT=5",
|
|
143
|
-
) => {
|
|
144
|
-
const created = await createCalendarEventFor(deps, accountConfigId, {
|
|
145
|
-
calendarId,
|
|
146
|
-
summary: "Stand-up",
|
|
147
|
-
start: "2026-09-07T09:00:00Z",
|
|
148
|
-
end: "2026-09-07T10:00:00Z",
|
|
149
|
-
recurrenceRule,
|
|
150
|
-
});
|
|
151
|
-
assert.ok(created.ok, JSON.stringify(created));
|
|
152
|
-
return created.value;
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
before(async () => {
|
|
156
|
-
_resetForTest();
|
|
157
|
-
({ client, cleanup } = await createCalendarSqliteClient());
|
|
158
|
-
setClient(client);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
after(() => {
|
|
162
|
-
_resetForTest();
|
|
163
|
-
cleanup();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe("listCalendarsFor", () => {
|
|
167
|
-
it("provisions the default calendar on a first read", async () => {
|
|
168
|
-
const account = anAccount();
|
|
169
|
-
|
|
170
|
-
const calendars = await listCalendarsFor(
|
|
171
|
-
account.deps(),
|
|
172
|
-
account.accountConfigId,
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
assert.equal(calendars.length, 1);
|
|
176
|
-
assert.equal(calendars[0]?.urlSegment, "default");
|
|
177
|
-
assert.equal(calendars[0]?.source, CalendarSource.Default);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("provisions it exactly once when several reads arrive together", async () => {
|
|
181
|
-
const account = anAccount();
|
|
182
|
-
|
|
183
|
-
const reads = await Promise.all(
|
|
184
|
-
Array.from({ length: 8 }, () =>
|
|
185
|
-
listCalendarsFor(account.deps(), account.accountConfigId),
|
|
186
|
-
),
|
|
187
|
-
);
|
|
188
|
-
|
|
189
|
-
assert.equal((await account.collections()).length, 1);
|
|
190
|
-
const ids = new Set(reads.flat().map((calendar) => calendar.calendarId));
|
|
191
|
-
assert.equal(ids.size, 1);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
describe("createCalendarFor", () => {
|
|
196
|
-
it("refuses a url segment the account already uses", async () => {
|
|
197
|
-
const account = anAccount();
|
|
198
|
-
const deps = account.deps();
|
|
199
|
-
await createCalendarFor(deps, account.accountConfigId, {
|
|
200
|
-
urlSegment: "work",
|
|
201
|
-
displayName: "Work",
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
const second = await createCalendarFor(deps, account.accountConfigId, {
|
|
205
|
-
urlSegment: "WORK",
|
|
206
|
-
displayName: "Work again",
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
assert.ok(!second.ok);
|
|
210
|
-
assert.equal(second.error.code, "UrlSegmentTaken");
|
|
211
|
-
const held = await account.collections();
|
|
212
|
-
assert.equal(held.length, 1);
|
|
213
|
-
assert.equal(
|
|
214
|
-
held[0]?.displayName,
|
|
215
|
-
"Work",
|
|
216
|
-
"the refused create never wrote over the calendar that holds the segment",
|
|
217
|
-
);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it("refuses an empty url segment", async () => {
|
|
221
|
-
const account = anAccount();
|
|
222
|
-
|
|
223
|
-
const created = await createCalendarFor(
|
|
224
|
-
account.deps(),
|
|
225
|
-
account.accountConfigId,
|
|
226
|
-
{
|
|
227
|
-
urlSegment: " ",
|
|
228
|
-
displayName: "Nameless",
|
|
229
|
-
},
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
assert.ok(!created.ok);
|
|
233
|
-
assert.equal(created.error.code, "InvalidUrlSegment");
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
describe("deleteCalendarFor", () => {
|
|
238
|
-
it("refuses to remove the calendar events fall back to", async () => {
|
|
239
|
-
const account = anAccount();
|
|
240
|
-
const deps = account.deps();
|
|
241
|
-
const [fallback] = await listCalendarsFor(deps, account.accountConfigId);
|
|
242
|
-
assert.ok(fallback);
|
|
243
|
-
|
|
244
|
-
const removed = await deleteCalendarFor(
|
|
245
|
-
deps,
|
|
246
|
-
account.accountConfigId,
|
|
247
|
-
fallback.calendarId,
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
assert.ok(!removed.ok);
|
|
251
|
-
assert.equal(removed.error.code, "DefaultCalendarUndeletable");
|
|
252
|
-
assert.equal((await account.collections()).length, 1);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("takes the events and their occurrences with a calendar it does remove", async () => {
|
|
256
|
-
const account = anAccount();
|
|
257
|
-
const deps = account.eventDeps();
|
|
258
|
-
const created = await createCalendarFor(deps, account.accountConfigId, {
|
|
259
|
-
urlSegment: "work",
|
|
260
|
-
displayName: "Work",
|
|
261
|
-
});
|
|
262
|
-
assert.ok(created.ok);
|
|
263
|
-
const event = await seedWeekly(
|
|
264
|
-
deps,
|
|
265
|
-
account.accountConfigId,
|
|
266
|
-
created.value.calendarId,
|
|
267
|
-
);
|
|
268
|
-
assert.equal((await account.objects()).length, 1);
|
|
269
|
-
|
|
270
|
-
const removed = await deleteCalendarFor(
|
|
271
|
-
deps,
|
|
272
|
-
account.accountConfigId,
|
|
273
|
-
created.value.calendarId,
|
|
274
|
-
);
|
|
275
|
-
|
|
276
|
-
assert.ok(removed.ok);
|
|
277
|
-
assert.deepEqual(await account.objects(), []);
|
|
278
|
-
assert.deepEqual(await account.occurrences(event), []);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it("answers not-found for a calendar on another account", async () => {
|
|
282
|
-
const account = anAccount();
|
|
283
|
-
const deps = account.deps();
|
|
284
|
-
const [mine] = await listCalendarsFor(deps, account.accountConfigId);
|
|
285
|
-
assert.ok(mine);
|
|
286
|
-
|
|
287
|
-
const removed = await deleteCalendarFor(
|
|
288
|
-
deps,
|
|
289
|
-
anAccount().accountConfigId,
|
|
290
|
-
mine.calendarId,
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
assert.ok(!removed.ok);
|
|
294
|
-
assert.equal(removed.error.code, "NotFound");
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
describe("readWindow", () => {
|
|
299
|
-
it("refuses a window that is missing, backwards or wider than a year", () => {
|
|
300
|
-
assert.equal(readWindow(undefined, "2026-09-08T00:00:00Z").ok, false);
|
|
301
|
-
assert.equal(readWindow("not a date", "2026-09-08T00:00:00Z").ok, false);
|
|
302
|
-
assert.equal(
|
|
303
|
-
readWindow("2026-09-08T00:00:00Z", "2026-09-07T00:00:00Z").ok,
|
|
304
|
-
false,
|
|
305
|
-
);
|
|
306
|
-
assert.equal(
|
|
307
|
-
readWindow("2026-01-01T00:00:00Z", "2027-06-01T00:00:00Z").ok,
|
|
308
|
-
false,
|
|
309
|
-
);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it("refuses an end that carries no zone offset", () => {
|
|
313
|
-
const wallTime = readWindow("2026-01-01T00:00:00", "2026-01-08T00:00:00Z");
|
|
314
|
-
assert.ok(!wallTime.ok);
|
|
315
|
-
assert.equal(wallTime.error.code, "InvalidWindow");
|
|
316
|
-
|
|
317
|
-
const dateOnly = readWindow("2026-01-01T00:00:00Z", "2026-01-08");
|
|
318
|
-
assert.ok(!dateOnly.ok);
|
|
319
|
-
assert.equal(dateOnly.error.code, "InvalidWindow");
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
it("accepts either spelling of an offset", () => {
|
|
323
|
-
assert.equal(
|
|
324
|
-
readWindow("2026-01-01T00:00:00Z", "2026-01-08T00:00:00Z").ok,
|
|
325
|
-
true,
|
|
326
|
-
);
|
|
327
|
-
assert.equal(
|
|
328
|
-
readWindow("2026-01-01T00:00:00+02:00", "2026-01-08T00:00:00+02:00").ok,
|
|
329
|
-
true,
|
|
330
|
-
);
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
it("normalises both ends to UTC instants", () => {
|
|
334
|
-
const window = readWindow(
|
|
335
|
-
"2026-09-07T02:00:00+02:00",
|
|
336
|
-
"2026-09-08T02:00:00+02:00",
|
|
337
|
-
);
|
|
338
|
-
|
|
339
|
-
assert.ok(window.ok);
|
|
340
|
-
assert.deepEqual(window.value, {
|
|
341
|
-
from: "2026-09-07T00:00:00Z",
|
|
342
|
-
to: "2026-09-08T00:00:00Z",
|
|
343
|
-
});
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
describe("etagMatches", () => {
|
|
348
|
-
it("lets a write through with no precondition, a wildcard or the tag it read", () => {
|
|
349
|
-
assert.equal(etagMatches(undefined, "abc"), true);
|
|
350
|
-
assert.equal(etagMatches("*", "abc"), true);
|
|
351
|
-
assert.equal(etagMatches('"abc"', "abc"), true);
|
|
352
|
-
assert.equal(etagMatches('W/"abc"', "abc"), true);
|
|
353
|
-
assert.equal(etagMatches('"other", "abc"', "abc"), true);
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it("refuses a tag the resource no longer carries", () => {
|
|
357
|
-
assert.equal(etagMatches('"stale"', "abc"), false);
|
|
358
|
-
});
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
describe("pickEventUpdate", () => {
|
|
362
|
-
it("keeps absence, so a rename touches nothing else", () => {
|
|
363
|
-
assert.deepEqual(pickEventUpdate({ summary: "Renamed" }), {
|
|
364
|
-
summary: "Renamed",
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
it("drops a field the API does not define", () => {
|
|
369
|
-
assert.deepEqual(
|
|
370
|
-
pickEventUpdate({ summary: "Renamed", icalData: "smuggled" } as never),
|
|
371
|
-
{ summary: "Renamed" },
|
|
372
|
-
);
|
|
373
|
-
});
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
describe("updateCalendarEventFor", () => {
|
|
377
|
-
it("refuses a write built on an etag the resource no longer carries", async () => {
|
|
378
|
-
const account = anAccount();
|
|
379
|
-
const deps = account.eventDeps();
|
|
380
|
-
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
381
|
-
assert.ok(calendar);
|
|
382
|
-
const event = await seedWeekly(
|
|
383
|
-
deps,
|
|
384
|
-
account.accountConfigId,
|
|
385
|
-
calendar.calendarId,
|
|
386
|
-
);
|
|
387
|
-
|
|
388
|
-
const first = await updateCalendarEventFor(
|
|
389
|
-
deps,
|
|
390
|
-
account.accountConfigId,
|
|
391
|
-
{
|
|
392
|
-
calendarId: calendar.calendarId,
|
|
393
|
-
calendarObjectId: event.calendarObjectId,
|
|
394
|
-
scope: RecurrenceScope.All,
|
|
395
|
-
recurrenceId: "",
|
|
396
|
-
ifMatch: `"${event.etag}"`,
|
|
397
|
-
},
|
|
398
|
-
{ summary: "Stand-up (renamed)" },
|
|
399
|
-
);
|
|
400
|
-
assert.ok(first.ok);
|
|
401
|
-
|
|
402
|
-
const stale = await updateCalendarEventFor(
|
|
403
|
-
deps,
|
|
404
|
-
account.accountConfigId,
|
|
405
|
-
{
|
|
406
|
-
calendarId: calendar.calendarId,
|
|
407
|
-
calendarObjectId: event.calendarObjectId,
|
|
408
|
-
scope: RecurrenceScope.All,
|
|
409
|
-
recurrenceId: "",
|
|
410
|
-
ifMatch: `"${event.etag}"`,
|
|
411
|
-
},
|
|
412
|
-
{ summary: "Stand-up (renamed again)" },
|
|
413
|
-
);
|
|
414
|
-
|
|
415
|
-
assert.ok(!stale.ok);
|
|
416
|
-
assert.equal(stale.error.code, "EtagMismatch");
|
|
417
|
-
const survivor = await account.object(
|
|
418
|
-
calendar.calendarId,
|
|
419
|
-
event.calendarObjectId,
|
|
420
|
-
);
|
|
421
|
-
assert.equal(
|
|
422
|
-
survivor?.summary,
|
|
423
|
-
"Stand-up (renamed)",
|
|
424
|
-
"the losing write left the resource alone",
|
|
425
|
-
);
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
it("writes both resources of a Following split", async () => {
|
|
429
|
-
const account = anAccount();
|
|
430
|
-
const deps = account.eventDeps();
|
|
431
|
-
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
432
|
-
assert.ok(calendar);
|
|
433
|
-
const event = await seedWeekly(
|
|
434
|
-
deps,
|
|
435
|
-
account.accountConfigId,
|
|
436
|
-
calendar.calendarId,
|
|
437
|
-
);
|
|
438
|
-
|
|
439
|
-
const split = await updateCalendarEventFor(
|
|
440
|
-
deps,
|
|
441
|
-
account.accountConfigId,
|
|
442
|
-
{
|
|
443
|
-
calendarId: calendar.calendarId,
|
|
444
|
-
calendarObjectId: event.calendarObjectId,
|
|
445
|
-
scope: RecurrenceScope.Following,
|
|
446
|
-
recurrenceId: "2026-09-21T09:00:00Z",
|
|
447
|
-
ifMatch: undefined,
|
|
448
|
-
},
|
|
449
|
-
{ summary: "Stand-up (new format)" },
|
|
450
|
-
);
|
|
451
|
-
|
|
452
|
-
assert.ok(split.ok, JSON.stringify(split));
|
|
453
|
-
const objects = (await account.objects()).sort((left, right) =>
|
|
454
|
-
left.dtStart.localeCompare(right.dtStart),
|
|
455
|
-
);
|
|
456
|
-
assert.equal(objects.length, 2);
|
|
457
|
-
const [head, tail] = objects;
|
|
458
|
-
assert.equal(head?.summary, "Stand-up");
|
|
459
|
-
assert.equal(tail?.summary, "Stand-up (new format)");
|
|
460
|
-
assert.notEqual(head?.icalUid, tail?.icalUid);
|
|
461
|
-
assert.equal(
|
|
462
|
-
split.value?.calendarObjectId,
|
|
463
|
-
tail?.calendarObjectId,
|
|
464
|
-
"the caller is handed the remainder, which is where their edit landed",
|
|
465
|
-
);
|
|
466
|
-
assert.notEqual(split.value?.calendarObjectId, event.calendarObjectId);
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
it("answers not-found for an event the calendar does not hold", async () => {
|
|
470
|
-
const account = anAccount();
|
|
471
|
-
const deps = account.eventDeps();
|
|
472
|
-
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
473
|
-
assert.ok(calendar);
|
|
474
|
-
|
|
475
|
-
const updated = await updateCalendarEventFor(
|
|
476
|
-
deps,
|
|
477
|
-
account.accountConfigId,
|
|
478
|
-
{
|
|
479
|
-
calendarId: calendar.calendarId,
|
|
480
|
-
calendarObjectId: "absent",
|
|
481
|
-
scope: RecurrenceScope.All,
|
|
482
|
-
recurrenceId: "",
|
|
483
|
-
ifMatch: undefined,
|
|
484
|
-
},
|
|
485
|
-
{ summary: "Renamed" },
|
|
486
|
-
);
|
|
487
|
-
|
|
488
|
-
assert.ok(!updated.ok);
|
|
489
|
-
assert.equal(updated.error.code, "NotFound");
|
|
490
|
-
});
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
describe("deleteCalendarEventFor", () => {
|
|
494
|
-
it("removes the resource and its occurrences under scope=All", async () => {
|
|
495
|
-
const account = anAccount();
|
|
496
|
-
const deps = account.eventDeps();
|
|
497
|
-
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
498
|
-
assert.ok(calendar);
|
|
499
|
-
const event = await seedWeekly(
|
|
500
|
-
deps,
|
|
501
|
-
account.accountConfigId,
|
|
502
|
-
calendar.calendarId,
|
|
503
|
-
);
|
|
504
|
-
|
|
505
|
-
const removed = await deleteCalendarEventFor(
|
|
506
|
-
deps,
|
|
507
|
-
account.accountConfigId,
|
|
508
|
-
{
|
|
509
|
-
calendarId: calendar.calendarId,
|
|
510
|
-
calendarObjectId: event.calendarObjectId,
|
|
511
|
-
scope: RecurrenceScope.All,
|
|
512
|
-
recurrenceId: "",
|
|
513
|
-
ifMatch: undefined,
|
|
514
|
-
},
|
|
515
|
-
);
|
|
516
|
-
|
|
517
|
-
assert.ok(removed.ok);
|
|
518
|
-
assert.deepEqual(await account.objects(), []);
|
|
519
|
-
assert.deepEqual(await account.occurrences(event), []);
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
it("keeps the series under scope=This and drops one occurrence from it", async () => {
|
|
523
|
-
const account = anAccount();
|
|
524
|
-
const deps = account.eventDeps();
|
|
525
|
-
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
526
|
-
assert.ok(calendar);
|
|
527
|
-
const event = await seedWeekly(
|
|
528
|
-
deps,
|
|
529
|
-
account.accountConfigId,
|
|
530
|
-
calendar.calendarId,
|
|
531
|
-
);
|
|
532
|
-
assert.equal((await account.occurrences(event)).length, 5);
|
|
533
|
-
|
|
534
|
-
const removed = await deleteCalendarEventFor(
|
|
535
|
-
deps,
|
|
536
|
-
account.accountConfigId,
|
|
537
|
-
{
|
|
538
|
-
calendarId: calendar.calendarId,
|
|
539
|
-
calendarObjectId: event.calendarObjectId,
|
|
540
|
-
scope: RecurrenceScope.This,
|
|
541
|
-
recurrenceId: "2026-09-21T09:00:00Z",
|
|
542
|
-
ifMatch: undefined,
|
|
543
|
-
},
|
|
544
|
-
);
|
|
545
|
-
|
|
546
|
-
assert.ok(removed.ok);
|
|
547
|
-
assert.equal((await account.objects()).length, 1);
|
|
548
|
-
const rows = await account.occurrences(event);
|
|
549
|
-
assert.equal(rows.length, 4);
|
|
550
|
-
assert.equal(
|
|
551
|
-
rows.some((row) => row.startAt === "2026-09-21T09:00:00Z"),
|
|
552
|
-
false,
|
|
553
|
-
);
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
it("refuses a per-occurrence delete of an event that happens once", async () => {
|
|
557
|
-
const account = anAccount();
|
|
558
|
-
const deps = account.eventDeps();
|
|
559
|
-
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
560
|
-
assert.ok(calendar);
|
|
561
|
-
const event = await seedWeekly(
|
|
562
|
-
deps,
|
|
563
|
-
account.accountConfigId,
|
|
564
|
-
calendar.calendarId,
|
|
565
|
-
"",
|
|
566
|
-
);
|
|
567
|
-
|
|
568
|
-
const removed = await deleteCalendarEventFor(
|
|
569
|
-
deps,
|
|
570
|
-
account.accountConfigId,
|
|
571
|
-
{
|
|
572
|
-
calendarId: calendar.calendarId,
|
|
573
|
-
calendarObjectId: event.calendarObjectId,
|
|
574
|
-
scope: RecurrenceScope.This,
|
|
575
|
-
recurrenceId: "2026-09-07T09:00:00Z",
|
|
576
|
-
ifMatch: undefined,
|
|
577
|
-
},
|
|
578
|
-
);
|
|
579
|
-
|
|
580
|
-
assert.ok(!removed.ok);
|
|
581
|
-
assert.equal(removed.error.code, "NotRecurring");
|
|
582
|
-
assert.equal((await account.objects()).length, 1);
|
|
583
|
-
});
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
describe("createCalendarEventFor", () => {
|
|
587
|
-
it("refuses an event aimed at a calendar the account does not hold", async () => {
|
|
588
|
-
const account = anAccount();
|
|
589
|
-
const deps = account.eventDeps();
|
|
590
|
-
|
|
591
|
-
const created = await createCalendarEventFor(
|
|
592
|
-
deps,
|
|
593
|
-
account.accountConfigId,
|
|
594
|
-
{
|
|
595
|
-
calendarId: "someone-elses-calendar",
|
|
596
|
-
summary: "Stand-up",
|
|
597
|
-
start: "2026-09-07T09:00:00Z",
|
|
598
|
-
end: "2026-09-07T10:00:00Z",
|
|
599
|
-
},
|
|
600
|
-
);
|
|
601
|
-
|
|
602
|
-
assert.ok(!created.ok);
|
|
603
|
-
assert.equal(created.error.code, "NotFound");
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
it("refuses an event that ends before it starts", async () => {
|
|
607
|
-
const account = anAccount();
|
|
608
|
-
const deps = account.eventDeps();
|
|
609
|
-
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
610
|
-
assert.ok(calendar);
|
|
611
|
-
|
|
612
|
-
const created = await createCalendarEventFor(
|
|
613
|
-
deps,
|
|
614
|
-
account.accountConfigId,
|
|
615
|
-
{
|
|
616
|
-
calendarId: calendar.calendarId,
|
|
617
|
-
summary: "Backwards",
|
|
618
|
-
start: "2026-09-07T10:00:00Z",
|
|
619
|
-
end: "2026-09-07T09:00:00Z",
|
|
620
|
-
},
|
|
621
|
-
);
|
|
622
|
-
|
|
623
|
-
assert.ok(!created.ok);
|
|
624
|
-
assert.equal(created.error.code, "BackwardsEnd");
|
|
625
|
-
assert.deepEqual(await account.objects(), []);
|
|
626
|
-
});
|
|
627
|
-
});
|
|
628
|
-
|
|
629
|
-
describe("readScope", () => {
|
|
630
|
-
it("reads an absent scope as the whole series", () => {
|
|
631
|
-
const scope = readScope(undefined);
|
|
632
|
-
assert.ok(scope.ok);
|
|
633
|
-
assert.equal(scope.value, RecurrenceScope.All);
|
|
634
|
-
});
|
|
635
|
-
|
|
636
|
-
it("refuses a scope it does not recognise rather than widening it", () => {
|
|
637
|
-
const scope = readScope("Everything");
|
|
638
|
-
assert.ok(!scope.ok);
|
|
639
|
-
assert.equal(scope.error.code, "InvalidScope");
|
|
640
|
-
});
|
|
641
|
-
});
|
|
642
|
-
|
|
643
|
-
describe("readCollectionTimezone", () => {
|
|
644
|
-
it("accepts an IANA name and an absent one", () => {
|
|
645
|
-
assert.deepEqual(readCollectionTimezone("Europe/Amsterdam"), {
|
|
646
|
-
ok: true,
|
|
647
|
-
value: "Europe/Amsterdam",
|
|
648
|
-
});
|
|
649
|
-
assert.deepEqual(readCollectionTimezone(undefined), {
|
|
650
|
-
ok: true,
|
|
651
|
-
value: "",
|
|
652
|
-
});
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
it("refuses a zone this server cannot resolve", () => {
|
|
656
|
-
const timezone = readCollectionTimezone("Pacific Standard Time");
|
|
657
|
-
assert.ok(!timezone.ok);
|
|
658
|
-
assert.equal(timezone.error.code, "UnknownTimeZone");
|
|
659
|
-
});
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
describe("updateCalendarFor", () => {
|
|
663
|
-
const allDay = {
|
|
664
|
-
summary: "Leave",
|
|
665
|
-
start: "2026-06-01",
|
|
666
|
-
end: "2026-06-02",
|
|
667
|
-
allDay: true,
|
|
668
|
-
};
|
|
669
|
-
|
|
670
|
-
it("refuses a timezone this server cannot resolve", async () => {
|
|
671
|
-
const account = anAccount();
|
|
672
|
-
const deps = account.deps();
|
|
673
|
-
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
674
|
-
assert.ok(calendar);
|
|
675
|
-
|
|
676
|
-
const updated = await updateCalendarFor(
|
|
677
|
-
deps,
|
|
678
|
-
account.accountConfigId,
|
|
679
|
-
calendar.calendarId,
|
|
680
|
-
{ timezone: "Pacific Standard Time" },
|
|
681
|
-
);
|
|
682
|
-
|
|
683
|
-
assert.ok(!updated.ok);
|
|
684
|
-
assert.equal(updated.error.code, "UnknownTimeZone");
|
|
685
|
-
assert.equal((await account.collection(calendar.calendarId))?.timezone, "");
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
it("re-expands the calendar's events when its timezone changes", async () => {
|
|
689
|
-
const account = anAccount();
|
|
690
|
-
const deps = account.eventDeps();
|
|
691
|
-
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
692
|
-
assert.ok(calendar);
|
|
693
|
-
const created = await createCalendarEventFor(
|
|
694
|
-
deps,
|
|
695
|
-
account.accountConfigId,
|
|
696
|
-
{ calendarId: calendar.calendarId, ...allDay },
|
|
697
|
-
);
|
|
698
|
-
assert.ok(created.ok, JSON.stringify(created));
|
|
699
|
-
assert.equal(
|
|
700
|
-
(await account.occurrences(created.value))[0]?.startAt,
|
|
701
|
-
"2026-06-01T00:00:00Z",
|
|
702
|
-
"an all-day event in a calendar with no zone starts at midnight UTC",
|
|
703
|
-
);
|
|
704
|
-
|
|
705
|
-
const updated = await updateCalendarFor(
|
|
706
|
-
deps,
|
|
707
|
-
account.accountConfigId,
|
|
708
|
-
calendar.calendarId,
|
|
709
|
-
{ timezone: "America/New_York" },
|
|
710
|
-
);
|
|
711
|
-
|
|
712
|
-
assert.ok(updated.ok, JSON.stringify(updated));
|
|
713
|
-
assert.equal(
|
|
714
|
-
(await account.occurrences(created.value))[0]?.startAt,
|
|
715
|
-
"2026-06-01T04:00:00Z",
|
|
716
|
-
"and midnight in the calendar's new zone once it has one",
|
|
717
|
-
);
|
|
718
|
-
assert.ok(
|
|
719
|
-
updated.value.syncSequence > calendar.syncSequence,
|
|
720
|
-
"a syncing client is told the calendar changed",
|
|
721
|
-
);
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
it("leaves the events alone when only the name changes", async () => {
|
|
725
|
-
const account = anAccount();
|
|
726
|
-
const deps = account.eventDeps();
|
|
727
|
-
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
728
|
-
assert.ok(calendar);
|
|
729
|
-
const created = await createCalendarEventFor(
|
|
730
|
-
deps,
|
|
731
|
-
account.accountConfigId,
|
|
732
|
-
{ calendarId: calendar.calendarId, ...allDay },
|
|
733
|
-
);
|
|
734
|
-
assert.ok(created.ok);
|
|
735
|
-
const before = (await account.collection(calendar.calendarId))
|
|
736
|
-
?.syncSequence;
|
|
737
|
-
|
|
738
|
-
const updated = await updateCalendarFor(
|
|
739
|
-
deps,
|
|
740
|
-
account.accountConfigId,
|
|
741
|
-
calendar.calendarId,
|
|
742
|
-
{ displayName: "Renamed" },
|
|
743
|
-
);
|
|
744
|
-
|
|
745
|
-
assert.ok(updated.ok);
|
|
746
|
-
assert.equal(updated.value.displayName, "Renamed");
|
|
747
|
-
assert.equal(updated.value.syncSequence, before);
|
|
748
|
-
});
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* The collection wrappers, driven the way an HTTP request drives them. The
|
|
753
|
-
* suites above hold the inner functions; these hold what the API answers with.
|
|
754
|
-
*/
|
|
755
|
-
|
|
756
|
-
type Handler = (
|
|
757
|
-
context: Context,
|
|
758
|
-
event: APIGatewayProxyEvent,
|
|
759
|
-
) => Promise<Record<string, unknown>>;
|
|
760
|
-
|
|
761
|
-
const createCalendar =
|
|
762
|
-
CalendarOperations.CalendarOperations_createCalendar as Handler;
|
|
763
|
-
const getCalendar =
|
|
764
|
-
CalendarDetailOperations.CalendarDetailOperations_getCalendar as Handler;
|
|
765
|
-
const updateCalendar =
|
|
766
|
-
CalendarDetailOperations.CalendarDetailOperations_updateCalendar as Handler;
|
|
767
|
-
const deleteCalendar =
|
|
768
|
-
CalendarDetailOperations.CalendarDetailOperations_deleteCalendar as Handler;
|
|
769
|
-
|
|
770
|
-
const contextOf = (request: {
|
|
771
|
-
params?: Record<string, string>;
|
|
772
|
-
requestBody?: unknown;
|
|
773
|
-
}): Context => ({ request }) as unknown as Context;
|
|
774
|
-
|
|
775
|
-
describe("the calendar collection wrappers", () => {
|
|
776
|
-
it("answers not-found for a collection on another account", async () => {
|
|
777
|
-
const stranger = anAccount();
|
|
778
|
-
const [theirs] = await listCalendarsFor(
|
|
779
|
-
stranger.deps(),
|
|
780
|
-
stranger.accountConfigId,
|
|
781
|
-
);
|
|
782
|
-
assert.ok(theirs);
|
|
783
|
-
const event = anAccount().request();
|
|
784
|
-
|
|
785
|
-
const read = await getCalendar(
|
|
786
|
-
contextOf({ params: { calendarId: theirs.calendarId } }),
|
|
787
|
-
event,
|
|
788
|
-
);
|
|
789
|
-
|
|
790
|
-
assert.equal(read.statusCode, 404);
|
|
791
|
-
assert.equal((read.body as { code: string }).code, "not_found");
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
it("refuses a second calendar under a segment the account already uses", async () => {
|
|
795
|
-
const event = anAccount().request();
|
|
796
|
-
await createCalendar(
|
|
797
|
-
contextOf({ requestBody: { urlSegment: "work", displayName: "Work" } }),
|
|
798
|
-
event,
|
|
799
|
-
);
|
|
800
|
-
|
|
801
|
-
const second = await createCalendar(
|
|
802
|
-
contextOf({
|
|
803
|
-
requestBody: { urlSegment: "Work", displayName: "Work again" },
|
|
804
|
-
}),
|
|
805
|
-
event,
|
|
806
|
-
);
|
|
807
|
-
|
|
808
|
-
assert.equal(second.statusCode, 400);
|
|
809
|
-
assert.equal((second.body as { code: string }).code, "url_segment_taken");
|
|
810
|
-
});
|
|
811
|
-
|
|
812
|
-
it("refuses a rename that names a zone this server cannot resolve", async () => {
|
|
813
|
-
const account = anAccount();
|
|
814
|
-
const event = account.request();
|
|
815
|
-
const [calendar] = await listCalendarsFor(
|
|
816
|
-
account.deps(),
|
|
817
|
-
account.accountConfigId,
|
|
818
|
-
);
|
|
819
|
-
assert.ok(calendar);
|
|
820
|
-
|
|
821
|
-
const updated = await updateCalendar(
|
|
822
|
-
contextOf({
|
|
823
|
-
params: { calendarId: calendar.calendarId },
|
|
824
|
-
requestBody: { timezone: "Pacific Standard Time" },
|
|
825
|
-
}),
|
|
826
|
-
event,
|
|
827
|
-
);
|
|
828
|
-
|
|
829
|
-
assert.equal(updated.statusCode, 400);
|
|
830
|
-
assert.equal((updated.body as { code: string }).code, "unknown_time_zone");
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
it("answers 204 for a calendar it removed and 400 for the default one", async () => {
|
|
834
|
-
const account = anAccount();
|
|
835
|
-
const event = account.request();
|
|
836
|
-
const held = await listCalendarsFor(
|
|
837
|
-
account.deps(),
|
|
838
|
-
account.accountConfigId,
|
|
839
|
-
);
|
|
840
|
-
const fallback = held.find(
|
|
841
|
-
(collection) => collection.source === CalendarSource.Default,
|
|
842
|
-
);
|
|
843
|
-
assert.ok(fallback);
|
|
844
|
-
const created = (await createCalendar(
|
|
845
|
-
contextOf({ requestBody: { urlSegment: "work", displayName: "Work" } }),
|
|
846
|
-
event,
|
|
847
|
-
)) as unknown as { calendarId: string };
|
|
848
|
-
|
|
849
|
-
const removed = await deleteCalendar(
|
|
850
|
-
contextOf({ params: { calendarId: created.calendarId } }),
|
|
851
|
-
event,
|
|
852
|
-
);
|
|
853
|
-
const refused = await deleteCalendar(
|
|
854
|
-
contextOf({ params: { calendarId: fallback.calendarId } }),
|
|
855
|
-
event,
|
|
856
|
-
);
|
|
857
|
-
|
|
858
|
-
assert.equal(removed.statusCode, 204);
|
|
859
|
-
assert.equal(refused.statusCode, 400);
|
|
860
|
-
assert.equal(
|
|
861
|
-
(refused.body as { code: string }).code,
|
|
862
|
-
"default_calendar_undeletable",
|
|
863
|
-
);
|
|
864
|
-
});
|
|
865
|
-
});
|
|
866
|
-
|
|
867
|
-
const FEED_URL =
|
|
868
|
-
"https://calendar.example/calendar/ical/rota%40example.com/private-0ddba11/basic.ics";
|
|
869
|
-
|
|
870
|
-
const feedOf = (...events: string[][]): string =>
|
|
871
|
-
`${[
|
|
872
|
-
"BEGIN:VCALENDAR",
|
|
873
|
-
"VERSION:2.0",
|
|
874
|
-
"PRODID:-//Example//Published Calendar//EN",
|
|
875
|
-
"METHOD:PUBLISH",
|
|
876
|
-
...events.flat(),
|
|
877
|
-
"END:VCALENDAR",
|
|
878
|
-
].join("\r\n")}\r\n`;
|
|
879
|
-
|
|
880
|
-
const FERRY = [
|
|
881
|
-
"BEGIN:VEVENT",
|
|
882
|
-
"UID:ferry@calendar.example",
|
|
883
|
-
"DTSTAMP:20260901T000000Z",
|
|
884
|
-
"DTSTART:20260908T070000Z",
|
|
885
|
-
"DTEND:20260908T080000Z",
|
|
886
|
-
"RRULE:FREQ=DAILY;COUNT=3",
|
|
887
|
-
"SUMMARY:Ferry crossing",
|
|
888
|
-
"END:VEVENT",
|
|
889
|
-
];
|
|
890
|
-
|
|
891
|
-
const LOCKS = [
|
|
892
|
-
"BEGIN:VEVENT",
|
|
893
|
-
"UID:locks@calendar.example",
|
|
894
|
-
"DTSTAMP:20260901T000000Z",
|
|
895
|
-
"DTSTART;VALUE=DATE:20260910",
|
|
896
|
-
"DTEND;VALUE=DATE:20260911",
|
|
897
|
-
"SUMMARY:Locks closed",
|
|
898
|
-
"END:VEVENT",
|
|
899
|
-
];
|
|
900
|
-
|
|
901
|
-
const serving =
|
|
902
|
-
(body: string, status = 200): CalendarFeedFetcher =>
|
|
903
|
-
async () =>
|
|
904
|
-
new Response(body, { status });
|
|
905
|
-
|
|
906
|
-
const subscribe = async (
|
|
907
|
-
account: CalendarAccount,
|
|
908
|
-
fetcher: CalendarFeedFetcher = serving(feedOf(FERRY, LOCKS)),
|
|
909
|
-
) =>
|
|
910
|
-
createCalendarFor(
|
|
911
|
-
account.eventDeps(),
|
|
912
|
-
account.accountConfigId,
|
|
913
|
-
{
|
|
914
|
-
urlSegment: "harbour",
|
|
915
|
-
displayName: "Harbour",
|
|
916
|
-
subscriptionUrl: FEED_URL.replace("https:", "webcal:"),
|
|
917
|
-
},
|
|
918
|
-
fetcher,
|
|
919
|
-
1_000,
|
|
920
|
-
);
|
|
921
|
-
|
|
922
|
-
describe("a subscribed calendar", () => {
|
|
923
|
-
it("is created with the feed's events already in it", async () => {
|
|
924
|
-
const account = anAccount();
|
|
925
|
-
|
|
926
|
-
const created = await subscribe(account);
|
|
927
|
-
|
|
928
|
-
assert.ok(created.ok, JSON.stringify(created));
|
|
929
|
-
assert.equal(created.value.source, CalendarSource.Subscribed);
|
|
930
|
-
assert.equal(created.value.subscriptionUrl, FEED_URL);
|
|
931
|
-
assert.equal(created.value.subscriptionEnabled, true);
|
|
932
|
-
assert.equal(created.value.subscriptionFetchedAt, 1_000);
|
|
933
|
-
assert.equal(created.value.subscriptionError, "");
|
|
934
|
-
const objects = await account.objects();
|
|
935
|
-
assert.deepEqual(objects.map((object) => object.summary).sort(), [
|
|
936
|
-
"Ferry crossing",
|
|
937
|
-
"Locks closed",
|
|
938
|
-
]);
|
|
939
|
-
const ferry = objects.find((object) => object.summary === "Ferry crossing");
|
|
940
|
-
assert.ok(ferry);
|
|
941
|
-
assert.equal((await account.occurrences(ferry)).length, 3);
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
it("is not created when the feed cannot be read, and says why without the address", async () => {
|
|
945
|
-
const account = anAccount();
|
|
946
|
-
|
|
947
|
-
const created = await subscribe(account, serving("nope", 401));
|
|
948
|
-
|
|
949
|
-
assert.ok(!created.ok);
|
|
950
|
-
assert.equal(created.error.code, "SubscriptionUnreachable");
|
|
951
|
-
assert.equal(
|
|
952
|
-
created.error.message,
|
|
953
|
-
"the calendar was not created: the feed answered HTTP 401",
|
|
954
|
-
);
|
|
955
|
-
assert.equal(created.error.message.includes("private-0ddba11"), false);
|
|
956
|
-
assert.deepEqual(await account.collections(), []);
|
|
957
|
-
});
|
|
958
|
-
|
|
959
|
-
it("refuses an address that is not a web address", async () => {
|
|
960
|
-
const account = anAccount();
|
|
961
|
-
|
|
962
|
-
const created = await createCalendarFor(
|
|
963
|
-
account.deps(),
|
|
964
|
-
account.accountConfigId,
|
|
965
|
-
{
|
|
966
|
-
urlSegment: "harbour",
|
|
967
|
-
displayName: "Harbour",
|
|
968
|
-
subscriptionUrl: "file:///etc/passwd",
|
|
969
|
-
},
|
|
970
|
-
serving(feedOf(FERRY)),
|
|
971
|
-
);
|
|
972
|
-
|
|
973
|
-
assert.ok(!created.ok);
|
|
974
|
-
assert.equal(created.error.code, "InvalidSubscriptionUrl");
|
|
975
|
-
});
|
|
976
|
-
|
|
977
|
-
it("refuses a new event, an edit and a delete", async () => {
|
|
978
|
-
const account = anAccount();
|
|
979
|
-
const created = await subscribe(account);
|
|
980
|
-
assert.ok(created.ok);
|
|
981
|
-
const deps = account.eventDeps();
|
|
982
|
-
const [ferry] = (await account.objects()).filter(
|
|
983
|
-
(object) => object.summary === "Ferry crossing",
|
|
984
|
-
);
|
|
985
|
-
assert.ok(ferry);
|
|
986
|
-
const request = {
|
|
987
|
-
calendarId: created.value.calendarId,
|
|
988
|
-
calendarObjectId: ferry.calendarObjectId,
|
|
989
|
-
scope: RecurrenceScope.All,
|
|
990
|
-
recurrenceId: "",
|
|
991
|
-
ifMatch: undefined,
|
|
992
|
-
};
|
|
993
|
-
|
|
994
|
-
const added = await createCalendarEventFor(deps, account.accountConfigId, {
|
|
995
|
-
calendarId: created.value.calendarId,
|
|
996
|
-
summary: "Smuggled in",
|
|
997
|
-
start: "2026-09-09T09:00:00Z",
|
|
998
|
-
end: "2026-09-09T10:00:00Z",
|
|
999
|
-
});
|
|
1000
|
-
const edited = await updateCalendarEventFor(
|
|
1001
|
-
deps,
|
|
1002
|
-
account.accountConfigId,
|
|
1003
|
-
request,
|
|
1004
|
-
{ summary: "Renamed" },
|
|
1005
|
-
);
|
|
1006
|
-
const removed = await deleteCalendarEventFor(
|
|
1007
|
-
deps,
|
|
1008
|
-
account.accountConfigId,
|
|
1009
|
-
request,
|
|
1010
|
-
);
|
|
1011
|
-
|
|
1012
|
-
for (const outcome of [added, edited, removed]) {
|
|
1013
|
-
assert.ok(!outcome.ok);
|
|
1014
|
-
assert.equal(outcome.error.code, "ReadOnlyCalendar");
|
|
1015
|
-
}
|
|
1016
|
-
const stored = await account.object(
|
|
1017
|
-
created.value.calendarId,
|
|
1018
|
-
ferry.calendarObjectId,
|
|
1019
|
-
);
|
|
1020
|
-
assert.equal(stored?.etag, ferry.etag);
|
|
1021
|
-
assert.equal((await account.objects()).length, 2);
|
|
1022
|
-
});
|
|
1023
|
-
|
|
1024
|
-
it("turns its refresh off without removing a collection or an event", async () => {
|
|
1025
|
-
const account = anAccount();
|
|
1026
|
-
const created = await subscribe(account);
|
|
1027
|
-
assert.ok(created.ok);
|
|
1028
|
-
const held = await account.objects();
|
|
1029
|
-
|
|
1030
|
-
const paused = await updateCalendarFor(
|
|
1031
|
-
account.deps(),
|
|
1032
|
-
account.accountConfigId,
|
|
1033
|
-
created.value.calendarId,
|
|
1034
|
-
{ subscriptionEnabled: false },
|
|
1035
|
-
);
|
|
1036
|
-
|
|
1037
|
-
assert.ok(paused.ok);
|
|
1038
|
-
assert.equal(paused.value.subscriptionEnabled, false);
|
|
1039
|
-
assert.equal(paused.value.source, CalendarSource.Subscribed);
|
|
1040
|
-
assert.deepEqual(
|
|
1041
|
-
(await account.objects()).map((object) => object.etag).sort(),
|
|
1042
|
-
held.map((object) => object.etag).sort(),
|
|
1043
|
-
);
|
|
1044
|
-
const enabled = await client.calendarCollection.listEnabledSubscriptions();
|
|
1045
|
-
assert.equal(
|
|
1046
|
-
enabled.some(
|
|
1047
|
-
(collection) => collection.calendarId === created.value.calendarId,
|
|
1048
|
-
),
|
|
1049
|
-
false,
|
|
1050
|
-
);
|
|
1051
|
-
|
|
1052
|
-
const resumed = await updateCalendarFor(
|
|
1053
|
-
account.deps(),
|
|
1054
|
-
account.accountConfigId,
|
|
1055
|
-
created.value.calendarId,
|
|
1056
|
-
{ subscriptionEnabled: true },
|
|
1057
|
-
);
|
|
1058
|
-
assert.ok(resumed.ok);
|
|
1059
|
-
assert.equal(resumed.value.subscriptionEnabled, true);
|
|
1060
|
-
});
|
|
1061
|
-
|
|
1062
|
-
it("refuses to turn a refresh on for a calendar that has no feed", async () => {
|
|
1063
|
-
const account = anAccount();
|
|
1064
|
-
const [own] = await listCalendarsFor(
|
|
1065
|
-
account.deps(),
|
|
1066
|
-
account.accountConfigId,
|
|
1067
|
-
);
|
|
1068
|
-
assert.ok(own);
|
|
1069
|
-
|
|
1070
|
-
const updated = await updateCalendarFor(
|
|
1071
|
-
account.deps(),
|
|
1072
|
-
account.accountConfigId,
|
|
1073
|
-
own.calendarId,
|
|
1074
|
-
{ subscriptionEnabled: true },
|
|
1075
|
-
);
|
|
1076
|
-
|
|
1077
|
-
assert.ok(!updated.ok);
|
|
1078
|
-
assert.equal(updated.error.code, "NotSubscribed");
|
|
1079
|
-
});
|
|
1080
|
-
});
|