@remit/data-ports 0.0.83 → 0.0.85

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 (174) hide show
  1. package/dist/account-settings.d.ts +371 -0
  2. package/dist/account-settings.js +114 -0
  3. package/dist/conformance/account.d.ts +3 -0
  4. package/dist/conformance/account.js +118 -0
  5. package/dist/conformance/calendar-collection.d.ts +3 -0
  6. package/dist/conformance/calendar-collection.js +251 -0
  7. package/dist/conformance/calendar-event-index.d.ts +3 -0
  8. package/dist/conformance/calendar-event-index.js +114 -0
  9. package/dist/conformance/calendar-feed-token.d.ts +3 -0
  10. package/dist/conformance/calendar-feed-token.js +97 -0
  11. package/dist/conformance/calendar-object.d.ts +3 -0
  12. package/dist/conformance/calendar-object.js +156 -0
  13. package/dist/conformance/calendar-suggestion.d.ts +3 -0
  14. package/dist/conformance/calendar-suggestion.js +255 -0
  15. package/{src/conformance/harness.ts → dist/conformance/harness.d.ts} +4 -4
  16. package/dist/conformance/harness.js +1 -0
  17. package/dist/conformance/index.js +8 -0
  18. package/dist/conformance/label.d.ts +3 -0
  19. package/dist/conformance/label.js +92 -0
  20. package/dist/conformance/sender-signer-standing.d.ts +3 -0
  21. package/dist/conformance/sender-signer-standing.js +81 -0
  22. package/dist/display-name.d.ts +1 -0
  23. package/dist/display-name.js +26 -0
  24. package/dist/errors.d.ts +150 -0
  25. package/dist/errors.js +181 -0
  26. package/dist/filter-ttl.d.ts +7 -0
  27. package/{src/filter-ttl.ts → dist/filter-ttl.js} +8 -11
  28. package/dist/folder-role.d.ts +239 -0
  29. package/dist/folder-role.js +276 -0
  30. package/dist/id.d.ts +124 -0
  31. package/{src/id.ts → dist/id.js} +41 -127
  32. package/dist/index.d.ts +37 -0
  33. package/dist/index.js +5 -0
  34. package/dist/interfaces/account-config.d.ts +15 -0
  35. package/dist/interfaces/account-config.js +1 -0
  36. package/dist/interfaces/account-export-request.d.ts +10 -0
  37. package/dist/interfaces/account-export-request.js +1 -0
  38. package/dist/interfaces/account-setting.d.ts +8 -0
  39. package/dist/interfaces/account-setting.js +1 -0
  40. package/dist/interfaces/account.d.ts +29 -0
  41. package/dist/interfaces/account.js +1 -0
  42. package/dist/interfaces/address.d.ts +70 -0
  43. package/dist/interfaces/address.js +1 -0
  44. package/dist/interfaces/calendar-collection.d.ts +38 -0
  45. package/dist/interfaces/calendar-collection.js +1 -0
  46. package/dist/interfaces/calendar-event-index.d.ts +19 -0
  47. package/dist/interfaces/calendar-event-index.js +1 -0
  48. package/dist/interfaces/calendar-feed-token.d.ts +31 -0
  49. package/dist/interfaces/calendar-feed-token.js +1 -0
  50. package/dist/interfaces/calendar-object.d.ts +36 -0
  51. package/dist/interfaces/calendar-object.js +1 -0
  52. package/dist/interfaces/calendar-store.d.ts +93 -0
  53. package/dist/interfaces/calendar-store.js +1 -0
  54. package/dist/interfaces/calendar-suggestion.d.ts +62 -0
  55. package/dist/interfaces/calendar-suggestion.js +1 -0
  56. package/dist/interfaces/config-import.d.ts +8 -0
  57. package/dist/interfaces/config-import.js +1 -0
  58. package/dist/interfaces/envelope.d.ts +24 -0
  59. package/dist/interfaces/envelope.js +1 -0
  60. package/dist/interfaces/filter.d.ts +28 -0
  61. package/dist/interfaces/filter.js +1 -0
  62. package/dist/interfaces/label.d.ts +17 -0
  63. package/dist/interfaces/label.js +1 -0
  64. package/dist/interfaces/mailbox-lock.d.ts +12 -0
  65. package/dist/interfaces/mailbox-lock.js +1 -0
  66. package/dist/interfaces/mailbox-special-use.d.ts +54 -0
  67. package/dist/interfaces/mailbox-special-use.js +1 -0
  68. package/dist/interfaces/mailbox.d.ts +79 -0
  69. package/dist/interfaces/mailbox.js +1 -0
  70. package/dist/interfaces/message-flag-push.d.ts +9 -0
  71. package/dist/interfaces/message-flag-push.js +1 -0
  72. package/dist/interfaces/message-flag.d.ts +14 -0
  73. package/dist/interfaces/message-flag.js +1 -0
  74. package/dist/interfaces/message-label.d.ts +21 -0
  75. package/dist/interfaces/message-label.js +1 -0
  76. package/dist/interfaces/message-placement-move.d.ts +8 -0
  77. package/dist/interfaces/message-placement-move.js +1 -0
  78. package/dist/interfaces/message.d.ts +60 -0
  79. package/dist/interfaces/message.js +1 -0
  80. package/dist/interfaces/organize-job-request.d.ts +10 -0
  81. package/dist/interfaces/organize-job-request.js +1 -0
  82. package/dist/interfaces/outbox-attachment.d.ts +101 -0
  83. package/dist/interfaces/outbox-attachment.js +7 -0
  84. package/dist/interfaces/outbox-message.d.ts +70 -0
  85. package/dist/interfaces/outbox-message.js +20 -0
  86. package/dist/interfaces/quarantine.d.ts +20 -0
  87. package/dist/interfaces/quarantine.js +1 -0
  88. package/dist/interfaces/sender-signer-standing.d.ts +26 -0
  89. package/dist/interfaces/sender-signer-standing.js +1 -0
  90. package/dist/interfaces/sync-warning.d.ts +11 -0
  91. package/dist/interfaces/sync-warning.js +1 -0
  92. package/dist/interfaces/thread-message.d.ts +202 -0
  93. package/dist/interfaces/thread-message.js +1 -0
  94. package/dist/mailbox-name.d.ts +35 -0
  95. package/dist/mailbox-name.js +68 -0
  96. package/dist/message-parts.d.ts +22 -0
  97. package/dist/message-parts.js +18 -0
  98. package/dist/message-settlement.d.ts +35 -0
  99. package/{src/message-settlement.ts → dist/message-settlement.js} +5 -19
  100. package/dist/mutation-events.d.ts +19 -0
  101. package/{src/mutation-events.ts → dist/mutation-events.js} +2 -5
  102. package/dist/sender-mute.d.ts +26 -0
  103. package/{src/sender-mute.ts → dist/sender-mute.js} +12 -34
  104. package/dist/sync-warning-state.d.ts +3 -0
  105. package/dist/sync-warning-state.js +5 -0
  106. package/dist/types.d.ts +491 -0
  107. package/dist/types.js +1 -0
  108. package/dist/update-manifest.d.ts +24 -0
  109. package/dist/update-manifest.js +20 -0
  110. package/dist/virtual-copy.d.ts +8 -0
  111. package/{src/virtual-copy.ts → dist/virtual-copy.js} +10 -15
  112. package/dist/wellknown.d.ts +47 -0
  113. package/{src/wellknown.ts → dist/wellknown.js} +26 -49
  114. package/package.json +44 -79
  115. package/src/account-settings.ts +0 -194
  116. package/src/conformance/account.ts +0 -104
  117. package/src/conformance/calendar-collection.ts +0 -342
  118. package/src/conformance/calendar-event-index.ts +0 -203
  119. package/src/conformance/calendar-feed-token.ts +0 -148
  120. package/src/conformance/calendar-object.ts +0 -269
  121. package/src/conformance/calendar-suggestion.ts +0 -490
  122. package/src/conformance/label.ts +0 -136
  123. package/src/conformance/sender-signer-standing.ts +0 -117
  124. package/src/display-name.test.ts +0 -101
  125. package/src/display-name.ts +0 -40
  126. package/src/errors.ts +0 -244
  127. package/src/folder-role.test.ts +0 -422
  128. package/src/folder-role.ts +0 -447
  129. package/src/id.test.ts +0 -352
  130. package/src/index.ts +0 -170
  131. package/src/interfaces/account-config.ts +0 -25
  132. package/src/interfaces/account-export-request.ts +0 -21
  133. package/src/interfaces/account-setting.ts +0 -14
  134. package/src/interfaces/account.ts +0 -43
  135. package/src/interfaces/address.ts +0 -122
  136. package/src/interfaces/calendar-collection.ts +0 -60
  137. package/src/interfaces/calendar-event-index.ts +0 -34
  138. package/src/interfaces/calendar-feed-token.ts +0 -38
  139. package/src/interfaces/calendar-object.ts +0 -58
  140. package/src/interfaces/calendar-suggestion.ts +0 -93
  141. package/src/interfaces/calendar-unit-of-work.ts +0 -47
  142. package/src/interfaces/config-import.ts +0 -16
  143. package/src/interfaces/envelope.ts +0 -32
  144. package/src/interfaces/filter-anchor-transaction.ts +0 -23
  145. package/src/interfaces/filter-anchor.ts +0 -11
  146. package/src/interfaces/filter.ts +0 -41
  147. package/src/interfaces/label.ts +0 -30
  148. package/src/interfaces/mailbox-lock.ts +0 -32
  149. package/src/interfaces/mailbox-special-use.ts +0 -60
  150. package/src/interfaces/mailbox.ts +0 -115
  151. package/src/interfaces/message-flag-push.ts +0 -17
  152. package/src/interfaces/message-flag.ts +0 -15
  153. package/src/interfaces/message-label.ts +0 -25
  154. package/src/interfaces/message-placement-move.ts +0 -15
  155. package/src/interfaces/message.ts +0 -65
  156. package/src/interfaces/organize-job-request.ts +0 -19
  157. package/src/interfaces/outbox-attachment.ts +0 -132
  158. package/src/interfaces/outbox-message.ts +0 -106
  159. package/src/interfaces/quarantine.ts +0 -22
  160. package/src/interfaces/sender-signer-standing.ts +0 -37
  161. package/src/interfaces/thread-message.ts +0 -264
  162. package/src/interfaces/unit-of-work.ts +0 -28
  163. package/src/mailbox-name.test.ts +0 -106
  164. package/src/mailbox-name.ts +0 -90
  165. package/src/mailbox-state-doors.test.ts +0 -104
  166. package/src/message-placement-doors.test.ts +0 -100
  167. package/src/message-settlement.test.ts +0 -132
  168. package/src/sender-mute.test.ts +0 -76
  169. package/src/types.ts +0 -820
  170. package/src/update-manifest.test.ts +0 -130
  171. package/src/update-manifest.ts +0 -22
  172. package/src/virtual-copy.test.ts +0 -34
  173. package/tsconfig.json +0 -8
  174. /package/{src/conformance/index.ts → dist/conformance/index.d.ts} +0 -0
