@remit/calendar-service 0.0.17 → 0.0.19
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/accept.d.ts +71 -0
- package/dist/accept.js +159 -0
- package/dist/build.d.ts +79 -0
- package/dist/build.js +196 -0
- package/dist/correct.d.ts +4 -0
- package/dist/correct.js +22 -0
- package/dist/errors.d.ts +56 -0
- package/dist/errors.js +1 -0
- package/dist/etag.d.ts +9 -0
- package/{src/etag.ts → dist/etag.js} +1 -3
- package/dist/expand.d.ts +73 -0
- package/dist/expand.js +188 -0
- package/dist/feed.d.ts +81 -0
- package/dist/feed.js +157 -0
- package/dist/fixtures.d.ts +19 -0
- package/dist/fixtures.js +37 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +15 -0
- package/dist/memory-store.d.ts +105 -0
- package/dist/memory-store.js +319 -0
- package/dist/parse.d.ts +39 -0
- package/dist/parse.js +87 -0
- package/dist/project.d.ts +30 -0
- package/dist/project.js +57 -0
- package/dist/put.d.ts +52 -0
- package/dist/put.js +78 -0
- package/dist/reopen.d.ts +14 -0
- package/dist/reopen.js +92 -0
- package/dist/scope.d.ts +70 -0
- package/dist/scope.js +687 -0
- package/dist/subscribe.d.ts +96 -0
- package/dist/subscribe.js +298 -0
- package/dist/suggest.d.ts +74 -0
- package/dist/suggest.js +138 -0
- package/dist/time.d.ts +76 -0
- package/dist/time.js +164 -0
- package/dist/window.d.ts +85 -0
- package/dist/window.js +161 -0
- package/package.json +40 -35
- package/src/accept.test.ts +0 -403
- package/src/accept.ts +0 -229
- package/src/build.test.ts +0 -113
- package/src/build.ts +0 -311
- package/src/correct.test.ts +0 -173
- package/src/correct.ts +0 -40
- package/src/errors.ts +0 -62
- package/src/etag.test.ts +0 -54
- package/src/expand.test.ts +0 -361
- package/src/expand.ts +0 -303
- package/src/feed.test.ts +0 -334
- package/src/feed.ts +0 -203
- package/src/feeds/google-secret-address.ics +0 -99
- package/src/fixtures.ts +0 -52
- package/src/index.ts +0 -125
- package/src/memory-store.ts +0 -404
- package/src/parse.test.ts +0 -264
- package/src/parse.ts +0 -138
- package/src/project.test.ts +0 -247
- package/src/project.ts +0 -122
- package/src/put.test.ts +0 -320
- package/src/put.ts +0 -123
- package/src/reopen.test.ts +0 -531
- package/src/reopen.ts +0 -190
- package/src/scope.test.ts +0 -1421
- package/src/scope.ts +0 -1003
- package/src/subscribe.test.ts +0 -578
- package/src/subscribe.ts +0 -420
- package/src/suggest.test.ts +0 -376
- package/src/suggest.ts +0 -244
- package/src/time.ts +0 -260
- package/src/window.test.ts +0 -648
- package/src/window.ts +0 -281
- package/tsconfig.json +0 -8
package/src/subscribe.test.ts
DELETED
|
@@ -1,578 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { describe, it } from "node:test";
|
|
4
|
-
import type { CalendarCollectionItem } from "@remit/data-ports";
|
|
5
|
-
import { CalendarSource } from "@remit/domain-enums";
|
|
6
|
-
import { ical } from "./fixtures.js";
|
|
7
|
-
import { MemoryCalendarStore } from "./memory-store.js";
|
|
8
|
-
import { parseCalendar } from "./parse.js";
|
|
9
|
-
import {
|
|
10
|
-
type CalendarFeedFetcher,
|
|
11
|
-
fetchCalendarFeed,
|
|
12
|
-
isSubscriptionDue,
|
|
13
|
-
readSubscriptionUrl,
|
|
14
|
-
refreshCalendarSubscription,
|
|
15
|
-
splitCalendarFeed,
|
|
16
|
-
writeCalendarFeed,
|
|
17
|
-
} from "./subscribe.js";
|
|
18
|
-
import { listCalendarInstances } from "./window.js";
|
|
19
|
-
|
|
20
|
-
const ACCOUNT_CONFIG_ID = "account-config-1";
|
|
21
|
-
const SECRET_URL =
|
|
22
|
-
"https://calendar.example/calendar/ical/harbour%40example.com/private-5f1c0ffee/basic.ics";
|
|
23
|
-
const GOOGLE_FEED = readFileSync(
|
|
24
|
-
new URL("./feeds/google-secret-address.ics", import.meta.url),
|
|
25
|
-
"utf8",
|
|
26
|
-
);
|
|
27
|
-
const WINDOW = {
|
|
28
|
-
from: "2026-10-01T00:00:00Z",
|
|
29
|
-
to: "2026-11-16T00:00:00Z",
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const answering =
|
|
33
|
-
(body: string, init: ResponseInit = {}): CalendarFeedFetcher =>
|
|
34
|
-
async () =>
|
|
35
|
-
new Response(body, init);
|
|
36
|
-
|
|
37
|
-
const subscribed = async (): Promise<{
|
|
38
|
-
store: MemoryCalendarStore;
|
|
39
|
-
collection: CalendarCollectionItem;
|
|
40
|
-
}> => {
|
|
41
|
-
const store = new MemoryCalendarStore();
|
|
42
|
-
const collection = await store.transaction((repos) =>
|
|
43
|
-
repos.calendarCollection.create({
|
|
44
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
45
|
-
urlSegment: "harbour-rota",
|
|
46
|
-
displayName: "Harbour rota",
|
|
47
|
-
source: CalendarSource.Subscribed,
|
|
48
|
-
subscriptionUrl: SECRET_URL,
|
|
49
|
-
subscriptionEnabled: true,
|
|
50
|
-
}),
|
|
51
|
-
);
|
|
52
|
-
return { store, collection };
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const drawn = async (
|
|
56
|
-
store: MemoryCalendarStore,
|
|
57
|
-
collection: CalendarCollectionItem,
|
|
58
|
-
) => {
|
|
59
|
-
const current = await store.transaction((repos) =>
|
|
60
|
-
repos.calendarCollection.get(ACCOUNT_CONFIG_ID, collection.calendarId),
|
|
61
|
-
);
|
|
62
|
-
const instances = await store.transaction((repos) =>
|
|
63
|
-
listCalendarInstances(repos, [current], WINDOW),
|
|
64
|
-
);
|
|
65
|
-
return instances.map((instance) => ({
|
|
66
|
-
summary: instance.summary,
|
|
67
|
-
startAt: instance.startAt,
|
|
68
|
-
allDay: instance.allDay,
|
|
69
|
-
status: instance.status,
|
|
70
|
-
}));
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const writeFeed = async (
|
|
74
|
-
store: MemoryCalendarStore,
|
|
75
|
-
collection: CalendarCollectionItem,
|
|
76
|
-
feed: string,
|
|
77
|
-
now: number,
|
|
78
|
-
) => {
|
|
79
|
-
const split = await splitCalendarFeed(feed);
|
|
80
|
-
assert.ok(split.ok);
|
|
81
|
-
return writeCalendarFeed(
|
|
82
|
-
store,
|
|
83
|
-
{
|
|
84
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
85
|
-
calendarId: collection.calendarId,
|
|
86
|
-
},
|
|
87
|
-
split.value,
|
|
88
|
-
now,
|
|
89
|
-
);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
describe("readSubscriptionUrl", () => {
|
|
93
|
-
it("reads webcal as the same address over https", () => {
|
|
94
|
-
const url = readSubscriptionUrl(
|
|
95
|
-
" webcal://calendar.example/feeds/rota.ics ",
|
|
96
|
-
);
|
|
97
|
-
assert.deepEqual(url, {
|
|
98
|
-
ok: true,
|
|
99
|
-
value: "https://calendar.example/feeds/rota.ics",
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("keeps http and https as given", () => {
|
|
104
|
-
assert.deepEqual(readSubscriptionUrl("http://127.0.0.1:8080/a.ics"), {
|
|
105
|
-
ok: true,
|
|
106
|
-
value: "http://127.0.0.1:8080/a.ics",
|
|
107
|
-
});
|
|
108
|
-
assert.deepEqual(readSubscriptionUrl(SECRET_URL), {
|
|
109
|
-
ok: true,
|
|
110
|
-
value: SECRET_URL,
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("refuses what is not a web address", () => {
|
|
115
|
-
for (const raw of [
|
|
116
|
-
"",
|
|
117
|
-
"calendar.example/rota.ics",
|
|
118
|
-
"ftp://calendar.example/rota.ics",
|
|
119
|
-
"file:///etc/passwd",
|
|
120
|
-
"https://user:secret@calendar.example/rota.ics",
|
|
121
|
-
]) {
|
|
122
|
-
const url = readSubscriptionUrl(raw);
|
|
123
|
-
assert.equal(url.ok, false, raw);
|
|
124
|
-
if (!url.ok) assert.equal(url.error.code, "InvalidSubscriptionUrl");
|
|
125
|
-
}
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe("fetchCalendarFeed", () => {
|
|
130
|
-
it("answers the feed's text", async () => {
|
|
131
|
-
const fetched = await fetchCalendarFeed(
|
|
132
|
-
SECRET_URL,
|
|
133
|
-
answering(GOOGLE_FEED, { status: 200 }),
|
|
134
|
-
);
|
|
135
|
-
assert.deepEqual(fetched, { ok: true, value: GOOGLE_FEED });
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("names the status of a refusal and never the address", async () => {
|
|
139
|
-
const fetched = await fetchCalendarFeed(
|
|
140
|
-
SECRET_URL,
|
|
141
|
-
answering("gone", { status: 404 }),
|
|
142
|
-
);
|
|
143
|
-
assert.deepEqual(fetched, {
|
|
144
|
-
ok: false,
|
|
145
|
-
reason: "the feed answered HTTP 404",
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("names why an address could not be reached, without the address", async () => {
|
|
150
|
-
const fetched = await fetchCalendarFeed(SECRET_URL, async (url) => {
|
|
151
|
-
throw new TypeError("fetch failed", {
|
|
152
|
-
cause: Object.assign(new Error(`connect ECONNREFUSED ${url}`), {
|
|
153
|
-
code: "ECONNREFUSED",
|
|
154
|
-
}),
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
assert.deepEqual(fetched, {
|
|
158
|
-
ok: false,
|
|
159
|
-
reason: "the feed could not be reached (ECONNREFUSED)",
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("says so when the feed does not answer in time", async () => {
|
|
164
|
-
const fetched = await fetchCalendarFeed(SECRET_URL, async () => {
|
|
165
|
-
throw new DOMException("timed out", "TimeoutError");
|
|
166
|
-
});
|
|
167
|
-
assert.deepEqual(fetched, {
|
|
168
|
-
ok: false,
|
|
169
|
-
reason: "the feed did not answer within 30 seconds",
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("stops reading a chunked feed once it passes the cap", async () => {
|
|
174
|
-
let pulled = 0;
|
|
175
|
-
let cancelled = false;
|
|
176
|
-
const chunk = new Uint8Array(1024 * 1024).fill(0x41);
|
|
177
|
-
const endless = new ReadableStream<Uint8Array>({
|
|
178
|
-
pull(controller) {
|
|
179
|
-
pulled += 1;
|
|
180
|
-
controller.enqueue(chunk);
|
|
181
|
-
},
|
|
182
|
-
cancel() {
|
|
183
|
-
cancelled = true;
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const fetched = await fetchCalendarFeed(SECRET_URL, async () => {
|
|
188
|
-
const response = new Response(endless, { status: 200 });
|
|
189
|
-
assert.equal(response.headers.get("content-length"), null);
|
|
190
|
-
return response;
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
assert.deepEqual(fetched, {
|
|
194
|
-
ok: false,
|
|
195
|
-
reason: "the feed is larger than 10 MB",
|
|
196
|
-
});
|
|
197
|
-
assert.equal(cancelled, true);
|
|
198
|
-
assert.ok(pulled <= 12, `read ${pulled} MB of a feed capped at 10`);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("refuses a feed larger than it will store", async () => {
|
|
202
|
-
const fetched = await fetchCalendarFeed(
|
|
203
|
-
SECRET_URL,
|
|
204
|
-
answering("x", {
|
|
205
|
-
status: 200,
|
|
206
|
-
headers: { "content-length": String(64 * 1024 * 1024) },
|
|
207
|
-
}),
|
|
208
|
-
);
|
|
209
|
-
assert.deepEqual(fetched, {
|
|
210
|
-
ok: false,
|
|
211
|
-
reason: "the feed is larger than 10 MB",
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
describe("splitCalendarFeed", () => {
|
|
217
|
-
it("cuts a feed into one resource per UID, each one the write path accepts", async () => {
|
|
218
|
-
const split = await splitCalendarFeed(GOOGLE_FEED);
|
|
219
|
-
assert.ok(split.ok);
|
|
220
|
-
assert.equal(split.value.unreadable, 0);
|
|
221
|
-
assert.deepEqual(
|
|
222
|
-
split.value.resources.map((resource) => resource.uid),
|
|
223
|
-
[
|
|
224
|
-
"pilot-standup@google.com",
|
|
225
|
-
"quay-offsite@google.com",
|
|
226
|
-
"tide-review@google.com",
|
|
227
|
-
],
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
for (const resource of split.value.resources) {
|
|
231
|
-
assert.match(resource.resourceName, /^[0-9a-f]{40}\.ics$/);
|
|
232
|
-
const parsed = await parseCalendar(resource.icalData);
|
|
233
|
-
assert.ok(parsed.ok, resource.uid);
|
|
234
|
-
assert.equal(parsed.value.uid, resource.uid);
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it("keeps a series with its overrides and only the zones it names", async () => {
|
|
239
|
-
const split = await splitCalendarFeed(GOOGLE_FEED);
|
|
240
|
-
assert.ok(split.ok);
|
|
241
|
-
const standup = split.value.resources[0];
|
|
242
|
-
assert.ok(standup);
|
|
243
|
-
|
|
244
|
-
const parsed = await parseCalendar(standup.icalData);
|
|
245
|
-
assert.ok(parsed.ok);
|
|
246
|
-
assert.equal(parsed.value.overrides.length, 2);
|
|
247
|
-
assert.deepEqual(
|
|
248
|
-
parsed.value.component
|
|
249
|
-
.getAllSubcomponents("vtimezone")
|
|
250
|
-
.map((zone) => zone.getFirstPropertyValue("tzid")),
|
|
251
|
-
["Europe/Amsterdam"],
|
|
252
|
-
);
|
|
253
|
-
assert.equal(parsed.value.component.hasProperty("method"), false);
|
|
254
|
-
assert.equal(
|
|
255
|
-
parsed.value.component.getFirstPropertyValue("x-wr-calname"),
|
|
256
|
-
"Harbour rota",
|
|
257
|
-
);
|
|
258
|
-
|
|
259
|
-
const offsite = split.value.resources[1];
|
|
260
|
-
assert.ok(offsite);
|
|
261
|
-
assert.doesNotMatch(offsite.icalData, /BEGIN:VTIMEZONE/);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it("names the same resource on every read of the same feed", async () => {
|
|
265
|
-
const first = await splitCalendarFeed(GOOGLE_FEED);
|
|
266
|
-
const second = await splitCalendarFeed(GOOGLE_FEED);
|
|
267
|
-
assert.ok(first.ok && second.ok);
|
|
268
|
-
assert.deepEqual(first.value.resources, second.value.resources);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("counts an event with no UID instead of storing it", async () => {
|
|
272
|
-
const split = await splitCalendarFeed(
|
|
273
|
-
ical(
|
|
274
|
-
"BEGIN:VCALENDAR",
|
|
275
|
-
"VERSION:2.0",
|
|
276
|
-
"PRODID:-//Example//Feed//EN",
|
|
277
|
-
"BEGIN:VEVENT",
|
|
278
|
-
"DTSTART:20261008T140000Z",
|
|
279
|
-
"SUMMARY:Nobody's event",
|
|
280
|
-
"END:VEVENT",
|
|
281
|
-
"END:VCALENDAR",
|
|
282
|
-
),
|
|
283
|
-
);
|
|
284
|
-
assert.ok(split.ok);
|
|
285
|
-
assert.equal(split.value.resources.length, 0);
|
|
286
|
-
assert.equal(split.value.unreadable, 1);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("refuses bytes that are not a calendar", async () => {
|
|
290
|
-
const split = await splitCalendarFeed(
|
|
291
|
-
"<!doctype html><title>Sign in</title>",
|
|
292
|
-
);
|
|
293
|
-
assert.equal(split.ok, false);
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
describe("writeCalendarFeed", () => {
|
|
298
|
-
it("keeps recurrence, a moved and a cancelled instance, all-day and zoned times", async () => {
|
|
299
|
-
const { store, collection } = await subscribed();
|
|
300
|
-
|
|
301
|
-
const write = await writeFeed(store, collection, GOOGLE_FEED, 5_000);
|
|
302
|
-
|
|
303
|
-
assert.deepEqual(write, {
|
|
304
|
-
paused: false,
|
|
305
|
-
written: 3,
|
|
306
|
-
unchanged: 0,
|
|
307
|
-
removed: 0,
|
|
308
|
-
skipped: 0,
|
|
309
|
-
});
|
|
310
|
-
assert.deepEqual(await drawn(store, collection), [
|
|
311
|
-
{
|
|
312
|
-
summary: "Pilot standup",
|
|
313
|
-
startAt: "2026-10-05T07:00:00Z",
|
|
314
|
-
allDay: false,
|
|
315
|
-
status: "Confirmed",
|
|
316
|
-
},
|
|
317
|
-
{
|
|
318
|
-
summary: "Quay offsite",
|
|
319
|
-
startAt: "2026-10-07T00:00:00Z",
|
|
320
|
-
allDay: true,
|
|
321
|
-
status: "Confirmed",
|
|
322
|
-
},
|
|
323
|
-
{
|
|
324
|
-
summary: "Tide table review",
|
|
325
|
-
startAt: "2026-10-08T14:00:00Z",
|
|
326
|
-
allDay: false,
|
|
327
|
-
status: "Confirmed",
|
|
328
|
-
},
|
|
329
|
-
{
|
|
330
|
-
summary: "Pilot standup (moved)",
|
|
331
|
-
startAt: "2026-10-12T08:00:00Z",
|
|
332
|
-
allDay: false,
|
|
333
|
-
status: "Confirmed",
|
|
334
|
-
},
|
|
335
|
-
{
|
|
336
|
-
summary: "Pilot standup",
|
|
337
|
-
startAt: "2026-10-26T08:00:00Z",
|
|
338
|
-
allDay: false,
|
|
339
|
-
status: "Confirmed",
|
|
340
|
-
},
|
|
341
|
-
{
|
|
342
|
-
summary: "Pilot standup",
|
|
343
|
-
startAt: "2026-11-02T08:00:00Z",
|
|
344
|
-
allDay: false,
|
|
345
|
-
status: "Cancelled",
|
|
346
|
-
},
|
|
347
|
-
{
|
|
348
|
-
summary: "Pilot standup",
|
|
349
|
-
startAt: "2026-11-09T08:00:00Z",
|
|
350
|
-
allDay: false,
|
|
351
|
-
status: "Confirmed",
|
|
352
|
-
},
|
|
353
|
-
]);
|
|
354
|
-
|
|
355
|
-
const stored = await store.transaction((repos) =>
|
|
356
|
-
repos.calendarCollection.get(ACCOUNT_CONFIG_ID, collection.calendarId),
|
|
357
|
-
);
|
|
358
|
-
assert.equal(stored.subscriptionCheckedAt, 5_000);
|
|
359
|
-
assert.equal(stored.subscriptionFetchedAt, 5_000);
|
|
360
|
-
assert.equal(stored.subscriptionError, "");
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it("leaves unchanged events alone and removes what the feed dropped", async () => {
|
|
364
|
-
const { store, collection } = await subscribed();
|
|
365
|
-
await writeFeed(store, collection, GOOGLE_FEED, 5_000);
|
|
366
|
-
const sequenceBefore = (
|
|
367
|
-
await store.transaction((repos) =>
|
|
368
|
-
repos.calendarCollection.get(ACCOUNT_CONFIG_ID, collection.calendarId),
|
|
369
|
-
)
|
|
370
|
-
).syncSequence;
|
|
371
|
-
|
|
372
|
-
const dropped = GOOGLE_FEED.replace(
|
|
373
|
-
/BEGIN:VEVENT\r\nDTSTART:20261008T140000Z[\s\S]*?END:VEVENT\r\n/,
|
|
374
|
-
"",
|
|
375
|
-
);
|
|
376
|
-
const write = await writeFeed(store, collection, dropped, 9_000);
|
|
377
|
-
|
|
378
|
-
assert.deepEqual(write, {
|
|
379
|
-
paused: false,
|
|
380
|
-
written: 0,
|
|
381
|
-
unchanged: 2,
|
|
382
|
-
removed: 1,
|
|
383
|
-
skipped: 0,
|
|
384
|
-
});
|
|
385
|
-
const summaries = (await drawn(store, collection)).map(
|
|
386
|
-
(instance) => instance.summary,
|
|
387
|
-
);
|
|
388
|
-
assert.equal(summaries.includes("Tide table review"), false);
|
|
389
|
-
assert.equal(summaries.length, 6);
|
|
390
|
-
const after = await store.transaction((repos) =>
|
|
391
|
-
repos.calendarCollection.get(ACCOUNT_CONFIG_ID, collection.calendarId),
|
|
392
|
-
);
|
|
393
|
-
assert.equal(after.syncSequence, sequenceBefore + 1);
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it("writes nothing when a second fetch differs only in its export stamps", async () => {
|
|
397
|
-
const { store, collection } = await subscribed();
|
|
398
|
-
await writeFeed(store, collection, GOOGLE_FEED, 5_000);
|
|
399
|
-
const before = await store.transaction((repos) =>
|
|
400
|
-
repos.calendarCollection.get(ACCOUNT_CONFIG_ID, collection.calendarId),
|
|
401
|
-
);
|
|
402
|
-
|
|
403
|
-
const restamped = GOOGLE_FEED.replaceAll(
|
|
404
|
-
"DTSTAMP:20260925T120000Z",
|
|
405
|
-
"DTSTAMP:20260926T081500Z",
|
|
406
|
-
);
|
|
407
|
-
const write = await writeFeed(store, collection, restamped, 9_000);
|
|
408
|
-
|
|
409
|
-
assert.deepEqual(write, {
|
|
410
|
-
paused: false,
|
|
411
|
-
written: 0,
|
|
412
|
-
unchanged: 3,
|
|
413
|
-
removed: 0,
|
|
414
|
-
skipped: 0,
|
|
415
|
-
});
|
|
416
|
-
const after = await store.transaction((repos) =>
|
|
417
|
-
repos.calendarCollection.get(ACCOUNT_CONFIG_ID, collection.calendarId),
|
|
418
|
-
);
|
|
419
|
-
assert.equal(after.syncSequence, before.syncSequence);
|
|
420
|
-
assert.equal(after.subscriptionFetchedAt, 9_000);
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
it("writes nothing for a subscription paused while its feed was read", async () => {
|
|
424
|
-
const { store, collection } = await subscribed();
|
|
425
|
-
await store.transaction((repos) =>
|
|
426
|
-
repos.calendarCollection.update(
|
|
427
|
-
ACCOUNT_CONFIG_ID,
|
|
428
|
-
collection.calendarId,
|
|
429
|
-
{ subscriptionEnabled: false },
|
|
430
|
-
),
|
|
431
|
-
);
|
|
432
|
-
|
|
433
|
-
const write = await writeFeed(store, collection, GOOGLE_FEED, 5_000);
|
|
434
|
-
|
|
435
|
-
assert.deepEqual(write, {
|
|
436
|
-
paused: true,
|
|
437
|
-
written: 0,
|
|
438
|
-
unchanged: 0,
|
|
439
|
-
removed: 0,
|
|
440
|
-
skipped: 0,
|
|
441
|
-
});
|
|
442
|
-
assert.deepEqual(await drawn(store, collection), []);
|
|
443
|
-
const stored = await store.transaction((repos) =>
|
|
444
|
-
repos.calendarCollection.get(ACCOUNT_CONFIG_ID, collection.calendarId),
|
|
445
|
-
);
|
|
446
|
-
assert.equal(stored.subscriptionFetchedAt, 0);
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
it("records the events it could not store and keeps the rest", async () => {
|
|
450
|
-
const { store, collection } = await subscribed();
|
|
451
|
-
|
|
452
|
-
const write = await writeFeed(
|
|
453
|
-
store,
|
|
454
|
-
collection,
|
|
455
|
-
ical(
|
|
456
|
-
"BEGIN:VCALENDAR",
|
|
457
|
-
"VERSION:2.0",
|
|
458
|
-
"PRODID:-//Example//Feed//EN",
|
|
459
|
-
"BEGIN:VEVENT",
|
|
460
|
-
"UID:kept@example.com",
|
|
461
|
-
"DTSTART:20261008T140000Z",
|
|
462
|
-
"DTEND:20261008T150000Z",
|
|
463
|
-
"SUMMARY:Kept",
|
|
464
|
-
"END:VEVENT",
|
|
465
|
-
"BEGIN:VEVENT",
|
|
466
|
-
"UID:backwards@example.com",
|
|
467
|
-
"DTSTART:20261008T140000Z",
|
|
468
|
-
"DTEND:20261008T130000Z",
|
|
469
|
-
"SUMMARY:Backwards",
|
|
470
|
-
"END:VEVENT",
|
|
471
|
-
"BEGIN:VEVENT",
|
|
472
|
-
"DTSTART:20261009T140000Z",
|
|
473
|
-
"SUMMARY:No UID",
|
|
474
|
-
"END:VEVENT",
|
|
475
|
-
"END:VCALENDAR",
|
|
476
|
-
),
|
|
477
|
-
7_000,
|
|
478
|
-
);
|
|
479
|
-
|
|
480
|
-
assert.equal(write.written, 1);
|
|
481
|
-
assert.equal(write.skipped, 2);
|
|
482
|
-
const stored = await store.transaction((repos) =>
|
|
483
|
-
repos.calendarCollection.get(ACCOUNT_CONFIG_ID, collection.calendarId),
|
|
484
|
-
);
|
|
485
|
-
assert.equal(
|
|
486
|
-
stored.subscriptionError,
|
|
487
|
-
"2 events in the feed could not be stored",
|
|
488
|
-
);
|
|
489
|
-
assert.deepEqual(
|
|
490
|
-
(await drawn(store, collection)).map((instance) => instance.summary),
|
|
491
|
-
["Kept"],
|
|
492
|
-
);
|
|
493
|
-
});
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
describe("refreshCalendarSubscription", () => {
|
|
497
|
-
it("records a failed fetch on the collection and keeps every event", async () => {
|
|
498
|
-
const { store, collection } = await subscribed();
|
|
499
|
-
await writeFeed(store, collection, GOOGLE_FEED, 5_000);
|
|
500
|
-
|
|
501
|
-
const refreshed = await refreshCalendarSubscription(
|
|
502
|
-
store,
|
|
503
|
-
collection,
|
|
504
|
-
9_000,
|
|
505
|
-
answering("unavailable", { status: 503 }),
|
|
506
|
-
);
|
|
507
|
-
|
|
508
|
-
assert.deepEqual(refreshed, {
|
|
509
|
-
ok: false,
|
|
510
|
-
reason: "the feed answered HTTP 503",
|
|
511
|
-
});
|
|
512
|
-
const stored = await store.transaction((repos) =>
|
|
513
|
-
repos.calendarCollection.get(ACCOUNT_CONFIG_ID, collection.calendarId),
|
|
514
|
-
);
|
|
515
|
-
assert.equal(stored.subscriptionCheckedAt, 9_000);
|
|
516
|
-
assert.equal(stored.subscriptionFetchedAt, 5_000);
|
|
517
|
-
assert.equal(stored.subscriptionError, "the feed answered HTTP 503");
|
|
518
|
-
assert.equal(stored.subscriptionError.includes(SECRET_URL), false);
|
|
519
|
-
assert.equal((await drawn(store, collection)).length, 7);
|
|
520
|
-
});
|
|
521
|
-
|
|
522
|
-
it("states that a page which is not a calendar is not one", async () => {
|
|
523
|
-
const { store, collection } = await subscribed();
|
|
524
|
-
|
|
525
|
-
const refreshed = await refreshCalendarSubscription(
|
|
526
|
-
store,
|
|
527
|
-
collection,
|
|
528
|
-
9_000,
|
|
529
|
-
answering("<!doctype html><title>Sign in</title>", { status: 200 }),
|
|
530
|
-
);
|
|
531
|
-
|
|
532
|
-
assert.equal(refreshed.ok, false);
|
|
533
|
-
if (!refreshed.ok) {
|
|
534
|
-
assert.match(refreshed.reason, /^the feed is not a calendar: /);
|
|
535
|
-
}
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
it("fetches the collection's own address and clears an earlier failure", async () => {
|
|
539
|
-
const { store, collection } = await subscribed();
|
|
540
|
-
await refreshCalendarSubscription(
|
|
541
|
-
store,
|
|
542
|
-
collection,
|
|
543
|
-
3_000,
|
|
544
|
-
answering("", { status: 500 }),
|
|
545
|
-
);
|
|
546
|
-
const asked: string[] = [];
|
|
547
|
-
|
|
548
|
-
const refreshed = await refreshCalendarSubscription(
|
|
549
|
-
store,
|
|
550
|
-
collection,
|
|
551
|
-
6_000,
|
|
552
|
-
async (url) => {
|
|
553
|
-
asked.push(url);
|
|
554
|
-
return new Response(GOOGLE_FEED, { status: 200 });
|
|
555
|
-
},
|
|
556
|
-
);
|
|
557
|
-
|
|
558
|
-
assert.deepEqual(asked, [SECRET_URL]);
|
|
559
|
-
assert.ok(refreshed.ok);
|
|
560
|
-
const stored = await store.transaction((repos) =>
|
|
561
|
-
repos.calendarCollection.get(ACCOUNT_CONFIG_ID, collection.calendarId),
|
|
562
|
-
);
|
|
563
|
-
assert.equal(stored.subscriptionError, "");
|
|
564
|
-
assert.equal(stored.subscriptionFetchedAt, 6_000);
|
|
565
|
-
});
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
describe("isSubscriptionDue", () => {
|
|
569
|
-
it("is due once the last fetch is an interval old, failed or not", async () => {
|
|
570
|
-
const { collection } = await subscribed();
|
|
571
|
-
const checked = { ...collection, subscriptionCheckedAt: 10_000 };
|
|
572
|
-
|
|
573
|
-
assert.equal(isSubscriptionDue(checked, 10_000 + 899, 900), false);
|
|
574
|
-
assert.equal(isSubscriptionDue(checked, 10_000 + 900, 900), true);
|
|
575
|
-
assert.equal(isSubscriptionDue(collection, 1, 900), false);
|
|
576
|
-
assert.equal(isSubscriptionDue(collection, 900, 900), true);
|
|
577
|
-
});
|
|
578
|
-
});
|