@remit/backend 0.0.91 → 0.0.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/handlers/calendar-event.test.ts +568 -0
- package/src/handlers/calendar-sqlite-fixture.ts +64 -0
- package/src/handlers/calendar-suggestion.test.ts +558 -0
- package/src/handlers/calendar-suggestion.ts +289 -0
- package/src/handlers/calendar.test.ts +412 -445
- package/src/handlers/index.ts +8 -0
- package/src/service/compose-sqlite.ts +2 -0
- package/src/service/create-remit-client.ts +20 -6
- package/src/types.ts +21 -1
|
@@ -1,32 +1,24 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
2
|
+
import { after, before, describe, it } from "node:test";
|
|
3
3
|
import type {
|
|
4
4
|
CalendarCollectionItem,
|
|
5
5
|
CalendarEventIndexItem,
|
|
6
6
|
CalendarObjectItem,
|
|
7
|
-
CalendarOccurrenceInput,
|
|
8
|
-
CreateCalendarCollectionInput,
|
|
9
|
-
ICalendarCollectionRepository,
|
|
10
|
-
ICalendarEventIndexRepository,
|
|
11
|
-
ICalendarObjectRepository,
|
|
12
|
-
ICalendarUnitOfWork,
|
|
13
|
-
PutCalendarObjectInput,
|
|
14
|
-
UpdateCalendarCollectionInput,
|
|
15
7
|
} from "@remit/data-ports";
|
|
16
|
-
import {
|
|
8
|
+
import { CalendarSource, RecurrenceScope } from "@remit/domain-enums";
|
|
9
|
+
import type { APIGatewayProxyEvent } from "aws-lambda";
|
|
10
|
+
import type { Context } from "openapi-backend";
|
|
11
|
+
import { deriveAccountConfigId } from "../auth.js";
|
|
17
12
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} from "
|
|
22
|
-
import {
|
|
23
|
-
CalendarColor,
|
|
24
|
-
CalendarComponentSet,
|
|
25
|
-
CalendarSource,
|
|
26
|
-
RecurrenceScope,
|
|
27
|
-
} from "@remit/domain-enums";
|
|
13
|
+
_resetForTest,
|
|
14
|
+
type RemitClient,
|
|
15
|
+
setClient,
|
|
16
|
+
} from "../service/data-client.js";
|
|
28
17
|
import {
|
|
29
18
|
type CalendarDeps,
|
|
19
|
+
CalendarDetailOperations,
|
|
20
|
+
CalendarOperations,
|
|
21
|
+
calendarDepsOf,
|
|
30
22
|
createCalendarFor,
|
|
31
23
|
deleteCalendarFor,
|
|
32
24
|
listCalendarsFor,
|
|
@@ -43,325 +35,112 @@ import {
|
|
|
43
35
|
readWindow,
|
|
44
36
|
updateCalendarEventFor,
|
|
45
37
|
} from "./calendar-event.js";
|
|
38
|
+
import { createCalendarSqliteClient } from "./calendar-sqlite-fixture.js";
|
|
46
39
|
|
|
47
40
|
/**
|
|
48
|
-
* The calendar
|
|
49
|
-
*
|
|
50
|
-
*
|
|
41
|
+
* The calendar handlers against the store the self-host build ships.
|
|
42
|
+
*
|
|
43
|
+
* This file used to run on a set of in-memory port implementations that claimed
|
|
44
|
+
* to behave exactly as sqlite. Nothing checked the claim, so it was worth
|
|
45
|
+
* nothing: `putCalendarObject` projects, expands and bumps the sequence, and a
|
|
46
|
+
* memory twin of that is a second implementation of the write path that can
|
|
47
|
+
* drift from the real one silently. Every test here now writes through the
|
|
48
|
+
* drizzle repositories, so the projection, the expansion and the transaction
|
|
49
|
+
* are the shipped ones.
|
|
51
50
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* going through it would fail these tests rather than pass them.
|
|
51
|
+
* One database serves the file. Every calendar row is scoped by account config
|
|
52
|
+
* and `calendarId` is derived from it, so a test that mints its own account
|
|
53
|
+
* sees only what it wrote.
|
|
56
54
|
*/
|
|
57
|
-
class CalendarState {
|
|
58
|
-
readonly collections = new Map<string, CalendarCollectionItem>();
|
|
59
|
-
readonly objects = new Map<string, CalendarObjectItem>();
|
|
60
|
-
readonly occurrences = new Map<string, CalendarEventIndexItem[]>();
|
|
61
|
-
}
|
|
62
55
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
async create(
|
|
67
|
-
input: CreateCalendarCollectionInput,
|
|
68
|
-
): Promise<CalendarCollectionItem> {
|
|
69
|
-
const urlSegment = normalizeCalendarUrlSegment(input.urlSegment);
|
|
70
|
-
const calendarId = deriveCalendarId(input.accountConfigId, urlSegment);
|
|
71
|
-
const existing = this.state.collections.get(calendarId);
|
|
72
|
-
if (existing) return existing;
|
|
73
|
-
|
|
74
|
-
const created: CalendarCollectionItem = {
|
|
75
|
-
calendarId,
|
|
76
|
-
accountConfigId: input.accountConfigId,
|
|
77
|
-
urlSegment,
|
|
78
|
-
displayName: input.displayName,
|
|
79
|
-
color: input.color ?? CalendarColor.Cal1,
|
|
80
|
-
componentSet: input.componentSet ?? CalendarComponentSet.VeventOnly,
|
|
81
|
-
source: input.source ?? CalendarSource.UserCreated,
|
|
82
|
-
timezone: input.timezone ?? "",
|
|
83
|
-
syncSequence: 0,
|
|
84
|
-
createdAt: 0,
|
|
85
|
-
updatedAt: 0,
|
|
86
|
-
};
|
|
87
|
-
this.state.collections.set(calendarId, created);
|
|
88
|
-
return created;
|
|
89
|
-
}
|
|
56
|
+
let client: RemitClient;
|
|
57
|
+
let cleanup: () => void;
|
|
58
|
+
let mintedAccounts = 0;
|
|
90
59
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const calendarId = deriveCalendarId(
|
|
95
|
-
input.accountConfigId,
|
|
96
|
-
normalizeCalendarUrlSegment(input.urlSegment),
|
|
97
|
-
);
|
|
98
|
-
if (this.state.collections.has(calendarId)) return null;
|
|
99
|
-
return this.create(input);
|
|
100
|
-
}
|
|
60
|
+
/** One caller's calendars, and the two ways the handlers reach them. */
|
|
61
|
+
class CalendarAccount {
|
|
62
|
+
readonly accountConfigId: string;
|
|
101
63
|
|
|
102
|
-
|
|
103
|
-
accountConfigId
|
|
104
|
-
calendarId: string,
|
|
105
|
-
): Promise<CalendarCollectionItem> {
|
|
106
|
-
const found = this.state.collections.get(calendarId);
|
|
107
|
-
if (!found || found.accountConfigId !== accountConfigId) {
|
|
108
|
-
throw new NotFoundError(`Calendar not found: ${calendarId}`);
|
|
109
|
-
}
|
|
110
|
-
return found;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async update(
|
|
114
|
-
accountConfigId: string,
|
|
115
|
-
calendarId: string,
|
|
116
|
-
input: UpdateCalendarCollectionInput,
|
|
117
|
-
): Promise<CalendarCollectionItem> {
|
|
118
|
-
const updated = {
|
|
119
|
-
...(await this.get(accountConfigId, calendarId)),
|
|
120
|
-
...input,
|
|
121
|
-
};
|
|
122
|
-
this.state.collections.set(calendarId, updated);
|
|
123
|
-
return updated;
|
|
64
|
+
constructor(readonly sub: string) {
|
|
65
|
+
this.accountConfigId = deriveAccountConfigId(sub);
|
|
124
66
|
}
|
|
125
67
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
accountConfigId: string,
|
|
132
|
-
): Promise<CalendarCollectionItem[]> {
|
|
133
|
-
return [...this.state.collections.values()]
|
|
134
|
-
.filter((item) => item.accountConfigId === accountConfigId)
|
|
135
|
-
.sort((left, right) => left.urlSegment.localeCompare(right.urlSegment));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async findByUrlSegment(
|
|
139
|
-
accountConfigId: string,
|
|
140
|
-
urlSegment: string,
|
|
141
|
-
): Promise<CalendarCollectionItem | null> {
|
|
142
|
-
return (
|
|
143
|
-
this.state.collections.get(
|
|
144
|
-
deriveCalendarId(
|
|
145
|
-
accountConfigId,
|
|
146
|
-
normalizeCalendarUrlSegment(urlSegment),
|
|
147
|
-
),
|
|
148
|
-
) ?? null
|
|
149
|
-
);
|
|
68
|
+
/** The request an authenticated caller of this account arrives on. */
|
|
69
|
+
request(): APIGatewayProxyEvent {
|
|
70
|
+
return {
|
|
71
|
+
requestContext: { authorizer: { claims: { sub: this.sub } } },
|
|
72
|
+
} as unknown as APIGatewayProxyEvent;
|
|
150
73
|
}
|
|
151
74
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
calendarId: string,
|
|
155
|
-
): Promise<number> {
|
|
156
|
-
const current = await this.get(accountConfigId, calendarId);
|
|
157
|
-
const bumped = { ...current, syncSequence: current.syncSequence + 1 };
|
|
158
|
-
this.state.collections.set(calendarId, bumped);
|
|
159
|
-
return bumped.syncSequence;
|
|
75
|
+
deps(): CalendarDeps {
|
|
76
|
+
return calendarDepsOf(client);
|
|
160
77
|
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
class MemoryObjects implements ICalendarObjectRepository {
|
|
164
|
-
constructor(private state: CalendarState) {}
|
|
165
78
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
79
|
+
/** The same deps with id minting and the clock pinned. */
|
|
80
|
+
eventDeps(): CalendarEventDeps {
|
|
81
|
+
let minted = 0;
|
|
82
|
+
return {
|
|
83
|
+
...this.deps(),
|
|
84
|
+
newId: () => {
|
|
85
|
+
minted += 1;
|
|
86
|
+
return `${this.accountConfigId}-minted-${minted}`;
|
|
87
|
+
},
|
|
88
|
+
now: () => new Date("2026-08-29T00:00:00Z"),
|
|
176
89
|
};
|
|
177
|
-
this.state.objects.set(calendarObjectId, stored);
|
|
178
|
-
return stored;
|
|
179
90
|
}
|
|
180
91
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
calendarObjectId: string,
|
|
184
|
-
): Promise<CalendarObjectItem> {
|
|
185
|
-
const found = await this.find(calendarId, calendarObjectId);
|
|
186
|
-
if (!found) {
|
|
187
|
-
throw new NotFoundError(`Calendar object not found: ${calendarObjectId}`);
|
|
188
|
-
}
|
|
189
|
-
return found;
|
|
92
|
+
collections(): Promise<CalendarCollectionItem[]> {
|
|
93
|
+
return client.calendarCollection.listByAccountConfig(this.accountConfigId);
|
|
190
94
|
}
|
|
191
95
|
|
|
192
|
-
async
|
|
193
|
-
|
|
194
|
-
calendarObjectId: string,
|
|
195
|
-
): Promise<CalendarObjectItem | null> {
|
|
196
|
-
const found = this.state.objects.get(calendarObjectId);
|
|
197
|
-
return found && found.calendarId === calendarId ? found : null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async delete(_calendarId: string, calendarObjectId: string): Promise<void> {
|
|
201
|
-
this.state.objects.delete(calendarObjectId);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
async findByResourceName(
|
|
205
|
-
calendarId: string,
|
|
206
|
-
resourceName: string,
|
|
207
|
-
): Promise<CalendarObjectItem | null> {
|
|
208
|
-
return this.find(
|
|
209
|
-
calendarId,
|
|
210
|
-
deriveCalendarObjectId(calendarId, resourceName),
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async findByUid(
|
|
215
|
-
calendarId: string,
|
|
216
|
-
icalUid: string,
|
|
217
|
-
): Promise<CalendarObjectItem | null> {
|
|
96
|
+
async collection(calendarId: string): Promise<CalendarCollectionItem | null> {
|
|
97
|
+
const held = await this.collections();
|
|
218
98
|
return (
|
|
219
|
-
|
|
220
|
-
(object) =>
|
|
221
|
-
object.calendarId === calendarId && object.icalUid === icalUid,
|
|
222
|
-
) ?? null
|
|
99
|
+
held.find((collection) => collection.calendarId === calendarId) ?? null
|
|
223
100
|
);
|
|
224
101
|
}
|
|
225
102
|
|
|
226
|
-
async
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
103
|
+
async objects(): Promise<CalendarObjectItem[]> {
|
|
104
|
+
const held = await this.collections();
|
|
105
|
+
const objects: CalendarObjectItem[] = [];
|
|
106
|
+
for (const collection of held) {
|
|
107
|
+
objects.push(
|
|
108
|
+
...(await client.calendarObject.listByCalendar(collection.calendarId)),
|
|
231
109
|
);
|
|
110
|
+
}
|
|
111
|
+
return objects;
|
|
232
112
|
}
|
|
233
113
|
|
|
234
|
-
|
|
235
|
-
calendarId: string,
|
|
236
|
-
instant: string,
|
|
237
|
-
): Promise<CalendarObjectItem[]> {
|
|
238
|
-
return (await this.listByCalendar(calendarId)).filter(
|
|
239
|
-
(object) =>
|
|
240
|
-
object.expandedThrough !== "" && object.expandedThrough < instant,
|
|
241
|
-
);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async listChangedSince(
|
|
245
|
-
calendarId: string,
|
|
246
|
-
syncSequence: number,
|
|
247
|
-
): Promise<CalendarObjectItem[]> {
|
|
248
|
-
return (await this.listByCalendar(calendarId))
|
|
249
|
-
.filter((object) => object.syncSequence > syncSequence)
|
|
250
|
-
.sort((left, right) => left.syncSequence - right.syncSequence);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
class MemoryOccurrences implements ICalendarEventIndexRepository {
|
|
255
|
-
constructor(private state: CalendarState) {}
|
|
256
|
-
|
|
257
|
-
async replaceForObject(
|
|
114
|
+
object(
|
|
258
115
|
calendarId: string,
|
|
259
116
|
calendarObjectId: string,
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
this.state.occurrences.set(
|
|
263
|
-
calendarObjectId,
|
|
264
|
-
occurrences.map((occurrence) => ({
|
|
265
|
-
...occurrence,
|
|
266
|
-
calendarId,
|
|
267
|
-
calendarObjectId,
|
|
268
|
-
createdAt: 0,
|
|
269
|
-
updatedAt: 0,
|
|
270
|
-
})),
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async deleteForObject(
|
|
275
|
-
_calendarId: string,
|
|
276
|
-
calendarObjectId: string,
|
|
277
|
-
): Promise<void> {
|
|
278
|
-
this.state.occurrences.delete(calendarObjectId);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
async listForObject(
|
|
282
|
-
_calendarId: string,
|
|
283
|
-
calendarObjectId: string,
|
|
284
|
-
): Promise<CalendarEventIndexItem[]> {
|
|
285
|
-
return this.state.occurrences.get(calendarObjectId) ?? [];
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
async listByStartRange(
|
|
289
|
-
calendarId: string,
|
|
290
|
-
startAt: string,
|
|
291
|
-
endAt: string,
|
|
292
|
-
): Promise<CalendarEventIndexItem[]> {
|
|
293
|
-
return [...this.state.occurrences.values()]
|
|
294
|
-
.flat()
|
|
295
|
-
.filter(
|
|
296
|
-
(row) =>
|
|
297
|
-
row.calendarId === calendarId &&
|
|
298
|
-
row.startAt >= startAt &&
|
|
299
|
-
row.startAt < endAt,
|
|
300
|
-
)
|
|
301
|
-
.sort((left, right) => left.startAt.localeCompare(right.startAt));
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
class InMemoryCalendarStore implements ICalendarUnitOfWork {
|
|
306
|
-
readonly state = new CalendarState();
|
|
307
|
-
readonly calendarCollection = new MemoryCollections(this.state);
|
|
308
|
-
readonly calendarObject = new MemoryObjects(this.state);
|
|
309
|
-
readonly calendarEventIndex = new MemoryOccurrences(this.state);
|
|
310
|
-
|
|
311
|
-
get collections(): Map<string, CalendarCollectionItem> {
|
|
312
|
-
return this.state.collections;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
get objects(): Map<string, CalendarObjectItem> {
|
|
316
|
-
return this.state.objects;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
get occurrences(): Map<string, CalendarEventIndexItem[]> {
|
|
320
|
-
return this.state.occurrences;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// No isolation to model: the tests that care about atomicity run against
|
|
324
|
-
// sqlite, where the transaction is real.
|
|
325
|
-
transaction<T>(
|
|
326
|
-
fn: (repos: {
|
|
327
|
-
calendarCollection: ICalendarCollectionRepository;
|
|
328
|
-
calendarObject: ICalendarObjectRepository;
|
|
329
|
-
calendarEventIndex: ICalendarEventIndexRepository;
|
|
330
|
-
}) => Promise<T>,
|
|
331
|
-
): Promise<T> {
|
|
332
|
-
return fn(this);
|
|
117
|
+
): Promise<CalendarObjectItem | null> {
|
|
118
|
+
return client.calendarObject.find(calendarId, calendarObjectId);
|
|
333
119
|
}
|
|
334
120
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
121
|
+
occurrences(object: {
|
|
122
|
+
calendarId: string;
|
|
123
|
+
calendarObjectId: string;
|
|
124
|
+
}): Promise<CalendarEventIndexItem[]> {
|
|
125
|
+
return client.calendarEventIndex.listForObject(
|
|
126
|
+
object.calendarId,
|
|
127
|
+
object.calendarObjectId,
|
|
128
|
+
);
|
|
342
129
|
}
|
|
343
130
|
}
|
|
344
131
|
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
let minted = 0;
|
|
349
|
-
return {
|
|
350
|
-
...store.deps(),
|
|
351
|
-
newId: () => {
|
|
352
|
-
minted += 1;
|
|
353
|
-
return `minted-${minted}`;
|
|
354
|
-
},
|
|
355
|
-
now: () => new Date("2026-08-29T00:00:00Z"),
|
|
356
|
-
};
|
|
132
|
+
const anAccount = (): CalendarAccount => {
|
|
133
|
+
mintedAccounts += 1;
|
|
134
|
+
return new CalendarAccount(`calendar-sub-${mintedAccounts}`);
|
|
357
135
|
};
|
|
358
136
|
|
|
359
137
|
const seedWeekly = async (
|
|
360
138
|
deps: CalendarEventDeps,
|
|
139
|
+
accountConfigId: string,
|
|
361
140
|
calendarId: string,
|
|
362
141
|
recurrenceRule = "FREQ=WEEKLY;COUNT=5",
|
|
363
142
|
) => {
|
|
364
|
-
const created = await createCalendarEventFor(deps,
|
|
143
|
+
const created = await createCalendarEventFor(deps, accountConfigId, {
|
|
365
144
|
calendarId,
|
|
366
145
|
summary: "Stand-up",
|
|
367
146
|
start: "2026-09-07T09:00:00Z",
|
|
@@ -372,11 +151,25 @@ const seedWeekly = async (
|
|
|
372
151
|
return created.value;
|
|
373
152
|
};
|
|
374
153
|
|
|
154
|
+
before(async () => {
|
|
155
|
+
_resetForTest();
|
|
156
|
+
({ client, cleanup } = await createCalendarSqliteClient());
|
|
157
|
+
setClient(client);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
after(() => {
|
|
161
|
+
_resetForTest();
|
|
162
|
+
cleanup();
|
|
163
|
+
});
|
|
164
|
+
|
|
375
165
|
describe("listCalendarsFor", () => {
|
|
376
166
|
it("provisions the default calendar on a first read", async () => {
|
|
377
|
-
const
|
|
167
|
+
const account = anAccount();
|
|
378
168
|
|
|
379
|
-
const calendars = await listCalendarsFor(
|
|
169
|
+
const calendars = await listCalendarsFor(
|
|
170
|
+
account.deps(),
|
|
171
|
+
account.accountConfigId,
|
|
172
|
+
);
|
|
380
173
|
|
|
381
174
|
assert.equal(calendars.length, 1);
|
|
382
175
|
assert.equal(calendars[0]?.urlSegment, "default");
|
|
@@ -384,13 +177,15 @@ describe("listCalendarsFor", () => {
|
|
|
384
177
|
});
|
|
385
178
|
|
|
386
179
|
it("provisions it exactly once when several reads arrive together", async () => {
|
|
387
|
-
const
|
|
180
|
+
const account = anAccount();
|
|
388
181
|
|
|
389
182
|
const reads = await Promise.all(
|
|
390
|
-
Array.from({ length: 8 }, () =>
|
|
183
|
+
Array.from({ length: 8 }, () =>
|
|
184
|
+
listCalendarsFor(account.deps(), account.accountConfigId),
|
|
185
|
+
),
|
|
391
186
|
);
|
|
392
187
|
|
|
393
|
-
assert.equal(
|
|
188
|
+
assert.equal((await account.collections()).length, 1);
|
|
394
189
|
const ids = new Set(reads.flat().map((calendar) => calendar.calendarId));
|
|
395
190
|
assert.equal(ids.size, 1);
|
|
396
191
|
});
|
|
@@ -398,35 +193,40 @@ describe("listCalendarsFor", () => {
|
|
|
398
193
|
|
|
399
194
|
describe("createCalendarFor", () => {
|
|
400
195
|
it("refuses a url segment the account already uses", async () => {
|
|
401
|
-
const
|
|
402
|
-
const deps =
|
|
403
|
-
await createCalendarFor(deps,
|
|
196
|
+
const account = anAccount();
|
|
197
|
+
const deps = account.deps();
|
|
198
|
+
await createCalendarFor(deps, account.accountConfigId, {
|
|
404
199
|
urlSegment: "work",
|
|
405
200
|
displayName: "Work",
|
|
406
201
|
});
|
|
407
202
|
|
|
408
|
-
const second = await createCalendarFor(deps,
|
|
203
|
+
const second = await createCalendarFor(deps, account.accountConfigId, {
|
|
409
204
|
urlSegment: "WORK",
|
|
410
205
|
displayName: "Work again",
|
|
411
206
|
});
|
|
412
207
|
|
|
413
208
|
assert.ok(!second.ok);
|
|
414
209
|
assert.equal(second.error.code, "UrlSegmentTaken");
|
|
415
|
-
|
|
210
|
+
const held = await account.collections();
|
|
211
|
+
assert.equal(held.length, 1);
|
|
416
212
|
assert.equal(
|
|
417
|
-
[
|
|
213
|
+
held[0]?.displayName,
|
|
418
214
|
"Work",
|
|
419
215
|
"the refused create never wrote over the calendar that holds the segment",
|
|
420
216
|
);
|
|
421
217
|
});
|
|
422
218
|
|
|
423
219
|
it("refuses an empty url segment", async () => {
|
|
424
|
-
const
|
|
220
|
+
const account = anAccount();
|
|
425
221
|
|
|
426
|
-
const created = await createCalendarFor(
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
222
|
+
const created = await createCalendarFor(
|
|
223
|
+
account.deps(),
|
|
224
|
+
account.accountConfigId,
|
|
225
|
+
{
|
|
226
|
+
urlSegment: " ",
|
|
227
|
+
displayName: "Nameless",
|
|
228
|
+
},
|
|
229
|
+
);
|
|
430
230
|
|
|
431
231
|
assert.ok(!created.ok);
|
|
432
232
|
assert.equal(created.error.code, "InvalidUrlSegment");
|
|
@@ -435,49 +235,57 @@ describe("createCalendarFor", () => {
|
|
|
435
235
|
|
|
436
236
|
describe("deleteCalendarFor", () => {
|
|
437
237
|
it("refuses to remove the calendar events fall back to", async () => {
|
|
438
|
-
const
|
|
439
|
-
const deps =
|
|
440
|
-
const [fallback] = await listCalendarsFor(deps,
|
|
238
|
+
const account = anAccount();
|
|
239
|
+
const deps = account.deps();
|
|
240
|
+
const [fallback] = await listCalendarsFor(deps, account.accountConfigId);
|
|
441
241
|
assert.ok(fallback);
|
|
442
242
|
|
|
443
|
-
const removed = await deleteCalendarFor(
|
|
243
|
+
const removed = await deleteCalendarFor(
|
|
244
|
+
deps,
|
|
245
|
+
account.accountConfigId,
|
|
246
|
+
fallback.calendarId,
|
|
247
|
+
);
|
|
444
248
|
|
|
445
249
|
assert.ok(!removed.ok);
|
|
446
250
|
assert.equal(removed.error.code, "DefaultCalendarUndeletable");
|
|
447
|
-
assert.equal(
|
|
251
|
+
assert.equal((await account.collections()).length, 1);
|
|
448
252
|
});
|
|
449
253
|
|
|
450
254
|
it("takes the events and their occurrences with a calendar it does remove", async () => {
|
|
451
|
-
const
|
|
452
|
-
const deps = eventDeps(
|
|
453
|
-
const created = await createCalendarFor(deps,
|
|
255
|
+
const account = anAccount();
|
|
256
|
+
const deps = account.eventDeps();
|
|
257
|
+
const created = await createCalendarFor(deps, account.accountConfigId, {
|
|
454
258
|
urlSegment: "work",
|
|
455
259
|
displayName: "Work",
|
|
456
260
|
});
|
|
457
261
|
assert.ok(created.ok);
|
|
458
|
-
await seedWeekly(
|
|
459
|
-
|
|
262
|
+
const event = await seedWeekly(
|
|
263
|
+
deps,
|
|
264
|
+
account.accountConfigId,
|
|
265
|
+
created.value.calendarId,
|
|
266
|
+
);
|
|
267
|
+
assert.equal((await account.objects()).length, 1);
|
|
460
268
|
|
|
461
269
|
const removed = await deleteCalendarFor(
|
|
462
270
|
deps,
|
|
463
|
-
|
|
271
|
+
account.accountConfigId,
|
|
464
272
|
created.value.calendarId,
|
|
465
273
|
);
|
|
466
274
|
|
|
467
275
|
assert.ok(removed.ok);
|
|
468
|
-
assert.
|
|
469
|
-
assert.
|
|
276
|
+
assert.deepEqual(await account.objects(), []);
|
|
277
|
+
assert.deepEqual(await account.occurrences(event), []);
|
|
470
278
|
});
|
|
471
279
|
|
|
472
280
|
it("answers not-found for a calendar on another account", async () => {
|
|
473
|
-
const
|
|
474
|
-
const deps =
|
|
475
|
-
const [mine] = await listCalendarsFor(deps,
|
|
281
|
+
const account = anAccount();
|
|
282
|
+
const deps = account.deps();
|
|
283
|
+
const [mine] = await listCalendarsFor(deps, account.accountConfigId);
|
|
476
284
|
assert.ok(mine);
|
|
477
285
|
|
|
478
286
|
const removed = await deleteCalendarFor(
|
|
479
287
|
deps,
|
|
480
|
-
|
|
288
|
+
anAccount().accountConfigId,
|
|
481
289
|
mine.calendarId,
|
|
482
290
|
);
|
|
483
291
|
|
|
@@ -545,15 +353,19 @@ describe("pickEventUpdate", () => {
|
|
|
545
353
|
|
|
546
354
|
describe("updateCalendarEventFor", () => {
|
|
547
355
|
it("refuses a write built on an etag the resource no longer carries", async () => {
|
|
548
|
-
const
|
|
549
|
-
const deps = eventDeps(
|
|
550
|
-
const [calendar] = await listCalendarsFor(deps,
|
|
356
|
+
const account = anAccount();
|
|
357
|
+
const deps = account.eventDeps();
|
|
358
|
+
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
551
359
|
assert.ok(calendar);
|
|
552
|
-
const event = await seedWeekly(
|
|
360
|
+
const event = await seedWeekly(
|
|
361
|
+
deps,
|
|
362
|
+
account.accountConfigId,
|
|
363
|
+
calendar.calendarId,
|
|
364
|
+
);
|
|
553
365
|
|
|
554
366
|
const first = await updateCalendarEventFor(
|
|
555
367
|
deps,
|
|
556
|
-
|
|
368
|
+
account.accountConfigId,
|
|
557
369
|
{
|
|
558
370
|
calendarId: calendar.calendarId,
|
|
559
371
|
calendarObjectId: event.calendarObjectId,
|
|
@@ -567,7 +379,7 @@ describe("updateCalendarEventFor", () => {
|
|
|
567
379
|
|
|
568
380
|
const stale = await updateCalendarEventFor(
|
|
569
381
|
deps,
|
|
570
|
-
|
|
382
|
+
account.accountConfigId,
|
|
571
383
|
{
|
|
572
384
|
calendarId: calendar.calendarId,
|
|
573
385
|
calendarObjectId: event.calendarObjectId,
|
|
@@ -580,23 +392,31 @@ describe("updateCalendarEventFor", () => {
|
|
|
580
392
|
|
|
581
393
|
assert.ok(!stale.ok);
|
|
582
394
|
assert.equal(stale.error.code, "EtagMismatch");
|
|
395
|
+
const survivor = await account.object(
|
|
396
|
+
calendar.calendarId,
|
|
397
|
+
event.calendarObjectId,
|
|
398
|
+
);
|
|
583
399
|
assert.equal(
|
|
584
|
-
|
|
400
|
+
survivor?.summary,
|
|
585
401
|
"Stand-up (renamed)",
|
|
586
402
|
"the losing write left the resource alone",
|
|
587
403
|
);
|
|
588
404
|
});
|
|
589
405
|
|
|
590
406
|
it("writes both resources of a Following split", async () => {
|
|
591
|
-
const
|
|
592
|
-
const deps = eventDeps(
|
|
593
|
-
const [calendar] = await listCalendarsFor(deps,
|
|
407
|
+
const account = anAccount();
|
|
408
|
+
const deps = account.eventDeps();
|
|
409
|
+
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
594
410
|
assert.ok(calendar);
|
|
595
|
-
const event = await seedWeekly(
|
|
411
|
+
const event = await seedWeekly(
|
|
412
|
+
deps,
|
|
413
|
+
account.accountConfigId,
|
|
414
|
+
calendar.calendarId,
|
|
415
|
+
);
|
|
596
416
|
|
|
597
417
|
const split = await updateCalendarEventFor(
|
|
598
418
|
deps,
|
|
599
|
-
|
|
419
|
+
account.accountConfigId,
|
|
600
420
|
{
|
|
601
421
|
calendarId: calendar.calendarId,
|
|
602
422
|
calendarObjectId: event.calendarObjectId,
|
|
@@ -608,10 +428,11 @@ describe("updateCalendarEventFor", () => {
|
|
|
608
428
|
);
|
|
609
429
|
|
|
610
430
|
assert.ok(split.ok, JSON.stringify(split));
|
|
611
|
-
|
|
612
|
-
const [head, tail] = [...store.objects.values()].sort((left, right) =>
|
|
431
|
+
const objects = (await account.objects()).sort((left, right) =>
|
|
613
432
|
left.dtStart.localeCompare(right.dtStart),
|
|
614
433
|
);
|
|
434
|
+
assert.equal(objects.length, 2);
|
|
435
|
+
const [head, tail] = objects;
|
|
615
436
|
assert.equal(head?.summary, "Stand-up");
|
|
616
437
|
assert.equal(tail?.summary, "Stand-up (new format)");
|
|
617
438
|
assert.notEqual(head?.icalUid, tail?.icalUid);
|
|
@@ -624,14 +445,14 @@ describe("updateCalendarEventFor", () => {
|
|
|
624
445
|
});
|
|
625
446
|
|
|
626
447
|
it("answers not-found for an event the calendar does not hold", async () => {
|
|
627
|
-
const
|
|
628
|
-
const deps = eventDeps(
|
|
629
|
-
const [calendar] = await listCalendarsFor(deps,
|
|
448
|
+
const account = anAccount();
|
|
449
|
+
const deps = account.eventDeps();
|
|
450
|
+
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
630
451
|
assert.ok(calendar);
|
|
631
452
|
|
|
632
453
|
const updated = await updateCalendarEventFor(
|
|
633
454
|
deps,
|
|
634
|
-
|
|
455
|
+
account.accountConfigId,
|
|
635
456
|
{
|
|
636
457
|
calendarId: calendar.calendarId,
|
|
637
458
|
calendarObjectId: "absent",
|
|
@@ -649,44 +470,60 @@ describe("updateCalendarEventFor", () => {
|
|
|
649
470
|
|
|
650
471
|
describe("deleteCalendarEventFor", () => {
|
|
651
472
|
it("removes the resource and its occurrences under scope=All", async () => {
|
|
652
|
-
const
|
|
653
|
-
const deps = eventDeps(
|
|
654
|
-
const [calendar] = await listCalendarsFor(deps,
|
|
473
|
+
const account = anAccount();
|
|
474
|
+
const deps = account.eventDeps();
|
|
475
|
+
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
655
476
|
assert.ok(calendar);
|
|
656
|
-
const event = await seedWeekly(
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
477
|
+
const event = await seedWeekly(
|
|
478
|
+
deps,
|
|
479
|
+
account.accountConfigId,
|
|
480
|
+
calendar.calendarId,
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
const removed = await deleteCalendarEventFor(
|
|
484
|
+
deps,
|
|
485
|
+
account.accountConfigId,
|
|
486
|
+
{
|
|
487
|
+
calendarId: calendar.calendarId,
|
|
488
|
+
calendarObjectId: event.calendarObjectId,
|
|
489
|
+
scope: RecurrenceScope.All,
|
|
490
|
+
recurrenceId: "",
|
|
491
|
+
ifMatch: undefined,
|
|
492
|
+
},
|
|
493
|
+
);
|
|
665
494
|
|
|
666
495
|
assert.ok(removed.ok);
|
|
667
|
-
assert.
|
|
668
|
-
assert.
|
|
496
|
+
assert.deepEqual(await account.objects(), []);
|
|
497
|
+
assert.deepEqual(await account.occurrences(event), []);
|
|
669
498
|
});
|
|
670
499
|
|
|
671
500
|
it("keeps the series under scope=This and drops one occurrence from it", async () => {
|
|
672
|
-
const
|
|
673
|
-
const deps = eventDeps(
|
|
674
|
-
const [calendar] = await listCalendarsFor(deps,
|
|
501
|
+
const account = anAccount();
|
|
502
|
+
const deps = account.eventDeps();
|
|
503
|
+
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
675
504
|
assert.ok(calendar);
|
|
676
|
-
const event = await seedWeekly(
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
505
|
+
const event = await seedWeekly(
|
|
506
|
+
deps,
|
|
507
|
+
account.accountConfigId,
|
|
508
|
+
calendar.calendarId,
|
|
509
|
+
);
|
|
510
|
+
assert.equal((await account.occurrences(event)).length, 5);
|
|
511
|
+
|
|
512
|
+
const removed = await deleteCalendarEventFor(
|
|
513
|
+
deps,
|
|
514
|
+
account.accountConfigId,
|
|
515
|
+
{
|
|
516
|
+
calendarId: calendar.calendarId,
|
|
517
|
+
calendarObjectId: event.calendarObjectId,
|
|
518
|
+
scope: RecurrenceScope.This,
|
|
519
|
+
recurrenceId: "2026-09-21T09:00:00Z",
|
|
520
|
+
ifMatch: undefined,
|
|
521
|
+
},
|
|
522
|
+
);
|
|
686
523
|
|
|
687
524
|
assert.ok(removed.ok);
|
|
688
|
-
assert.equal(
|
|
689
|
-
const rows =
|
|
525
|
+
assert.equal((await account.objects()).length, 1);
|
|
526
|
+
const rows = await account.occurrences(event);
|
|
690
527
|
assert.equal(rows.length, 4);
|
|
691
528
|
assert.equal(
|
|
692
529
|
rows.some((row) => row.startAt === "2026-09-21T09:00:00Z"),
|
|
@@ -695,58 +532,75 @@ describe("deleteCalendarEventFor", () => {
|
|
|
695
532
|
});
|
|
696
533
|
|
|
697
534
|
it("refuses a per-occurrence delete of an event that happens once", async () => {
|
|
698
|
-
const
|
|
699
|
-
const deps = eventDeps(
|
|
700
|
-
const [calendar] = await listCalendarsFor(deps,
|
|
535
|
+
const account = anAccount();
|
|
536
|
+
const deps = account.eventDeps();
|
|
537
|
+
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
701
538
|
assert.ok(calendar);
|
|
702
|
-
const event = await seedWeekly(
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
539
|
+
const event = await seedWeekly(
|
|
540
|
+
deps,
|
|
541
|
+
account.accountConfigId,
|
|
542
|
+
calendar.calendarId,
|
|
543
|
+
"",
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
const removed = await deleteCalendarEventFor(
|
|
547
|
+
deps,
|
|
548
|
+
account.accountConfigId,
|
|
549
|
+
{
|
|
550
|
+
calendarId: calendar.calendarId,
|
|
551
|
+
calendarObjectId: event.calendarObjectId,
|
|
552
|
+
scope: RecurrenceScope.This,
|
|
553
|
+
recurrenceId: "2026-09-07T09:00:00Z",
|
|
554
|
+
ifMatch: undefined,
|
|
555
|
+
},
|
|
556
|
+
);
|
|
711
557
|
|
|
712
558
|
assert.ok(!removed.ok);
|
|
713
559
|
assert.equal(removed.error.code, "NotRecurring");
|
|
714
|
-
assert.equal(
|
|
560
|
+
assert.equal((await account.objects()).length, 1);
|
|
715
561
|
});
|
|
716
562
|
});
|
|
717
563
|
|
|
718
564
|
describe("createCalendarEventFor", () => {
|
|
719
565
|
it("refuses an event aimed at a calendar the account does not hold", async () => {
|
|
720
|
-
const
|
|
721
|
-
const deps = eventDeps(
|
|
722
|
-
|
|
723
|
-
const created = await createCalendarEventFor(
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
566
|
+
const account = anAccount();
|
|
567
|
+
const deps = account.eventDeps();
|
|
568
|
+
|
|
569
|
+
const created = await createCalendarEventFor(
|
|
570
|
+
deps,
|
|
571
|
+
account.accountConfigId,
|
|
572
|
+
{
|
|
573
|
+
calendarId: "someone-elses-calendar",
|
|
574
|
+
summary: "Stand-up",
|
|
575
|
+
start: "2026-09-07T09:00:00Z",
|
|
576
|
+
end: "2026-09-07T10:00:00Z",
|
|
577
|
+
},
|
|
578
|
+
);
|
|
729
579
|
|
|
730
580
|
assert.ok(!created.ok);
|
|
731
581
|
assert.equal(created.error.code, "NotFound");
|
|
732
582
|
});
|
|
733
583
|
|
|
734
584
|
it("refuses an event that ends before it starts", async () => {
|
|
735
|
-
const
|
|
736
|
-
const deps = eventDeps(
|
|
737
|
-
const [calendar] = await listCalendarsFor(deps,
|
|
585
|
+
const account = anAccount();
|
|
586
|
+
const deps = account.eventDeps();
|
|
587
|
+
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
738
588
|
assert.ok(calendar);
|
|
739
589
|
|
|
740
|
-
const created = await createCalendarEventFor(
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
590
|
+
const created = await createCalendarEventFor(
|
|
591
|
+
deps,
|
|
592
|
+
account.accountConfigId,
|
|
593
|
+
{
|
|
594
|
+
calendarId: calendar.calendarId,
|
|
595
|
+
summary: "Backwards",
|
|
596
|
+
start: "2026-09-07T10:00:00Z",
|
|
597
|
+
end: "2026-09-07T09:00:00Z",
|
|
598
|
+
},
|
|
599
|
+
);
|
|
746
600
|
|
|
747
601
|
assert.ok(!created.ok);
|
|
748
602
|
assert.equal(created.error.code, "BackwardsEnd");
|
|
749
|
-
assert.
|
|
603
|
+
assert.deepEqual(await account.objects(), []);
|
|
750
604
|
});
|
|
751
605
|
});
|
|
752
606
|
|
|
@@ -792,53 +646,50 @@ describe("updateCalendarFor", () => {
|
|
|
792
646
|
};
|
|
793
647
|
|
|
794
648
|
it("refuses a timezone this server cannot resolve", async () => {
|
|
795
|
-
const
|
|
796
|
-
const deps =
|
|
797
|
-
const [calendar] = await listCalendarsFor(deps,
|
|
649
|
+
const account = anAccount();
|
|
650
|
+
const deps = account.deps();
|
|
651
|
+
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
798
652
|
assert.ok(calendar);
|
|
799
653
|
|
|
800
654
|
const updated = await updateCalendarFor(
|
|
801
655
|
deps,
|
|
802
|
-
|
|
656
|
+
account.accountConfigId,
|
|
803
657
|
calendar.calendarId,
|
|
804
|
-
{
|
|
805
|
-
timezone: "Pacific Standard Time",
|
|
806
|
-
},
|
|
658
|
+
{ timezone: "Pacific Standard Time" },
|
|
807
659
|
);
|
|
808
660
|
|
|
809
661
|
assert.ok(!updated.ok);
|
|
810
662
|
assert.equal(updated.error.code, "UnknownTimeZone");
|
|
811
|
-
assert.equal(
|
|
663
|
+
assert.equal((await account.collection(calendar.calendarId))?.timezone, "");
|
|
812
664
|
});
|
|
813
665
|
|
|
814
666
|
it("re-expands the calendar's events when its timezone changes", async () => {
|
|
815
|
-
const
|
|
816
|
-
const deps = eventDeps(
|
|
817
|
-
const [calendar] = await listCalendarsFor(deps,
|
|
667
|
+
const account = anAccount();
|
|
668
|
+
const deps = account.eventDeps();
|
|
669
|
+
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
818
670
|
assert.ok(calendar);
|
|
819
|
-
const created = await createCalendarEventFor(
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
671
|
+
const created = await createCalendarEventFor(
|
|
672
|
+
deps,
|
|
673
|
+
account.accountConfigId,
|
|
674
|
+
{ calendarId: calendar.calendarId, ...allDay },
|
|
675
|
+
);
|
|
823
676
|
assert.ok(created.ok, JSON.stringify(created));
|
|
824
677
|
assert.equal(
|
|
825
|
-
|
|
678
|
+
(await account.occurrences(created.value))[0]?.startAt,
|
|
826
679
|
"2026-06-01T00:00:00Z",
|
|
827
680
|
"an all-day event in a calendar with no zone starts at midnight UTC",
|
|
828
681
|
);
|
|
829
682
|
|
|
830
683
|
const updated = await updateCalendarFor(
|
|
831
684
|
deps,
|
|
832
|
-
|
|
685
|
+
account.accountConfigId,
|
|
833
686
|
calendar.calendarId,
|
|
834
|
-
{
|
|
835
|
-
timezone: "America/New_York",
|
|
836
|
-
},
|
|
687
|
+
{ timezone: "America/New_York" },
|
|
837
688
|
);
|
|
838
689
|
|
|
839
690
|
assert.ok(updated.ok, JSON.stringify(updated));
|
|
840
691
|
assert.equal(
|
|
841
|
-
|
|
692
|
+
(await account.occurrences(created.value))[0]?.startAt,
|
|
842
693
|
"2026-06-01T04:00:00Z",
|
|
843
694
|
"and midnight in the calendar's new zone once it has one",
|
|
844
695
|
);
|
|
@@ -849,24 +700,24 @@ describe("updateCalendarFor", () => {
|
|
|
849
700
|
});
|
|
850
701
|
|
|
851
702
|
it("leaves the events alone when only the name changes", async () => {
|
|
852
|
-
const
|
|
853
|
-
const deps = eventDeps(
|
|
854
|
-
const [calendar] = await listCalendarsFor(deps,
|
|
703
|
+
const account = anAccount();
|
|
704
|
+
const deps = account.eventDeps();
|
|
705
|
+
const [calendar] = await listCalendarsFor(deps, account.accountConfigId);
|
|
855
706
|
assert.ok(calendar);
|
|
856
|
-
const created = await createCalendarEventFor(
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
707
|
+
const created = await createCalendarEventFor(
|
|
708
|
+
deps,
|
|
709
|
+
account.accountConfigId,
|
|
710
|
+
{ calendarId: calendar.calendarId, ...allDay },
|
|
711
|
+
);
|
|
860
712
|
assert.ok(created.ok);
|
|
861
|
-
const before =
|
|
713
|
+
const before = (await account.collection(calendar.calendarId))
|
|
714
|
+
?.syncSequence;
|
|
862
715
|
|
|
863
716
|
const updated = await updateCalendarFor(
|
|
864
717
|
deps,
|
|
865
|
-
|
|
718
|
+
account.accountConfigId,
|
|
866
719
|
calendar.calendarId,
|
|
867
|
-
{
|
|
868
|
-
displayName: "Renamed",
|
|
869
|
-
},
|
|
720
|
+
{ displayName: "Renamed" },
|
|
870
721
|
);
|
|
871
722
|
|
|
872
723
|
assert.ok(updated.ok);
|
|
@@ -874,3 +725,119 @@ describe("updateCalendarFor", () => {
|
|
|
874
725
|
assert.equal(updated.value.syncSequence, before);
|
|
875
726
|
});
|
|
876
727
|
});
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* The collection wrappers, driven the way an HTTP request drives them. The
|
|
731
|
+
* suites above hold the inner functions; these hold what the API answers with.
|
|
732
|
+
*/
|
|
733
|
+
|
|
734
|
+
type Handler = (
|
|
735
|
+
context: Context,
|
|
736
|
+
event: APIGatewayProxyEvent,
|
|
737
|
+
) => Promise<Record<string, unknown>>;
|
|
738
|
+
|
|
739
|
+
const createCalendar =
|
|
740
|
+
CalendarOperations.CalendarOperations_createCalendar as Handler;
|
|
741
|
+
const getCalendar =
|
|
742
|
+
CalendarDetailOperations.CalendarDetailOperations_getCalendar as Handler;
|
|
743
|
+
const updateCalendar =
|
|
744
|
+
CalendarDetailOperations.CalendarDetailOperations_updateCalendar as Handler;
|
|
745
|
+
const deleteCalendar =
|
|
746
|
+
CalendarDetailOperations.CalendarDetailOperations_deleteCalendar as Handler;
|
|
747
|
+
|
|
748
|
+
const contextOf = (request: {
|
|
749
|
+
params?: Record<string, string>;
|
|
750
|
+
requestBody?: unknown;
|
|
751
|
+
}): Context => ({ request }) as unknown as Context;
|
|
752
|
+
|
|
753
|
+
describe("the calendar collection wrappers", () => {
|
|
754
|
+
it("answers not-found for a collection on another account", async () => {
|
|
755
|
+
const stranger = anAccount();
|
|
756
|
+
const [theirs] = await listCalendarsFor(
|
|
757
|
+
stranger.deps(),
|
|
758
|
+
stranger.accountConfigId,
|
|
759
|
+
);
|
|
760
|
+
assert.ok(theirs);
|
|
761
|
+
const event = anAccount().request();
|
|
762
|
+
|
|
763
|
+
const read = await getCalendar(
|
|
764
|
+
contextOf({ params: { calendarId: theirs.calendarId } }),
|
|
765
|
+
event,
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
assert.equal(read.statusCode, 404);
|
|
769
|
+
assert.equal((read.body as { code: string }).code, "NotFound");
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it("refuses a second calendar under a segment the account already uses", async () => {
|
|
773
|
+
const event = anAccount().request();
|
|
774
|
+
await createCalendar(
|
|
775
|
+
contextOf({ requestBody: { urlSegment: "work", displayName: "Work" } }),
|
|
776
|
+
event,
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
const second = await createCalendar(
|
|
780
|
+
contextOf({
|
|
781
|
+
requestBody: { urlSegment: "Work", displayName: "Work again" },
|
|
782
|
+
}),
|
|
783
|
+
event,
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
assert.equal(second.statusCode, 400);
|
|
787
|
+
assert.equal((second.body as { code: string }).code, "UrlSegmentTaken");
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it("refuses a rename that names a zone this server cannot resolve", async () => {
|
|
791
|
+
const account = anAccount();
|
|
792
|
+
const event = account.request();
|
|
793
|
+
const [calendar] = await listCalendarsFor(
|
|
794
|
+
account.deps(),
|
|
795
|
+
account.accountConfigId,
|
|
796
|
+
);
|
|
797
|
+
assert.ok(calendar);
|
|
798
|
+
|
|
799
|
+
const updated = await updateCalendar(
|
|
800
|
+
contextOf({
|
|
801
|
+
params: { calendarId: calendar.calendarId },
|
|
802
|
+
requestBody: { timezone: "Pacific Standard Time" },
|
|
803
|
+
}),
|
|
804
|
+
event,
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
assert.equal(updated.statusCode, 400);
|
|
808
|
+
assert.equal((updated.body as { code: string }).code, "UnknownTimeZone");
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
it("answers 204 for a calendar it removed and 400 for the default one", async () => {
|
|
812
|
+
const account = anAccount();
|
|
813
|
+
const event = account.request();
|
|
814
|
+
const held = await listCalendarsFor(
|
|
815
|
+
account.deps(),
|
|
816
|
+
account.accountConfigId,
|
|
817
|
+
);
|
|
818
|
+
const fallback = held.find(
|
|
819
|
+
(collection) => collection.source === CalendarSource.Default,
|
|
820
|
+
);
|
|
821
|
+
assert.ok(fallback);
|
|
822
|
+
const created = (await createCalendar(
|
|
823
|
+
contextOf({ requestBody: { urlSegment: "work", displayName: "Work" } }),
|
|
824
|
+
event,
|
|
825
|
+
)) as unknown as { calendarId: string };
|
|
826
|
+
|
|
827
|
+
const removed = await deleteCalendar(
|
|
828
|
+
contextOf({ params: { calendarId: created.calendarId } }),
|
|
829
|
+
event,
|
|
830
|
+
);
|
|
831
|
+
const refused = await deleteCalendar(
|
|
832
|
+
contextOf({ params: { calendarId: fallback.calendarId } }),
|
|
833
|
+
event,
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
assert.equal(removed.statusCode, 204);
|
|
837
|
+
assert.equal(refused.statusCode, 400);
|
|
838
|
+
assert.equal(
|
|
839
|
+
(refused.body as { code: string }).code,
|
|
840
|
+
"DefaultCalendarUndeletable",
|
|
841
|
+
);
|
|
842
|
+
});
|
|
843
|
+
});
|