@remit/backend 0.0.142 → 0.0.144

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/dist/dev-server/content-handler.d.ts +38 -0
  2. package/dist/dev-server/content-handler.js +46 -0
  3. package/dist/src/api.d.ts +7 -0
  4. package/dist/src/api.js +144 -0
  5. package/dist/src/auth.d.ts +30 -0
  6. package/dist/src/auth.js +73 -0
  7. package/dist/src/config/msoauth.d.ts +30 -0
  8. package/dist/src/config/msoauth.js +30 -0
  9. package/{src/data-backend.ts → dist/src/data-backend.d.ts} +2 -4
  10. package/dist/src/data-backend.js +12 -0
  11. package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
  12. package/dist/src/derive/autoMoved.js +49 -0
  13. package/dist/src/derive/contentSignature.d.ts +41 -0
  14. package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +27 -72
  15. package/dist/src/derive/contentUrl.d.ts +46 -0
  16. package/dist/src/derive/contentUrl.js +46 -0
  17. package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
  18. package/dist/src/derive/deriveMuted.js +7 -0
  19. package/dist/src/derive/enrichThreadRows.d.ts +118 -0
  20. package/dist/src/derive/enrichThreadRows.js +151 -0
  21. package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
  22. package/dist/src/derive/filterThreadCriteria.js +24 -0
  23. package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
  24. package/dist/src/derive/pendingMoveCounts.js +53 -0
  25. package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
  26. package/dist/src/derive/senderTrust.js +19 -0
  27. package/dist/src/error.d.ts +21 -0
  28. package/dist/src/error.js +85 -0
  29. package/dist/src/handlers/account-guards.d.ts +36 -0
  30. package/dist/src/handlers/account-guards.js +107 -0
  31. package/dist/src/handlers/account-oauth.d.ts +14 -0
  32. package/dist/src/handlers/account-oauth.js +387 -0
  33. package/dist/src/handlers/account-overrides.d.ts +53 -0
  34. package/dist/src/handlers/account-overrides.js +233 -0
  35. package/dist/src/handlers/account-signature.d.ts +28 -0
  36. package/dist/src/handlers/account-signature.js +85 -0
  37. package/dist/src/handlers/account.d.ts +6 -0
  38. package/dist/src/handlers/account.js +350 -0
  39. package/dist/src/handlers/address.d.ts +35 -0
  40. package/dist/src/handlers/address.js +141 -0
  41. package/dist/src/handlers/calendar-event.d.ts +70 -0
  42. package/dist/src/handlers/calendar-event.js +425 -0
  43. package/dist/src/handlers/calendar-feed.d.ts +47 -0
  44. package/dist/src/handlers/calendar-feed.js +192 -0
  45. package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
  46. package/dist/src/handlers/calendar-suggestion.js +212 -0
  47. package/dist/src/handlers/calendar.d.ts +116 -0
  48. package/dist/src/handlers/calendar.js +330 -0
  49. package/dist/src/handlers/config.d.ts +43 -0
  50. package/dist/src/handlers/config.js +176 -0
  51. package/dist/src/handlers/ensure-account-config.d.ts +9 -0
  52. package/dist/src/handlers/ensure-account-config.js +27 -0
  53. package/dist/src/handlers/filter.d.ts +3 -0
  54. package/dist/src/handlers/filter.js +103 -0
  55. package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
  56. package/dist/src/handlers/folder-role-appointments.js +168 -0
  57. package/dist/src/handlers/folder-role-labels.d.ts +26 -0
  58. package/dist/src/handlers/folder-role-labels.js +40 -0
  59. package/dist/src/handlers/folder-role.d.ts +9 -0
  60. package/dist/src/handlers/folder-role.js +40 -0
  61. package/dist/src/handlers/index.d.ts +2 -0
  62. package/dist/src/handlers/index.js +60 -0
  63. package/dist/src/handlers/label.d.ts +43 -0
  64. package/dist/src/handlers/label.js +114 -0
  65. package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
  66. package/dist/src/handlers/mailbox-bindings.js +61 -0
  67. package/dist/src/handlers/mailbox.d.ts +23 -0
  68. package/dist/src/handlers/mailbox.js +219 -0
  69. package/dist/src/handlers/me.d.ts +2 -0
  70. package/dist/src/handlers/me.js +138 -0
  71. package/dist/src/handlers/message.d.ts +84 -0
  72. package/dist/src/handlers/message.js +607 -0
  73. package/dist/src/handlers/organize.d.ts +10 -0
  74. package/dist/src/handlers/organize.js +112 -0
  75. package/dist/src/handlers/outbox-attachment.d.ts +23 -0
  76. package/dist/src/handlers/outbox-attachment.js +81 -0
  77. package/dist/src/handlers/outbox.d.ts +4 -0
  78. package/dist/src/handlers/outbox.js +144 -0
  79. package/dist/src/handlers/sync-progress.d.ts +46 -0
  80. package/dist/src/handlers/sync-progress.js +63 -0
  81. package/dist/src/handlers/sync.d.ts +28 -0
  82. package/dist/src/handlers/sync.js +82 -0
  83. package/dist/src/handlers/system-update.d.ts +2 -0
  84. package/dist/src/handlers/system-update.js +172 -0
  85. package/dist/src/handlers/thread.d.ts +148 -0
  86. package/dist/src/handlers/thread.js +233 -0
  87. package/dist/src/handlers/unified-threads.d.ts +247 -0
  88. package/dist/src/handlers/unified-threads.js +409 -0
  89. package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
  90. package/dist/src/handlers/vip-suggestions.js +16 -0
  91. package/dist/src/index.d.ts +2 -0
  92. package/dist/src/index.js +37 -0
  93. package/{src/json.ts → dist/src/json.d.ts} +1 -4
  94. package/dist/src/json.js +10 -0
  95. package/dist/src/jwt-auth.d.ts +18 -0
  96. package/dist/src/jwt-auth.js +97 -0
  97. package/dist/src/request-context.d.ts +9 -0
  98. package/dist/src/request-context.js +22 -0
  99. package/dist/src/request.d.ts +3 -0
  100. package/dist/src/request.js +7 -0
  101. package/dist/src/response.d.ts +27 -0
  102. package/dist/src/response.js +87 -0
  103. package/dist/src/service/account-deletion.d.ts +22 -0
  104. package/dist/src/service/account-deletion.js +39 -0
  105. package/dist/src/service/compose-sqlite.d.ts +2 -0
  106. package/dist/src/service/compose-sqlite.js +11 -0
  107. package/dist/src/service/create-remit-client.d.ts +95 -0
  108. package/dist/src/service/create-remit-client.js +207 -0
  109. package/dist/src/service/data-client.d.ts +12 -0
  110. package/dist/src/service/data-client.js +44 -0
  111. package/dist/src/service/export-identity.d.ts +3 -0
  112. package/dist/src/service/export-identity.js +35 -0
  113. package/dist/src/service/fire-and-forget.d.ts +54 -0
  114. package/dist/src/service/fire-and-forget.js +38 -0
  115. package/dist/src/service/organize.d.ts +184 -0
  116. package/dist/src/service/organize.js +384 -0
  117. package/dist/src/service/refusal.d.ts +4 -0
  118. package/dist/src/service/refusal.js +20 -0
  119. package/dist/src/service/sender-category.d.ts +51 -0
  120. package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
  121. package/dist/src/service/sqs.d.ts +1 -0
  122. package/{src/service/sqs.ts → dist/src/service/sqs.js} +0 -1
  123. package/dist/src/service/trigger-sync.d.ts +51 -0
  124. package/dist/src/service/trigger-sync.js +39 -0
  125. package/dist/src/types.d.ts +49 -0
  126. package/dist/src/types.js +1 -0
  127. package/package.json +113 -98
  128. package/.env.test +0 -4
  129. package/dev-server/content-auth.test.ts +0 -113
  130. package/dev-server/content-auth.ts +0 -54
  131. package/dev-server/content-handler.test.ts +0 -105
  132. package/dev-server/content-handler.ts +0 -79
  133. package/dev-server/content-path.test.ts +0 -37
  134. package/dev-server/content-path.ts +0 -27
  135. package/dev-server/cors.test.ts +0 -48
  136. package/dev-server/cors.ts +0 -25
  137. package/dev-server/keep-alive.test.ts +0 -24
  138. package/dev-server/lambda-helpers.ts +0 -69
  139. package/dev-server/log-output.test.ts +0 -173
  140. package/dev-server/oauth-callback-route.test.ts +0 -66
  141. package/dev-server/relational-health.test.ts +0 -83
  142. package/dev-server/relational-health.ts +0 -39
  143. package/dev-server/server.ts +0 -384
  144. package/dev-server/sync-age.test.ts +0 -91
  145. package/dev-server/sync-age.ts +0 -55
  146. package/dev-server/upload-handler.ts +0 -132
  147. package/dev-server/upload-route.test.ts +0 -196
  148. package/scripts/backfill-classification.ts +0 -91
  149. package/scripts/backfill-list-id.ts +0 -83
  150. package/scripts/config-save.ts +0 -128
  151. package/src/auth.test.ts +0 -12
  152. package/src/auth.ts +0 -92
  153. package/src/config/msoauth.test.ts +0 -48
  154. package/src/config/msoauth.ts +0 -48
  155. package/src/data-backend.test.ts +0 -23
  156. package/src/derive/autoMoved.test.ts +0 -104
  157. package/src/derive/contentSignature.test.ts +0 -161
  158. package/src/derive/contentUrl.test.ts +0 -172
  159. package/src/derive/contentUrl.ts +0 -86
  160. package/src/derive/deriveMuted.test.ts +0 -34
  161. package/src/derive/enrichThreadRows.test.ts +0 -360
  162. package/src/derive/enrichThreadRows.ts +0 -234
  163. package/src/derive/filterThreadCriteria.test.ts +0 -126
  164. package/src/derive/filterThreadCriteria.ts +0 -51
  165. package/src/derive/senderTrust.test.ts +0 -67
  166. package/src/error.test.ts +0 -201
  167. package/src/error.ts +0 -123
  168. package/src/handlers/account-guards.test.ts +0 -67
  169. package/src/handlers/account-guards.ts +0 -169
  170. package/src/handlers/account-oauth-callback.test.ts +0 -155
  171. package/src/handlers/account-oauth-consent.test.ts +0 -336
  172. package/src/handlers/account-oauth-start.test.ts +0 -191
  173. package/src/handlers/account-oauth.test.ts +0 -439
  174. package/src/handlers/account-oauth.ts +0 -635
  175. package/src/handlers/account-overrides.test.ts +0 -88
  176. package/src/handlers/account-overrides.ts +0 -323
  177. package/src/handlers/account-ownership.ts +0 -25
  178. package/src/handlers/account-signature.ts +0 -117
  179. package/src/handlers/account-synced-services.test.ts +0 -245
  180. package/src/handlers/account.ts +0 -552
  181. package/src/handlers/address.test.ts +0 -381
  182. package/src/handlers/address.ts +0 -200
  183. package/src/handlers/calendar-event.test.ts +0 -685
  184. package/src/handlers/calendar-event.ts +0 -738
  185. package/src/handlers/calendar-feed-validators.test.ts +0 -228
  186. package/src/handlers/calendar-feed.test.ts +0 -439
  187. package/src/handlers/calendar-feed.ts +0 -291
  188. package/src/handlers/calendar-sqlite-fixture.ts +0 -36
  189. package/src/handlers/calendar-suggestion.test.ts +0 -1269
  190. package/src/handlers/calendar-suggestion.ts +0 -425
  191. package/src/handlers/calendar.test.ts +0 -1080
  192. package/src/handlers/calendar.ts +0 -517
  193. package/src/handlers/config.test.ts +0 -245
  194. package/src/handlers/config.ts +0 -332
  195. package/src/handlers/ensure-account-config.ts +0 -30
  196. package/src/handlers/filter.test.ts +0 -644
  197. package/src/handlers/filter.ts +0 -518
  198. package/src/handlers/folder-role-appointments.test.ts +0 -438
  199. package/src/handlers/folder-role-appointments.ts +0 -249
  200. package/src/handlers/folder-role-labels.ts +0 -63
  201. package/src/handlers/folder-role.test.ts +0 -209
  202. package/src/handlers/folder-role.ts +0 -80
  203. package/src/handlers/index.ts +0 -83
  204. package/src/handlers/label.test.ts +0 -112
  205. package/src/handlers/label.ts +0 -210
  206. package/src/handlers/mailbox-bindings.ts +0 -111
  207. package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
  208. package/src/handlers/mailbox-settled-target.test.ts +0 -251
  209. package/src/handlers/mailbox.test.ts +0 -280
  210. package/src/handlers/mailbox.ts +0 -581
  211. package/src/handlers/me.test.ts +0 -81
  212. package/src/handlers/me.ts +0 -203
  213. package/src/handlers/message-read-backfill.test.ts +0 -103
  214. package/src/handlers/message.test.ts +0 -131
  215. package/src/handlers/message.ts +0 -1131
  216. package/src/handlers/organize.test.ts +0 -277
  217. package/src/handlers/organize.ts +0 -197
  218. package/src/handlers/outbox-attachment.test.ts +0 -765
  219. package/src/handlers/outbox-attachment.ts +0 -126
  220. package/src/handlers/outbox-list.test.ts +0 -179
  221. package/src/handlers/outbox.test.ts +0 -772
  222. package/src/handlers/outbox.ts +0 -258
  223. package/src/handlers/search.test.ts +0 -161
  224. package/src/handlers/search.ts +0 -105
  225. package/src/handlers/sync-progress.test.ts +0 -205
  226. package/src/handlers/sync-progress.ts +0 -99
  227. package/src/handlers/sync.test.ts +0 -168
  228. package/src/handlers/sync.ts +0 -118
  229. package/src/handlers/system-update.test.ts +0 -497
  230. package/src/handlers/system-update.ts +0 -230
  231. package/src/handlers/thread.test.ts +0 -396
  232. package/src/handlers/thread.ts +0 -427
  233. package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
  234. package/src/handlers/unified-threads.test.ts +0 -785
  235. package/src/handlers/unified-threads.ts +0 -669
  236. package/src/index.test.ts +0 -83
  237. package/src/index.ts +0 -228
  238. package/src/jwt-auth.test.ts +0 -152
  239. package/src/jwt-auth.ts +0 -132
  240. package/src/log-redaction.test.ts +0 -144
  241. package/src/openapi-spec.test.ts +0 -118
  242. package/src/request-context.test.ts +0 -61
  243. package/src/request-context.ts +0 -33
  244. package/src/request-pipeline.test.ts +0 -88
  245. package/src/request.ts +0 -10
  246. package/src/response.test.ts +0 -130
  247. package/src/response.ts +0 -144
  248. package/src/service/compose-sqlite.ts +0 -98
  249. package/src/service/config-import.ts +0 -39
  250. package/src/service/create-remit-client.ts +0 -521
  251. package/src/service/data-client.test.ts +0 -97
  252. package/src/service/data-client.ts +0 -58
  253. package/src/service/export-identity.ts +0 -39
  254. package/src/service/filter.ts +0 -40
  255. package/src/service/fire-and-forget.test.ts +0 -126
  256. package/src/service/fire-and-forget.ts +0 -79
  257. package/src/service/localhost-env-config.test.ts +0 -36
  258. package/src/service/organize-queue.ts +0 -98
  259. package/src/service/organize.test.ts +0 -1451
  260. package/src/service/organize.ts +0 -1025
  261. package/src/service/read-path-filter-config.test.ts +0 -243
  262. package/src/service/semantic-capability.test.ts +0 -78
  263. package/src/service/semantic-capability.ts +0 -95
  264. package/src/service/sender-category.test.ts +0 -235
  265. package/src/service/trigger-sync.test.ts +0 -205
  266. package/src/service/trigger-sync.ts +0 -100
  267. package/src/stdout-capture-fixture.ts +0 -40
  268. package/src/types.ts +0 -271
  269. 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
- });