@@ -0,0 +1,251 @@
1
+ import assert from "node:assert/strict";
2
+ import { after, before, describe, test } from "node:test";
3
+ import { CalendarColor, CalendarSource } from "@remit/domain-enums";
4
+ import { deriveCalendarId } from "../id.js";
5
+ export function calendarCollectionRepositoryConformance(harness) {
6
+ describe("ICalendarCollectionRepository conformance", () => {
7
+ let repo;
8
+ before(async () => {
9
+ repo = await harness.createRepository();
10
+ });
11
+ after(() => harness.teardown());
12
+ test("create derives calendarId from the account config and the url segment", async () => {
13
+ const accountConfigId = harness.makeId();
14
+ const calendar = await repo.create({
15
+ accountConfigId,
16
+ urlSegment: "default",
17
+ displayName: "Calendar",
18
+ });
19
+ assert.equal(calendar.calendarId, deriveCalendarId(accountConfigId, "default"));
20
+ assert.equal(calendar.color, CalendarColor.Cal1);
21
+ assert.equal(calendar.source, CalendarSource.UserCreated);
22
+ assert.equal(calendar.componentSet, "VeventOnly");
23
+ assert.equal(calendar.timezone, "");
24
+ assert.equal(calendar.syncSequence, 0);
25
+ });
26
+ test("create is idempotent, so first-use provisioning never forks a second collection", async () => {
27
+ const accountConfigId = harness.makeId();
28
+ const first = await repo.create({
29
+ accountConfigId,
30
+ urlSegment: "default",
31
+ displayName: "Calendar",
32
+ source: CalendarSource.Default,
33
+ });
34
+ const second = await repo.create({
35
+ accountConfigId,
36
+ urlSegment: "default",
37
+ displayName: "Calendar",
38
+ source: CalendarSource.Default,
39
+ });
40
+ assert.equal(second.calendarId, first.calendarId);
41
+ const all = await repo.listByAccountConfig(accountConfigId);
42
+ assert.equal(all.length, 1);
43
+ });
44
+ test("createExclusive refuses a segment the account already uses", async () => {
45
+ const accountConfigId = harness.makeId();
46
+ const first = await repo.createExclusive({
47
+ accountConfigId,
48
+ urlSegment: "work",
49
+ displayName: "Work",
50
+ });
51
+ const second = await repo.createExclusive({
52
+ accountConfigId,
53
+ urlSegment: "work",
54
+ displayName: "Work again",
55
+ });
56
+ assert.ok(first);
57
+ assert.equal(second, null);
58
+ assert.equal((await repo.get(accountConfigId, first.calendarId)).displayName, "Work", "the loser never overwrote the calendar the winner made");
59
+ });
60
+ test("get throws a not-found error for a missing collection", async () => {
61
+ await assert.rejects(repo.get(harness.makeId(), harness.makeId()), (error) => harness.isNotFoundError(error));
62
+ });
63
+ test("update changes the display overlay, never the key", async () => {
64
+ const accountConfigId = harness.makeId();
65
+ const calendar = await repo.create({
66
+ accountConfigId,
67
+ urlSegment: "work",
68
+ displayName: "Work",
69
+ });
70
+ const updated = await repo.update(accountConfigId, calendar.calendarId, {
71
+ displayName: "Work & Travel",
72
+ color: CalendarColor.Cal4,
73
+ timezone: "Europe/Amsterdam",
74
+ });
75
+ assert.equal(updated.calendarId, calendar.calendarId);
76
+ assert.equal(updated.urlSegment, "work");
77
+ assert.equal(updated.displayName, "Work & Travel");
78
+ assert.equal(updated.color, CalendarColor.Cal4);
79
+ assert.equal(updated.timezone, "Europe/Amsterdam");
80
+ });
81
+ test("update throws a not-found error for a missing collection", async () => {
82
+ await assert.rejects(repo.update(harness.makeId(), harness.makeId(), {
83
+ displayName: "x",
84
+ }), (error) => harness.isNotFoundError(error));
85
+ });
86
+ test("findByUrlSegment resolves the path segment a client bookmarked", async () => {
87
+ const accountConfigId = harness.makeId();
88
+ const calendar = await repo.create({
89
+ accountConfigId,
90
+ urlSegment: "personal",
91
+ displayName: "Personal",
92
+ });
93
+ const found = await repo.findByUrlSegment(accountConfigId, "personal");
94
+ assert.equal(found?.calendarId, calendar.calendarId);
95
+ assert.equal(await repo.findByUrlSegment(accountConfigId, "nonexistent"), null);
96
+ });
97
+ test("a segment stored in one casing is found in another, and by its derived id", async () => {
98
+ // The id derivation folds case; the stored column has to fold with it,
99
+ // or one DAV path resolves through `findByUrlSegment` and not through a
100
+ // `get` by derived id.
101
+ const accountConfigId = harness.makeId();
102
+ const calendar = await repo.create({
103
+ accountConfigId,
104
+ urlSegment: "Work-Calendar",
105
+ displayName: "Work",
106
+ });
107
+ assert.equal(calendar.urlSegment, "work-calendar");
108
+ assert.equal(calendar.calendarId, deriveCalendarId(accountConfigId, "WORK-CALENDAR"));
109
+ assert.equal((await repo.findByUrlSegment(accountConfigId, "WORK-CALENDAR"))
110
+ ?.calendarId, calendar.calendarId);
111
+ assert.equal((await repo.get(accountConfigId, calendar.calendarId)).urlSegment, "work-calendar");
112
+ });
113
+ test("listByAccountConfig scopes to the account config", async () => {
114
+ const accountConfigId = harness.makeId();
115
+ const other = harness.makeId();
116
+ const mine = await repo.create({
117
+ accountConfigId,
118
+ urlSegment: "default",
119
+ displayName: "Mine",
120
+ });
121
+ await repo.create({
122
+ accountConfigId: other,
123
+ urlSegment: "default",
124
+ displayName: "Foreign",
125
+ });
126
+ const calendars = await repo.listByAccountConfig(accountConfigId);
127
+ assert.equal(calendars.length, 1);
128
+ assert.equal(calendars[0]?.calendarId, mine.calendarId);
129
+ });
130
+ test("bumpSyncSequence hands every caller a distinct increasing value", async () => {
131
+ const accountConfigId = harness.makeId();
132
+ const calendar = await repo.create({
133
+ accountConfigId,
134
+ urlSegment: "seq",
135
+ displayName: "Seq",
136
+ });
137
+ const sequences = [];
138
+ for (let i = 0; i < 5; i += 1) {
139
+ sequences.push(await repo.bumpSyncSequence(accountConfigId, calendar.calendarId));
140
+ }
141
+ assert.deepEqual(sequences, [1, 2, 3, 4, 5]);
142
+ const reread = await repo.get(accountConfigId, calendar.calendarId);
143
+ assert.equal(reread.syncSequence, 5);
144
+ });
145
+ // Every write and every delete of an object in the collection goes through
146
+ // this bump, which makes the collection the one timestamp that only moves
147
+ // forward — the calendar feed serves it as Last-Modified, and a validator
148
+ // that moved backwards on a delete would tell a subscriber its stale copy
149
+ // is still good.
150
+ test("bumpSyncSequence carries the collection's modification time forward", async () => {
151
+ const accountConfigId = harness.makeId();
152
+ const calendar = await repo.create({
153
+ accountConfigId,
154
+ urlSegment: "touched",
155
+ displayName: "Touched",
156
+ });
157
+ await repo.bumpSyncSequence(accountConfigId, calendar.calendarId);
158
+ const bumped = await repo.get(accountConfigId, calendar.calendarId);
159
+ assert.ok(bumped.updatedAt >= calendar.updatedAt);
160
+ assert.equal(bumped.createdAt, calendar.createdAt);
161
+ });
162
+ test("bumpSyncSequence throws a not-found error for a missing collection", async () => {
163
+ await assert.rejects(repo.bumpSyncSequence(harness.makeId(), harness.makeId()), (error) => harness.isNotFoundError(error));
164
+ });
165
+ test("a collection starts with no subscription and empty fetch bookkeeping", async () => {
166
+ const calendar = await repo.create({
167
+ accountConfigId: harness.makeId(),
168
+ urlSegment: "plain",
169
+ displayName: "Plain",
170
+ });
171
+ assert.equal(calendar.subscriptionUrl, "");
172
+ assert.equal(calendar.subscriptionEnabled, false);
173
+ assert.equal(calendar.subscriptionCheckedAt, 0);
174
+ assert.equal(calendar.subscriptionFetchedAt, 0);
175
+ assert.equal(calendar.subscriptionError, "");
176
+ });
177
+ test("listEnabledSubscriptions returns enabled subscriptions across account configs and nothing else", async () => {
178
+ const first = harness.makeId();
179
+ const second = harness.makeId();
180
+ const subscribed = await repo.create({
181
+ accountConfigId: first,
182
+ urlSegment: "feed",
183
+ displayName: "Feed",
184
+ source: CalendarSource.Subscribed,
185
+ subscriptionUrl: "https://calendar.example/feed.ics",
186
+ subscriptionEnabled: true,
187
+ });
188
+ const elsewhere = await repo.create({
189
+ accountConfigId: second,
190
+ urlSegment: "feed",
191
+ displayName: "Feed",
192
+ source: CalendarSource.Subscribed,
193
+ subscriptionUrl: "https://calendar.example/other.ics",
194
+ subscriptionEnabled: true,
195
+ });
196
+ const paused = await repo.create({
197
+ accountConfigId: first,
198
+ urlSegment: "paused",
199
+ displayName: "Paused",
200
+ source: CalendarSource.Subscribed,
201
+ subscriptionUrl: "https://calendar.example/paused.ics",
202
+ subscriptionEnabled: true,
203
+ });
204
+ await repo.update(first, paused.calendarId, {
205
+ subscriptionEnabled: false,
206
+ });
207
+ await repo.create({
208
+ accountConfigId: first,
209
+ urlSegment: "own",
210
+ displayName: "Own",
211
+ });
212
+ const ids = (await repo.listEnabledSubscriptions())
213
+ .filter((calendar) => calendar.accountConfigId === first ||
214
+ calendar.accountConfigId === second)
215
+ .map((calendar) => calendar.calendarId)
216
+ .sort();
217
+ assert.deepEqual(ids, [subscribed.calendarId, elsewhere.calendarId].sort());
218
+ });
219
+ test("update records a fetch outcome on the collection", async () => {
220
+ const accountConfigId = harness.makeId();
221
+ const calendar = await repo.create({
222
+ accountConfigId,
223
+ urlSegment: "fetched",
224
+ displayName: "Fetched",
225
+ source: CalendarSource.Subscribed,
226
+ subscriptionUrl: "https://calendar.example/feed.ics",
227
+ subscriptionEnabled: true,
228
+ });
229
+ const updated = await repo.update(accountConfigId, calendar.calendarId, {
230
+ subscriptionCheckedAt: 2_000,
231
+ subscriptionFetchedAt: 1_000,
232
+ subscriptionError: "the feed answered 404",
233
+ });
234
+ assert.equal(updated.subscriptionCheckedAt, 2_000);
235
+ assert.equal(updated.subscriptionFetchedAt, 1_000);
236
+ assert.equal(updated.subscriptionError, "the feed answered 404");
237
+ assert.equal(updated.subscriptionUrl, "https://calendar.example/feed.ics");
238
+ assert.equal(updated.subscriptionEnabled, true);
239
+ });
240
+ test("delete removes the row", async () => {
241
+ const accountConfigId = harness.makeId();
242
+ const calendar = await repo.create({
243
+ accountConfigId,
244
+ urlSegment: "gone",
245
+ displayName: "Gone",
246
+ });
247
+ await repo.delete(accountConfigId, calendar.calendarId);
248
+ await assert.rejects(repo.get(accountConfigId, calendar.calendarId), (error) => harness.isNotFoundError(error));
249
+ });
250
+ });
251
+ }
@@ -0,0 +1,3 @@
1
+ import type { ICalendarEventIndexRepository } from "../interfaces/calendar-event-index.js";
2
+ import type { RepositoryConformanceHarness } from "./harness.js";
3
+ export declare function calendarEventIndexRepositoryConformance(harness: RepositoryConformanceHarness<ICalendarEventIndexRepository>): void;
@@ -0,0 +1,114 @@
1
+ import assert from "node:assert/strict";
2
+ import { after, before, describe, test } from "node:test";
3
+ import { CalendarEventStatus, CalendarTransparency } from "@remit/domain-enums";
4
+ const occurrence = (recurrenceId, startAt, endAt, overrides = {}) => ({
5
+ recurrenceId,
6
+ startAt,
7
+ endAt,
8
+ allDay: false,
9
+ summary: "Stand-up",
10
+ status: CalendarEventStatus.Confirmed,
11
+ transparency: CalendarTransparency.Opaque,
12
+ ...overrides,
13
+ });
14
+ export function calendarEventIndexRepositoryConformance(harness) {
15
+ describe("ICalendarEventIndexRepository conformance", () => {
16
+ let repo;
17
+ before(async () => {
18
+ repo = await harness.createRepository();
19
+ });
20
+ after(() => harness.teardown());
21
+ test("replaceForObject writes the master occurrence under an empty recurrenceId", async () => {
22
+ const calendarId = harness.makeId();
23
+ const calendarObjectId = harness.makeId();
24
+ await repo.replaceForObject(calendarId, calendarObjectId, [
25
+ occurrence("", "2026-08-26T07:00:00Z", "2026-08-26T08:00:00Z"),
26
+ ]);
27
+ const rows = await repo.listForObject(calendarId, calendarObjectId);
28
+ assert.equal(rows.length, 1);
29
+ assert.equal(rows[0]?.recurrenceId, "");
30
+ assert.equal(rows[0]?.startAt, "2026-08-26T07:00:00Z");
31
+ });
32
+ test("replaceForObject drops the occurrences a shortened series no longer has", async () => {
33
+ const calendarId = harness.makeId();
34
+ const calendarObjectId = harness.makeId();
35
+ await repo.replaceForObject(calendarId, calendarObjectId, [
36
+ occurrence("2026-08-26T07:00:00Z", "2026-08-26T07:00:00Z", "2026-08-26T08:00:00Z"),
37
+ occurrence("2026-09-02T07:00:00Z", "2026-09-02T07:00:00Z", "2026-09-02T08:00:00Z"),
38
+ ]);
39
+ await repo.replaceForObject(calendarId, calendarObjectId, [
40
+ occurrence("2026-08-26T07:00:00Z", "2026-08-26T07:00:00Z", "2026-08-26T08:00:00Z"),
41
+ ]);
42
+ const rows = await repo.listForObject(calendarId, calendarObjectId);
43
+ assert.deepEqual(rows.map((row) => row.recurrenceId), ["2026-08-26T07:00:00Z"]);
44
+ });
45
+ test("replaceForObject leaves the other resources of the collection alone", async () => {
46
+ const calendarId = harness.makeId();
47
+ const mine = harness.makeId();
48
+ const other = harness.makeId();
49
+ await repo.replaceForObject(calendarId, other, [
50
+ occurrence("", "2026-08-26T07:00:00Z", "2026-08-26T08:00:00Z"),
51
+ ]);
52
+ await repo.replaceForObject(calendarId, mine, []);
53
+ assert.equal((await repo.listForObject(calendarId, other)).length, 1);
54
+ assert.equal((await repo.listForObject(calendarId, mine)).length, 0);
55
+ });
56
+ test("listByStartRange returns occurrences in start order, half-open on the end", async () => {
57
+ const calendarId = harness.makeId();
58
+ const calendarObjectId = harness.makeId();
59
+ await repo.replaceForObject(calendarId, calendarObjectId, [
60
+ occurrence("2026-09-02T07:00:00Z", "2026-09-02T07:00:00Z", "2026-09-02T08:00:00Z"),
61
+ occurrence("2026-08-26T07:00:00Z", "2026-08-26T07:00:00Z", "2026-08-26T08:00:00Z"),
62
+ occurrence("2026-09-09T07:00:00Z", "2026-09-09T07:00:00Z", "2026-09-09T08:00:00Z"),
63
+ ]);
64
+ const rows = await repo.listByStartRange(calendarId, "2026-08-26T07:00:00Z", "2026-09-09T07:00:00Z");
65
+ assert.deepEqual(rows.map((row) => row.startAt), ["2026-08-26T07:00:00Z", "2026-09-02T07:00:00Z"]);
66
+ });
67
+ test("listByStartRange scopes to the collection", async () => {
68
+ const calendarId = harness.makeId();
69
+ const other = harness.makeId();
70
+ await repo.replaceForObject(calendarId, harness.makeId(), [
71
+ occurrence("", "2026-08-26T07:00:00Z", "2026-08-26T08:00:00Z"),
72
+ ]);
73
+ await repo.replaceForObject(other, harness.makeId(), [
74
+ occurrence("", "2026-08-26T07:00:00Z", "2026-08-26T08:00:00Z"),
75
+ ]);
76
+ const rows = await repo.listByStartRange(calendarId, "2026-08-01T00:00:00Z", "2026-09-01T00:00:00Z");
77
+ assert.equal(rows.length, 1);
78
+ });
79
+ test("an occurrence keeps its own summary, status and transparency", async () => {
80
+ const calendarId = harness.makeId();
81
+ const calendarObjectId = harness.makeId();
82
+ await repo.replaceForObject(calendarId, calendarObjectId, [
83
+ occurrence("2026-08-26T07:00:00Z", "2026-08-26T07:00:00Z", "2026-08-26T08:00:00Z"),
84
+ occurrence("2026-09-02T07:00:00Z", "2026-09-02T07:00:00Z", "2026-09-02T08:00:00Z", {
85
+ summary: "Stand-up (moved)",
86
+ status: CalendarEventStatus.Cancelled,
87
+ transparency: CalendarTransparency.Transparent,
88
+ }),
89
+ ]);
90
+ const rows = await repo.listForObject(calendarId, calendarObjectId);
91
+ assert.deepEqual(rows.map((row) => [row.summary, row.status, row.transparency]), [
92
+ [
93
+ "Stand-up",
94
+ CalendarEventStatus.Confirmed,
95
+ CalendarTransparency.Opaque,
96
+ ],
97
+ [
98
+ "Stand-up (moved)",
99
+ CalendarEventStatus.Cancelled,
100
+ CalendarTransparency.Transparent,
101
+ ],
102
+ ]);
103
+ });
104
+ test("deleteForObject removes every occurrence of one resource", async () => {
105
+ const calendarId = harness.makeId();
106
+ const calendarObjectId = harness.makeId();
107
+ await repo.replaceForObject(calendarId, calendarObjectId, [
108
+ occurrence("", "2026-08-26T07:00:00Z", "2026-08-26T08:00:00Z"),
109
+ ]);
110
+ await repo.deleteForObject(calendarId, calendarObjectId);
111
+ assert.equal((await repo.listForObject(calendarId, calendarObjectId)).length, 0);
112
+ });
113
+ });
114
+ }
@@ -0,0 +1,3 @@
1
+ import type { ICalendarFeedTokenRepository } from "../interfaces/calendar-feed-token.js";
2
+ import type { RepositoryConformanceHarness } from "./harness.js";
3
+ export declare function calendarFeedTokenRepositoryConformance(harness: RepositoryConformanceHarness<ICalendarFeedTokenRepository>): void;
@@ -0,0 +1,97 @@
1
+ import assert from "node:assert/strict";
2
+ import { after, before, describe, test } from "node:test";
3
+ import { deriveCalendarFeedTokenId } from "../id.js";
4
+ const hashOf = (label) => label.padEnd(64, "0");
5
+ export function calendarFeedTokenRepositoryConformance(harness) {
6
+ describe("ICalendarFeedTokenRepository conformance", () => {
7
+ let repo;
8
+ before(async () => {
9
+ repo = await harness.createRepository();
10
+ });
11
+ after(() => harness.teardown());
12
+ test("put derives the id from the calendar and starts unrotated", async () => {
13
+ const accountConfigId = harness.makeId();
14
+ const calendarId = harness.makeId();
15
+ const minted = await repo.put({
16
+ accountConfigId,
17
+ calendarId,
18
+ tokenHash: hashOf("a"),
19
+ });
20
+ assert.equal(minted.feedTokenId, deriveCalendarFeedTokenId(calendarId));
21
+ assert.equal(minted.calendarId, calendarId);
22
+ assert.equal(minted.tokenHash, hashOf("a"));
23
+ assert.equal(minted.rotatedAt, 0);
24
+ assert.ok(minted.createdAt > 0);
25
+ });
26
+ test("a second put rotates the one address rather than adding another", async () => {
27
+ const accountConfigId = harness.makeId();
28
+ const calendarId = harness.makeId();
29
+ const minted = await repo.put({
30
+ accountConfigId,
31
+ calendarId,
32
+ tokenHash: hashOf("b"),
33
+ });
34
+ const rotated = await repo.put({
35
+ accountConfigId,
36
+ calendarId,
37
+ tokenHash: hashOf("c"),
38
+ });
39
+ assert.equal(rotated.feedTokenId, minted.feedTokenId);
40
+ assert.equal(rotated.tokenHash, hashOf("c"));
41
+ assert.equal(rotated.createdAt, minted.createdAt, "the feed is the same feed under a new secret");
42
+ assert.ok(rotated.rotatedAt > 0, "a replacement is distinguishable from a first mint");
43
+ assert.equal(await repo.findByTokenHash(hashOf("b")), null, "the address that was rotated away no longer resolves");
44
+ assert.equal((await repo.findByTokenHash(hashOf("c")))?.calendarId, calendarId);
45
+ });
46
+ test("findByCalendar answers only for the account config that owns it", async () => {
47
+ const accountConfigId = harness.makeId();
48
+ const stranger = harness.makeId();
49
+ const calendarId = harness.makeId();
50
+ await repo.put({
51
+ accountConfigId,
52
+ calendarId,
53
+ tokenHash: hashOf("d"),
54
+ });
55
+ assert.equal((await repo.findByCalendar(accountConfigId, calendarId))?.tokenHash, hashOf("d"));
56
+ assert.equal(await repo.findByCalendar(stranger, calendarId), null);
57
+ assert.equal(await repo.findByCalendar(accountConfigId, harness.makeId()), null);
58
+ });
59
+ test("findByTokenHash is unscoped, and names the owner it found", async () => {
60
+ const accountConfigId = harness.makeId();
61
+ const calendarId = harness.makeId();
62
+ await repo.put({
63
+ accountConfigId,
64
+ calendarId,
65
+ tokenHash: hashOf("e"),
66
+ });
67
+ const found = await repo.findByTokenHash(hashOf("e"));
68
+ assert.equal(found?.accountConfigId, accountConfigId);
69
+ assert.equal(found?.calendarId, calendarId);
70
+ assert.equal(await repo.findByTokenHash(hashOf("nothing")), null);
71
+ });
72
+ test("delete stops the address resolving", async () => {
73
+ const accountConfigId = harness.makeId();
74
+ const calendarId = harness.makeId();
75
+ await repo.put({
76
+ accountConfigId,
77
+ calendarId,
78
+ tokenHash: hashOf("f"),
79
+ });
80
+ await repo.delete(accountConfigId, calendarId);
81
+ assert.equal(await repo.findByTokenHash(hashOf("f")), null);
82
+ assert.equal(await repo.findByCalendar(accountConfigId, calendarId), null);
83
+ });
84
+ test("delete leaves another account config's address alone", async () => {
85
+ const accountConfigId = harness.makeId();
86
+ const stranger = harness.makeId();
87
+ const calendarId = harness.makeId();
88
+ await repo.put({
89
+ accountConfigId,
90
+ calendarId,
91
+ tokenHash: hashOf("g"),
92
+ });
93
+ await repo.delete(stranger, calendarId);
94
+ assert.equal((await repo.findByTokenHash(hashOf("g")))?.calendarId, calendarId);
95
+ });
96
+ });
97
+ }
@@ -0,0 +1,3 @@
1
+ import type { ICalendarObjectRepository } from "../interfaces/calendar-object.js";
2
+ import type { RepositoryConformanceHarness } from "./harness.js";
3
+ export declare function calendarObjectRepositoryConformance(harness: RepositoryConformanceHarness<ICalendarObjectRepository>): void;
@@ -0,0 +1,156 @@
1
+ import assert from "node:assert/strict";
2
+ import { after, before, describe, test } from "node:test";
3
+ import { CalendarEventStatus, CalendarTransparency, ZoneCertainty, } from "@remit/domain-enums";
4
+ import { deriveCalendarObjectId } from "../id.js";
5
+ const ICAL_DATA = [
6
+ "BEGIN:VCALENDAR",
7
+ "VERSION:2.0",
8
+ "BEGIN:VEVENT",
9
+ "UID:conformance@example.com",
10
+ "DTSTART:20260826T090000Z",
11
+ "DTEND:20260826T100000Z",
12
+ "SUMMARY:Stand-up",
13
+ "END:VEVENT",
14
+ "END:VCALENDAR",
15
+ "",
16
+ ].join("\r\n");
17
+ const objectInput = (calendarId, overrides = {}) => ({
18
+ calendarId,
19
+ resourceName: "conformance.ics",
20
+ icalUid: "conformance@example.com",
21
+ icalData: ICAL_DATA,
22
+ etag: "a".repeat(64),
23
+ sequence: 0,
24
+ syncSequence: 1,
25
+ summary: "Stand-up",
26
+ dtStart: "2026-08-26T09:00:00+00:00",
27
+ dtEnd: "2026-08-26T10:00:00+00:00",
28
+ allDay: false,
29
+ zoneCertainty: ZoneCertainty.Explicit,
30
+ status: CalendarEventStatus.Confirmed,
31
+ transparency: CalendarTransparency.Opaque,
32
+ hasRecurrence: false,
33
+ expandedThrough: "",
34
+ ...overrides,
35
+ });
36
+ export function calendarObjectRepositoryConformance(harness) {
37
+ describe("ICalendarObjectRepository conformance", () => {
38
+ let repo;
39
+ before(async () => {
40
+ repo = await harness.createRepository();
41
+ });
42
+ after(() => harness.teardown());
43
+ test("put derives calendarObjectId from the collection and the resource name", async () => {
44
+ const calendarId = harness.makeId();
45
+ const object = await repo.put(objectInput(calendarId));
46
+ assert.equal(object.calendarObjectId, deriveCalendarObjectId(calendarId, "conformance.ics"));
47
+ });
48
+ test("put stores the iCalendar bytes verbatim, CRLF intact", async () => {
49
+ const calendarId = harness.makeId();
50
+ await repo.put(objectInput(calendarId));
51
+ const stored = await repo.findByResourceName(calendarId, "conformance.ics");
52
+ assert.equal(stored?.icalData, ICAL_DATA);
53
+ assert.ok(stored?.icalData.includes("\r\n"));
54
+ });
55
+ test("put rewrites the same resource rather than forking a duplicate", async () => {
56
+ const calendarId = harness.makeId();
57
+ await repo.put(objectInput(calendarId));
58
+ const second = await repo.put(objectInput(calendarId, {
59
+ summary: "Stand-up (moved)",
60
+ etag: "b".repeat(64),
61
+ syncSequence: 2,
62
+ }));
63
+ assert.equal(second.summary, "Stand-up (moved)");
64
+ assert.equal(second.syncSequence, 2);
65
+ const all = await repo.listByCalendar(calendarId);
66
+ assert.equal(all.length, 1);
67
+ });
68
+ test("get throws a not-found error for a missing resource", async () => {
69
+ await assert.rejects(repo.get(harness.makeId(), harness.makeId()), (error) => harness.isNotFoundError(error));
70
+ });
71
+ test("find answers with null where get throws", async () => {
72
+ const calendarId = harness.makeId();
73
+ const object = await repo.put(objectInput(calendarId));
74
+ assert.equal((await repo.find(calendarId, object.calendarObjectId))?.icalData, ICAL_DATA);
75
+ assert.equal(await repo.find(calendarId, harness.makeId()), null);
76
+ assert.equal(await repo.find(harness.makeId(), object.calendarObjectId), null, "a resource is only found through the collection that holds it");
77
+ });
78
+ test("listIncompleteExpansions returns only the series whose index stops short", async () => {
79
+ const calendarId = harness.makeId();
80
+ await repo.put(objectInput(calendarId, { resourceName: "complete.ics" }));
81
+ const incomplete = await repo.put(objectInput(calendarId, {
82
+ resourceName: "incomplete.ics",
83
+ expandedThrough: "2026-01-01T00:00:00Z",
84
+ }));
85
+ assert.deepEqual((await repo.listIncompleteExpansions(calendarId, "2026-06-01T00:00:00Z")).map((object) => object.calendarObjectId), [incomplete.calendarObjectId], "a resource with no expandedThrough is complete and never appears");
86
+ assert.deepEqual(await repo.listIncompleteExpansions(calendarId, "2025-06-01T00:00:00Z"), [], "and one whose index already reaches the window does not either");
87
+ });
88
+ test("findByResourceName returns null for a name the collection does not hold", async () => {
89
+ assert.equal(await repo.findByResourceName(harness.makeId(), "absent.ics"), null);
90
+ });
91
+ test("findByUid resolves the resource carrying an iCalendar UID", async () => {
92
+ const calendarId = harness.makeId();
93
+ const object = await repo.put(objectInput(calendarId));
94
+ const found = await repo.findByUid(calendarId, "conformance@example.com");
95
+ assert.equal(found?.calendarObjectId, object.calendarObjectId);
96
+ assert.equal(await repo.findByUid(calendarId, "absent@example.com"), null);
97
+ });
98
+ test("listByUidPrefix finds the resources whose UID starts with the prefix, in one collection", async () => {
99
+ const calendarId = harness.makeId();
100
+ const other = harness.makeId();
101
+ const tail = await repo.put(objectInput(calendarId, {
102
+ resourceName: "tail.ics",
103
+ icalUid: "root@example.com/following/1",
104
+ }));
105
+ await repo.put(objectInput(calendarId, {
106
+ resourceName: "root.ics",
107
+ icalUid: "root@example.com",
108
+ }));
109
+ await repo.put(objectInput(calendarId, {
110
+ resourceName: "lookalike.ics",
111
+ icalUid: "root@example.comx/following/2",
112
+ }));
113
+ await repo.put(objectInput(other, {
114
+ resourceName: "elsewhere.ics",
115
+ icalUid: "root@example.com/following/3",
116
+ }));
117
+ const found = await repo.listByUidPrefix(calendarId, "root@example.com/following/");
118
+ assert.deepEqual(found.map((object) => object.calendarObjectId), [tail.calendarObjectId]);
119
+ });
120
+ test("listByCalendar scopes to the collection", async () => {
121
+ const calendarId = harness.makeId();
122
+ const other = harness.makeId();
123
+ const mine = await repo.put(objectInput(calendarId));
124
+ await repo.put(objectInput(other));
125
+ const objects = await repo.listByCalendar(calendarId);
126
+ assert.equal(objects.length, 1);
127
+ assert.equal(objects[0]?.calendarObjectId, mine.calendarObjectId);
128
+ });
129
+ test("listChangedSince returns later writes in change order, excluding the token itself", async () => {
130
+ const calendarId = harness.makeId();
131
+ await repo.put(objectInput(calendarId, {
132
+ resourceName: "one.ics",
133
+ icalUid: "one@example.com",
134
+ syncSequence: 1,
135
+ }));
136
+ await repo.put(objectInput(calendarId, {
137
+ resourceName: "two.ics",
138
+ icalUid: "two@example.com",
139
+ syncSequence: 2,
140
+ }));
141
+ await repo.put(objectInput(calendarId, {
142
+ resourceName: "three.ics",
143
+ icalUid: "three@example.com",
144
+ syncSequence: 3,
145
+ }));
146
+ const changed = await repo.listChangedSince(calendarId, 1);
147
+ assert.deepEqual(changed.map((object) => object.resourceName), ["two.ics", "three.ics"]);
148
+ });
149
+ test("delete removes the row", async () => {
150
+ const calendarId = harness.makeId();
151
+ const object = await repo.put(objectInput(calendarId));
152
+ await repo.delete(calendarId, object.calendarObjectId);
153
+ await assert.rejects(repo.get(calendarId, object.calendarObjectId), (error) => harness.isNotFoundError(error));
154
+ });
155
+ });
156
+ }
@@ -0,0 +1,3 @@
1
+ import type { ICalendarSuggestionRepository } from "../interfaces/calendar-suggestion.js";
2
+ import type { RepositoryConformanceHarness } from "./harness.js";
3
+ export declare function calendarSuggestionRepositoryConformance(harness: RepositoryConformanceHarness<ICalendarSuggestionRepository>): void;