@oneuptime/common 12.0.29 → 12.0.30

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 (189) hide show
  1. package/Models/DatabaseModels/Index.ts +11 -0
  2. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +43 -0
  3. package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +662 -0
  4. package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +584 -0
  5. package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +546 -0
  6. package/Models/DatabaseModels/UserOnCallShiftReminder.ts +231 -0
  7. package/Models/DatabaseModels/UserOnCallShiftReminderLog.ts +331 -0
  8. package/Server/API/OnCallCalendarAPI.ts +2194 -0
  9. package/Server/EnvironmentConfig.ts +138 -0
  10. package/Server/Infrastructure/OnCallCalendarFeedCache.ts +1050 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.ts +253 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  13. package/Server/Middleware/OnCallCalendarFeedRateLimit.ts +487 -0
  14. package/Server/Services/Index.ts +12 -0
  15. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +32 -0
  16. package/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.ts +260 -0
  17. package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +70 -3
  18. package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +138 -3
  19. package/Server/Services/OnCallDutyPolicyScheduleService.ts +1465 -134
  20. package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +167 -31
  21. package/Server/Services/ProjectOnCallCalendarFeedService.ts +192 -0
  22. package/Server/Services/TeamMemberService.ts +489 -1
  23. package/Server/Services/TeamService.ts +50 -0
  24. package/Server/Services/UserNotificationSettingService.ts +44 -1
  25. package/Server/Services/UserOnCallCalendarFeedService.ts +159 -0
  26. package/Server/Services/UserOnCallShiftReminderLogService.ts +151 -0
  27. package/Server/Services/UserOnCallShiftReminderService.ts +112 -0
  28. package/Server/Utils/OnCall/CalendarFeedToken.ts +241 -0
  29. package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1488 -0
  30. package/Server/Utils/OnCall/OnCallCalendarFeedUrls.ts +230 -0
  31. package/Server/Utils/OnCall/OnCallShiftChangeListeners.ts +227 -0
  32. package/Server/Utils/OnCall/OnCallShiftMaterializer.ts +1127 -0
  33. package/Server/Utils/OnCall/OnCallShiftReminderListener.ts +174 -0
  34. package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +2584 -0
  35. package/Server/Utils/Response.ts +134 -0
  36. package/Server/Utils/StartServer.ts +10 -0
  37. package/Server/Utils/Telemetry/AppMetrics.ts +51 -0
  38. package/Tests/App/Dashboard/OnCallCalendarFeedLinks.test.tsx +229 -0
  39. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +489 -0
  40. package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +149 -0
  41. package/Tests/App/Dashboard/OnCallCalendarFeedUtil.test.ts +855 -0
  42. package/Tests/App/Dashboard/OnCallPersonalCalendarFeed.test.tsx +1209 -0
  43. package/Tests/App/Dashboard/OnCallSharedCalendarFeedCard.test.tsx +1019 -0
  44. package/Tests/App/Dashboard/OnCallShiftRemindersAndUpcomingShifts.test.tsx +655 -0
  45. package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +73 -0
  46. package/Tests/Models/OnCallCalendarFeedModels.test.ts +642 -0
  47. package/Tests/Models/OnCallDutyPolicyScheduleCalendarFeed.test.ts +288 -0
  48. package/Tests/Models/OnCallDutyPolicyScheduleShiftConfigVersion.test.ts +88 -0
  49. package/Tests/Models/UserOnCallShiftReminderModels.test.ts +368 -0
  50. package/Tests/Server/API/Helpers.ts +8 -0
  51. package/Tests/Server/API/OnCallCalendarAPI.test.ts +4461 -0
  52. package/Tests/Server/EnvironmentConfigOnCallCalendarFeed.test.ts +357 -0
  53. package/Tests/Server/Infrastructure/OnCallCalendarFeedCache.test.ts +1589 -0
  54. package/Tests/Server/Infrastructure/Postgres/AddOnCallCalendarFeedsMigration.test.ts +369 -0
  55. package/Tests/Server/Middleware/OnCallCalendarFeedRateLimit.test.ts +1365 -0
  56. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +70 -0
  57. package/Tests/Server/Services/OnCallCalendarFeedServices.test.ts +1103 -0
  58. package/Tests/Server/Services/OnCallDutyPolicyScheduleResolver.test.ts +1037 -0
  59. package/Tests/Server/Services/OnCallShiftConfigPropagation.test.ts +1270 -0
  60. package/Tests/Server/Services/TeamDeleteMemberCleanup.test.ts +156 -0
  61. package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +755 -0
  62. package/Tests/Server/Services/UserNotificationSettingShiftReminderDefaults.test.ts +267 -0
  63. package/Tests/Server/Services/UserOnCallShiftReminderServices.test.ts +583 -0
  64. package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +254 -0
  65. package/Tests/Server/Utils/OnCall/CalendarFeedToken.test.ts +467 -0
  66. package/Tests/Server/Utils/OnCall/OnCallCalendarFeedRenderer.test.ts +2142 -0
  67. package/Tests/Server/Utils/OnCall/OnCallCalendarFeedUrls.test.ts +278 -0
  68. package/Tests/Server/Utils/OnCall/OnCallResolverTestHarness.ts +440 -0
  69. package/Tests/Server/Utils/OnCall/OnCallShiftChangeListeners.test.ts +314 -0
  70. package/Tests/Server/Utils/OnCall/OnCallShiftMaterializer.test.ts +1502 -0
  71. package/Tests/Server/Utils/OnCall/OnCallShiftReminderListener.test.ts +491 -0
  72. package/Tests/Server/Utils/OnCall/OnCallShiftReminderRunner.test.ts +3088 -0
  73. package/Tests/Server/Utils/OnCall/OnCallShiftReminderTestHarness.ts +910 -0
  74. package/Tests/Server/Utils/ResponseSendCalendarResponse.test.ts +561 -0
  75. package/Tests/Server/Utils/StartServerEncryptionSecretWarning.test.ts +64 -0
  76. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +29 -0
  77. package/Tests/Types/Calendar/ICalendar.test.ts +488 -0
  78. package/Tests/Types/NotificationSetting/NotificationSettingEventType.test.ts +53 -0
  79. package/Tests/Types/OnCallDutyPolicy/CalendarFeedTestFixtures.ts +310 -0
  80. package/Tests/Types/OnCallDutyPolicy/CalendarFeedWindow.test.ts +205 -0
  81. package/Tests/Types/OnCallDutyPolicy/LayerUtilLayerMetaAndIterationCap.test.ts +458 -0
  82. package/Tests/Types/OnCallDutyPolicy/MaterializedShift.test.ts +197 -0
  83. package/Tests/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.test.ts +1986 -0
  84. package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtilGroupKey.test.ts +678 -0
  85. package/Tests/Types/OnCallDutyPolicy/ShiftSeamUtil.test.ts +417 -0
  86. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
  87. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
  88. package/Types/Calendar/ICalendar.ts +442 -0
  89. package/Types/Email/EmailTemplateType.ts +2 -0
  90. package/Types/NotificationSetting/NotificationSettingEventType.ts +4 -0
  91. package/Types/OnCallDutyPolicy/CalendarFeedWindow.ts +140 -0
  92. package/Types/OnCallDutyPolicy/Layer.ts +190 -35
  93. package/Types/OnCallDutyPolicy/MaterializedShift.ts +274 -0
  94. package/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.ts +1415 -0
  95. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +159 -4
  96. package/Types/OnCallDutyPolicy/ShiftSeamUtil.ts +106 -0
  97. package/build/dist/Models/DatabaseModels/Index.js +11 -0
  98. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +44 -0
  100. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  101. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +699 -0
  102. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -0
  103. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +620 -0
  104. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -0
  105. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +585 -0
  106. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -0
  107. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js +247 -0
  108. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js.map +1 -0
  109. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js +355 -0
  110. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js.map +1 -0
  111. package/build/dist/Server/API/OnCallCalendarAPI.js +1371 -0
  112. package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -0
  113. package/build/dist/Server/EnvironmentConfig.js +102 -0
  114. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  115. package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js +663 -0
  116. package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js.map +1 -0
  117. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js +103 -0
  118. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js.map +1 -0
  119. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  121. package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js +342 -0
  122. package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js.map +1 -0
  123. package/build/dist/Server/Services/Index.js +12 -0
  124. package/build/dist/Server/Services/Index.js.map +1 -1
  125. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +20 -0
  126. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  127. package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js +219 -0
  128. package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js.map +1 -0
  129. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +47 -3
  130. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
  131. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +112 -3
  132. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
  133. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +955 -9
  134. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  135. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +93 -6
  136. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
  137. package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js +157 -0
  138. package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js.map +1 -0
  139. package/build/dist/Server/Services/TeamMemberService.js +396 -1
  140. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  141. package/build/dist/Server/Services/TeamService.js +43 -0
  142. package/build/dist/Server/Services/TeamService.js.map +1 -1
  143. package/build/dist/Server/Services/UserNotificationSettingService.js +26 -1
  144. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  145. package/build/dist/Server/Services/UserOnCallCalendarFeedService.js +140 -0
  146. package/build/dist/Server/Services/UserOnCallCalendarFeedService.js.map +1 -0
  147. package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js +116 -0
  148. package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js.map +1 -0
  149. package/build/dist/Server/Services/UserOnCallShiftReminderService.js +92 -0
  150. package/build/dist/Server/Services/UserOnCallShiftReminderService.js.map +1 -0
  151. package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js +161 -0
  152. package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js.map +1 -0
  153. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js +983 -0
  154. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js.map +1 -0
  155. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js +155 -0
  156. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js.map +1 -0
  157. package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js +145 -0
  158. package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js.map +1 -0
  159. package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js +752 -0
  160. package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js.map +1 -0
  161. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js +92 -0
  162. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js.map +1 -0
  163. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1786 -0
  164. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -0
  165. package/build/dist/Server/Utils/Response.js +97 -0
  166. package/build/dist/Server/Utils/Response.js.map +1 -1
  167. package/build/dist/Server/Utils/StartServer.js +9 -1
  168. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  169. package/build/dist/Server/Utils/Telemetry/AppMetrics.js +41 -0
  170. package/build/dist/Server/Utils/Telemetry/AppMetrics.js.map +1 -1
  171. package/build/dist/Types/Calendar/ICalendar.js +290 -0
  172. package/build/dist/Types/Calendar/ICalendar.js.map +1 -0
  173. package/build/dist/Types/Email/EmailTemplateType.js +2 -0
  174. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  175. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
  176. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
  177. package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js +97 -0
  178. package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js.map +1 -0
  179. package/build/dist/Types/OnCallDutyPolicy/Layer.js +113 -19
  180. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  181. package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js +134 -0
  182. package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js.map +1 -0
  183. package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js +800 -0
  184. package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js.map +1 -0
  185. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +97 -3
  186. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
  187. package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js +77 -0
  188. package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js.map +1 -0
  189. package/package.json +1 -1
@@ -0,0 +1,278 @@
1
+ import OnCallCalendarFeedUrls, {
2
+ FeedUrls,
3
+ GOOGLE_CALENDAR_ADD_BY_URL,
4
+ HOST_WARNING,
5
+ ON_CALL_CALENDAR_ROUTE_PREFIX,
6
+ PERSONAL_FEED_FILE_NAME,
7
+ PROJECT_FEED_FILE_NAME,
8
+ PROTOCOL_WARNING,
9
+ SCHEDULE_FEED_FILE_NAME,
10
+ } from "../../../../Server/Utils/OnCall/OnCallCalendarFeedUrls";
11
+ import CalendarFeedToken from "../../../../Server/Utils/OnCall/CalendarFeedToken";
12
+ import Protocol from "../../../../Types/API/Protocol";
13
+ import { OnCallCalendarFeedKind } from "../../../../Types/OnCallDutyPolicy/OnCallCalendarFeedUtil";
14
+ import { describe, expect, test } from "@jest/globals";
15
+
16
+ /*
17
+ * The subscription URLs the settings page hands out. What matters: the path
18
+ * segments match the Nginx access-log exemption and the API routes exactly,
19
+ * webcal follows the instance scheme, the Google link carries the https URL
20
+ * encoded, and the two warnings fire on the deployments where the link
21
+ * would not work or would leak.
22
+ */
23
+
24
+ const TOKEN: string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP_";
25
+
26
+ describe("OnCallCalendarFeedUrls", () => {
27
+ describe("route paths", () => {
28
+ test("the token is 43 characters (the shape the routes accept)", () => {
29
+ expect(CalendarFeedToken.isValidShape(TOKEN)).toBe(true);
30
+ });
31
+
32
+ test("personal feed path", () => {
33
+ expect(
34
+ OnCallCalendarFeedUrls.getFeedRoutePath(
35
+ OnCallCalendarFeedKind.Personal,
36
+ TOKEN,
37
+ ),
38
+ ).toBe(`${ON_CALL_CALENDAR_ROUTE_PREFIX}/user/${TOKEN}/shifts.ics`);
39
+ });
40
+
41
+ test("schedule feed path", () => {
42
+ expect(
43
+ OnCallCalendarFeedUrls.getFeedRoutePath(
44
+ OnCallCalendarFeedKind.Schedule,
45
+ TOKEN,
46
+ ),
47
+ ).toBe(`${ON_CALL_CALENDAR_ROUTE_PREFIX}/schedule/${TOKEN}/schedule.ics`);
48
+ });
49
+
50
+ test("project feed path", () => {
51
+ expect(
52
+ OnCallCalendarFeedUrls.getFeedRoutePath(
53
+ OnCallCalendarFeedKind.Project,
54
+ TOKEN,
55
+ ),
56
+ ).toBe(`${ON_CALL_CALENDAR_ROUTE_PREFIX}/project/${TOKEN}/project.ics`);
57
+ });
58
+
59
+ test("the public path carries the /api prefix", () => {
60
+ expect(
61
+ OnCallCalendarFeedUrls.getFeedPath(
62
+ OnCallCalendarFeedKind.Personal,
63
+ TOKEN,
64
+ ),
65
+ ).toBe(`/api/on-call-calendar/user/${TOKEN}/shifts.ics`);
66
+ });
67
+
68
+ test("the three kind segments are exactly what the Nginx location matches", () => {
69
+ const nginxLocation: RegExp =
70
+ /^\/api\/on-call-calendar\/(user|schedule|project)\//;
71
+
72
+ for (const kind of [
73
+ OnCallCalendarFeedKind.Personal,
74
+ OnCallCalendarFeedKind.Schedule,
75
+ OnCallCalendarFeedKind.Project,
76
+ ]) {
77
+ expect(OnCallCalendarFeedUrls.getFeedPath(kind, TOKEN)).toMatch(
78
+ nginxLocation,
79
+ );
80
+ }
81
+ });
82
+
83
+ test("file names", () => {
84
+ expect(
85
+ OnCallCalendarFeedUrls.getFileName(OnCallCalendarFeedKind.Personal),
86
+ ).toBe(PERSONAL_FEED_FILE_NAME);
87
+ expect(
88
+ OnCallCalendarFeedUrls.getFileName(OnCallCalendarFeedKind.Schedule),
89
+ ).toBe(SCHEDULE_FEED_FILE_NAME);
90
+ expect(
91
+ OnCallCalendarFeedUrls.getFileName(OnCallCalendarFeedKind.Project),
92
+ ).toBe(PROJECT_FEED_FILE_NAME);
93
+ expect(PERSONAL_FEED_FILE_NAME.endsWith(".ics")).toBe(true);
94
+ });
95
+
96
+ test("kind segments", () => {
97
+ expect(
98
+ OnCallCalendarFeedUrls.getKindSegment(OnCallCalendarFeedKind.Personal),
99
+ ).toBe("user");
100
+ expect(
101
+ OnCallCalendarFeedUrls.getKindSegment(OnCallCalendarFeedKind.Schedule),
102
+ ).toBe("schedule");
103
+ expect(
104
+ OnCallCalendarFeedUrls.getKindSegment(OnCallCalendarFeedKind.Project),
105
+ ).toBe("project");
106
+ });
107
+
108
+ test("a token with URL-unsafe characters is percent-encoded rather than trusted", () => {
109
+ const path: string = OnCallCalendarFeedUrls.getFeedRoutePath(
110
+ OnCallCalendarFeedKind.Personal,
111
+ "a/b?c",
112
+ );
113
+
114
+ expect(path).toBe(
115
+ `${ON_CALL_CALENDAR_ROUTE_PREFIX}/user/a%2Fb%3Fc/shifts.ics`,
116
+ );
117
+ });
118
+ });
119
+
120
+ describe("buildFeedUrls", () => {
121
+ test("https instance: https URL, webcals:// link and an encoded Google link", () => {
122
+ const urls: FeedUrls = OnCallCalendarFeedUrls.buildFeedUrls({
123
+ kind: OnCallCalendarFeedKind.Personal,
124
+ token: TOKEN,
125
+ host: "oneuptime.example.com",
126
+ protocol: Protocol.HTTPS,
127
+ });
128
+
129
+ expect(urls.https).toBe(
130
+ `https://oneuptime.example.com/api/on-call-calendar/user/${TOKEN}/shifts.ics`,
131
+ );
132
+ expect(urls.webcal).toBe(
133
+ `webcals://oneuptime.example.com/api/on-call-calendar/user/${TOKEN}/shifts.ics`,
134
+ );
135
+ expect(urls.googleAdd).toBe(
136
+ `${GOOGLE_CALENDAR_ADD_BY_URL}${encodeURIComponent(urls.https)}`,
137
+ );
138
+ });
139
+
140
+ test("http instance: the `https` key still carries the instance URL, and webcal is webcal://", () => {
141
+ const urls: FeedUrls = OnCallCalendarFeedUrls.buildFeedUrls({
142
+ kind: OnCallCalendarFeedKind.Schedule,
143
+ token: TOKEN,
144
+ host: "oneuptime.internal:3002",
145
+ protocol: Protocol.HTTP,
146
+ });
147
+
148
+ expect(urls.https).toBe(
149
+ `http://oneuptime.internal:3002/api/on-call-calendar/schedule/${TOKEN}/schedule.ics`,
150
+ );
151
+ expect(urls.webcal.startsWith("webcal://oneuptime.internal:3002/")).toBe(
152
+ true,
153
+ );
154
+ expect(urls.webcal.startsWith("webcals://")).toBe(false);
155
+ });
156
+
157
+ test("the Google link decodes back to the https URL", () => {
158
+ const urls: FeedUrls = OnCallCalendarFeedUrls.buildFeedUrls({
159
+ kind: OnCallCalendarFeedKind.Project,
160
+ token: TOKEN,
161
+ host: "oneuptime.example.com",
162
+ protocol: Protocol.HTTPS,
163
+ });
164
+
165
+ const cid: string = urls.googleAdd.slice(
166
+ GOOGLE_CALENDAR_ADD_BY_URL.length,
167
+ );
168
+
169
+ expect(decodeURIComponent(cid)).toBe(urls.https);
170
+ expect(cid).not.toContain("/");
171
+ });
172
+
173
+ test("a HOST that carries a scheme or a trailing slash is normalised", () => {
174
+ const urls: FeedUrls = OnCallCalendarFeedUrls.buildFeedUrls({
175
+ kind: OnCallCalendarFeedKind.Personal,
176
+ token: TOKEN,
177
+ host: "https://oneuptime.example.com/",
178
+ protocol: Protocol.HTTPS,
179
+ });
180
+
181
+ expect(urls.https).toBe(
182
+ `https://oneuptime.example.com/api/on-call-calendar/user/${TOKEN}/shifts.ics`,
183
+ );
184
+ expect(urls.https).not.toContain("//api");
185
+ expect(urls.https).not.toContain("https://https://");
186
+ });
187
+
188
+ test("the webcal and https links differ only in scheme", () => {
189
+ const urls: FeedUrls = OnCallCalendarFeedUrls.buildFeedUrls({
190
+ kind: OnCallCalendarFeedKind.Personal,
191
+ token: TOKEN,
192
+ host: "oneuptime.example.com",
193
+ protocol: Protocol.HTTPS,
194
+ });
195
+
196
+ expect(urls.webcal.replace(/^webcals:\/\//, "https://")).toBe(urls.https);
197
+ });
198
+
199
+ test("the defaults come from the environment (a string either way)", () => {
200
+ const urls: FeedUrls = OnCallCalendarFeedUrls.buildFeedUrls({
201
+ kind: OnCallCalendarFeedKind.Personal,
202
+ token: TOKEN,
203
+ });
204
+
205
+ expect(typeof urls.https).toBe("string");
206
+ expect(urls.https).toContain(
207
+ `/api/on-call-calendar/user/${TOKEN}/shifts.ics`,
208
+ );
209
+ expect(urls.https.startsWith("http")).toBe(true);
210
+ });
211
+ });
212
+
213
+ describe("getHostWarning", () => {
214
+ test.each([
215
+ "",
216
+ " ",
217
+ "localhost",
218
+ "localhost:3002",
219
+ "127.0.0.1",
220
+ "127.0.0.1:80",
221
+ "0.0.0.0",
222
+ "[::1]:3002",
223
+ "http://localhost/",
224
+ ])("warns for %j", (host: string) => {
225
+ expect(OnCallCalendarFeedUrls.getHostWarning(host)).toBe(HOST_WARNING);
226
+ });
227
+
228
+ test.each([
229
+ "oneuptime.example.com",
230
+ "oneuptime.example.com:8443",
231
+ "10.0.0.12",
232
+ "10.0.0.12:3002",
233
+ "oneuptime.internal",
234
+ ])("is silent for %j", (host: string) => {
235
+ expect(OnCallCalendarFeedUrls.getHostWarning(host)).toBeNull();
236
+ });
237
+
238
+ test("a hostname that merely CONTAINS localhost is fine", () => {
239
+ expect(
240
+ OnCallCalendarFeedUrls.getHostWarning("localhost.example.com"),
241
+ ).toBeNull();
242
+ });
243
+ });
244
+
245
+ describe("getProtocolWarning", () => {
246
+ test("http warns", () => {
247
+ expect(OnCallCalendarFeedUrls.getProtocolWarning(Protocol.HTTP)).toBe(
248
+ PROTOCOL_WARNING,
249
+ );
250
+ });
251
+
252
+ test("https is silent", () => {
253
+ expect(
254
+ OnCallCalendarFeedUrls.getProtocolWarning(Protocol.HTTPS),
255
+ ).toBeNull();
256
+ });
257
+
258
+ test("the default reads the environment and is a string or null", () => {
259
+ const warning: string | null =
260
+ OnCallCalendarFeedUrls.getProtocolWarning();
261
+
262
+ expect(warning === null || warning === PROTOCOL_WARNING).toBe(true);
263
+ });
264
+ });
265
+
266
+ describe("normalizeHost", () => {
267
+ test.each([
268
+ ["example.com", "example.com"],
269
+ ["https://example.com", "example.com"],
270
+ ["http://example.com/", "example.com"],
271
+ ["example.com///", "example.com"],
272
+ [" example.com:3002 ", "example.com:3002"],
273
+ ["", ""],
274
+ ])("%j -> %j", (input: string, expected: string) => {
275
+ expect(OnCallCalendarFeedUrls.normalizeHost(input)).toBe(expected);
276
+ });
277
+ });
278
+ });
@@ -0,0 +1,440 @@
1
+ import { FindOperator } from "typeorm";
2
+ import { jest } from "@jest/globals";
3
+ import OnCallDutyPolicyScheduleService from "../../../../Server/Services/OnCallDutyPolicyScheduleService";
4
+ import OnCallDutyPolicyScheduleLayerService from "../../../../Server/Services/OnCallDutyPolicyScheduleLayerService";
5
+ import OnCallDutyPolicyScheduleLayerUserService from "../../../../Server/Services/OnCallDutyPolicyScheduleLayerUserService";
6
+ import OnCallDutyPolicyEscalationRuleScheduleService from "../../../../Server/Services/OnCallDutyPolicyEscalationRuleScheduleService";
7
+ import OnCallDutyPolicyUserOverrideService from "../../../../Server/Services/OnCallDutyPolicyUserOverrideService";
8
+ import ProjectService from "../../../../Server/Services/ProjectService";
9
+ import UserService from "../../../../Server/Services/UserService";
10
+ import OneUptimeDate from "../../../../Types/Date";
11
+ import EventInterval from "../../../../Types/Events/EventInterval";
12
+ import Recurring from "../../../../Types/Events/Recurring";
13
+ import ObjectID from "../../../../Types/ObjectID";
14
+ import RestrictionTimes from "../../../../Types/OnCallDutyPolicy/RestrictionTimes";
15
+ import {
16
+ noRestriction,
17
+ rotation,
18
+ } from "../../../Types/OnCallDutyPolicy/CalendarFeedTestFixtures";
19
+
20
+ /*
21
+ * An in-memory stand-in for the handful of tables the shift resolver and the
22
+ * materializer read. Every service read (`findBy`, `findOneById`,
23
+ * `findOneBy`) is spied and answered from these rows, with the query
24
+ * interpreted just far enough to honour equality, IN, IS NULL, <= / >= and
25
+ * "= x OR IS NULL" — the operators QueryHelper builds — so the REAL service
26
+ * code runs end to end against the same fixture the tests inspect.
27
+ *
28
+ * Rows are plain objects shaped like the model instances the code reads:
29
+ * `_id` for queries, `id` for the getter the services use, relations as
30
+ * nested `{ id }` objects.
31
+ */
32
+
33
+ export type FakeRow = Record<string, unknown>;
34
+
35
+ export interface FakeDb {
36
+ schedules: Array<FakeRow>;
37
+ layers: Array<FakeRow>;
38
+ layerUsers: Array<FakeRow>;
39
+ attachments: Array<FakeRow>;
40
+ overrides: Array<FakeRow>;
41
+ users: Array<FakeRow>;
42
+ projects: Array<FakeRow>;
43
+ }
44
+
45
+ export function emptyDb(): FakeDb {
46
+ return {
47
+ schedules: [],
48
+ layers: [],
49
+ layerUsers: [],
50
+ attachments: [],
51
+ overrides: [],
52
+ users: [],
53
+ projects: [],
54
+ };
55
+ }
56
+
57
+ function toComparable(value: unknown): number {
58
+ if (value instanceof Date) {
59
+ return value.getTime();
60
+ }
61
+ if (typeof value === "number") {
62
+ return value;
63
+ }
64
+ const parsed: Date = new Date(String(value));
65
+ return parsed.getTime();
66
+ }
67
+
68
+ function toKey(value: unknown): string {
69
+ if (value === null || value === undefined) {
70
+ return "";
71
+ }
72
+ return String(value);
73
+ }
74
+
75
+ /*
76
+ * Interpret one query value against one row value. Unknown operators are
77
+ * treated as "matches" so a new QueryHelper construct degrades to "return
78
+ * everything" rather than to a silent empty result.
79
+ */
80
+ export function matchesValue(queryValue: unknown, rowValue: unknown): boolean {
81
+ if (queryValue === undefined) {
82
+ return true;
83
+ }
84
+
85
+ if (queryValue instanceof FindOperator) {
86
+ const generator: ((alias: string) => string) | undefined =
87
+ queryValue.getSql as ((alias: string) => string) | undefined;
88
+ const sql: string = generator ? generator("col") : "";
89
+ const parameters: Array<unknown> = Object.values(
90
+ queryValue.objectLiteralParameters || {},
91
+ );
92
+ const first: unknown = parameters[0];
93
+
94
+ if (sql.includes("TRUE = FALSE")) {
95
+ return false;
96
+ }
97
+
98
+ if (sql.includes(" or ") && sql.includes("IS NULL")) {
99
+ if (rowValue === null || rowValue === undefined) {
100
+ return true;
101
+ }
102
+ return parameters.map(toKey).includes(toKey(rowValue));
103
+ }
104
+
105
+ if (sql.includes("IS NOT NULL")) {
106
+ return rowValue !== null && rowValue !== undefined;
107
+ }
108
+
109
+ if (sql.includes("IS NULL")) {
110
+ return rowValue === null || rowValue === undefined;
111
+ }
112
+
113
+ if (sql.includes("IN (") && Array.isArray(first)) {
114
+ return (first as Array<unknown>).map(toKey).includes(toKey(rowValue));
115
+ }
116
+
117
+ if (sql.includes("<=")) {
118
+ return toComparable(rowValue) <= toComparable(first);
119
+ }
120
+
121
+ if (sql.includes(">=")) {
122
+ return toComparable(rowValue) >= toComparable(first);
123
+ }
124
+
125
+ return true;
126
+ }
127
+
128
+ return toKey(queryValue) === toKey(rowValue);
129
+ }
130
+
131
+ export function matchesQuery(
132
+ query: Record<string, unknown> | undefined,
133
+ row: FakeRow,
134
+ ): boolean {
135
+ if (!query) {
136
+ return true;
137
+ }
138
+
139
+ for (const [key, value] of Object.entries(query)) {
140
+ if (!matchesValue(value, row[key])) {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ return true;
146
+ }
147
+
148
+ function applySort(
149
+ rows: Array<FakeRow>,
150
+ sort: Record<string, unknown> | undefined,
151
+ ): Array<FakeRow> {
152
+ if (!sort) {
153
+ return rows;
154
+ }
155
+
156
+ const [key, direction] = Object.entries(sort)[0] || [];
157
+
158
+ if (!key) {
159
+ return rows;
160
+ }
161
+
162
+ const descending: boolean = String(direction)
163
+ .toLowerCase()
164
+ .startsWith("desc");
165
+
166
+ return [...rows].sort((a: FakeRow, b: FakeRow) => {
167
+ const left: number = toComparable(a[key]);
168
+ const right: number = toComparable(b[key]);
169
+ return descending ? right - left : left - right;
170
+ });
171
+ }
172
+
173
+ type FindByArgs = {
174
+ query?: Record<string, unknown>;
175
+ sort?: Record<string, unknown>;
176
+ };
177
+
178
+ function fakeFindBy(
179
+ table: () => Array<FakeRow>,
180
+ ): (args: FindByArgs) => Promise<Array<FakeRow>> {
181
+ return (args: FindByArgs): Promise<Array<FakeRow>> => {
182
+ const rows: Array<FakeRow> = table().filter((row: FakeRow) => {
183
+ return matchesQuery(args.query, row);
184
+ });
185
+ return Promise.resolve(applySort(rows, args.sort));
186
+ };
187
+ }
188
+
189
+ function fakeFindOneBy(
190
+ table: () => Array<FakeRow>,
191
+ ): (args: FindByArgs) => Promise<FakeRow | null> {
192
+ return (args: FindByArgs): Promise<FakeRow | null> => {
193
+ const row: FakeRow | undefined = table().find((candidate: FakeRow) => {
194
+ return matchesQuery(args.query, candidate);
195
+ });
196
+ return Promise.resolve(row || null);
197
+ };
198
+ }
199
+
200
+ function fakeFindOneById(
201
+ table: () => Array<FakeRow>,
202
+ ): (args: { id: ObjectID }) => Promise<FakeRow | null> {
203
+ return (args: { id: ObjectID }): Promise<FakeRow | null> => {
204
+ const row: FakeRow | undefined = table().find((candidate: FakeRow) => {
205
+ return toKey(candidate["_id"]) === toKey(args.id);
206
+ });
207
+ return Promise.resolve(row || null);
208
+ };
209
+ }
210
+
211
+ /*
212
+ * Spy every read the resolver / materializer / roster path performs and
213
+ * answer it from `db`. Call inside a test (or beforeEach); restore with
214
+ * jest.restoreAllMocks().
215
+ */
216
+ export function installFakeDb(db: FakeDb): void {
217
+ jest.spyOn(OnCallDutyPolicyScheduleService, "findBy").mockImplementation(
218
+ fakeFindBy(() => {
219
+ return db.schedules;
220
+ }) as never,
221
+ );
222
+ jest.spyOn(OnCallDutyPolicyScheduleService, "findOneById").mockImplementation(
223
+ fakeFindOneById(() => {
224
+ return db.schedules;
225
+ }) as never,
226
+ );
227
+ jest.spyOn(OnCallDutyPolicyScheduleService, "findOneBy").mockImplementation(
228
+ fakeFindOneBy(() => {
229
+ return db.schedules;
230
+ }) as never,
231
+ );
232
+
233
+ jest.spyOn(OnCallDutyPolicyScheduleLayerService, "findBy").mockImplementation(
234
+ fakeFindBy(() => {
235
+ return db.layers;
236
+ }) as never,
237
+ );
238
+
239
+ jest
240
+ .spyOn(OnCallDutyPolicyScheduleLayerUserService, "findBy")
241
+ .mockImplementation(
242
+ fakeFindBy(() => {
243
+ return db.layerUsers;
244
+ }) as never,
245
+ );
246
+
247
+ jest
248
+ .spyOn(OnCallDutyPolicyEscalationRuleScheduleService, "findBy")
249
+ .mockImplementation(
250
+ fakeFindBy(() => {
251
+ return db.attachments;
252
+ }) as never,
253
+ );
254
+
255
+ jest.spyOn(OnCallDutyPolicyUserOverrideService, "findBy").mockImplementation(
256
+ fakeFindBy(() => {
257
+ return db.overrides;
258
+ }) as never,
259
+ );
260
+
261
+ jest.spyOn(UserService, "findBy").mockImplementation(
262
+ fakeFindBy(() => {
263
+ return db.users;
264
+ }) as never,
265
+ );
266
+
267
+ jest.spyOn(ProjectService, "findBy").mockImplementation(
268
+ fakeFindBy(() => {
269
+ return db.projects;
270
+ }) as never,
271
+ );
272
+ }
273
+
274
+ // -- Row builders ----------------------------------------------------------
275
+
276
+ export function oid(value: string): ObjectID {
277
+ return new ObjectID(value);
278
+ }
279
+
280
+ export const DEFAULT_UPDATED_AT: Date = OneUptimeDate.fromString(
281
+ "2026-08-01T10:00:00Z",
282
+ );
283
+
284
+ export function makeSchedule(data: {
285
+ id: string;
286
+ projectId: string;
287
+ name?: string | undefined;
288
+ timezone?: string | undefined;
289
+ shiftConfigVersion?: number | undefined;
290
+ createdAt?: Date | undefined;
291
+ updatedAt?: Date | undefined;
292
+ }): FakeRow {
293
+ return {
294
+ _id: oid(data.id),
295
+ id: oid(data.id),
296
+ projectId: oid(data.projectId),
297
+ name: data.name ?? `Schedule ${data.id}`,
298
+ timezone: data.timezone,
299
+ shiftConfigVersion: data.shiftConfigVersion ?? 0,
300
+ createdAt:
301
+ data.createdAt ?? OneUptimeDate.fromString("2026-01-01T00:00:00Z"),
302
+ // Deliberately "recent": the resolver must NOT pick this up.
303
+ updatedAt:
304
+ data.updatedAt ?? OneUptimeDate.fromString("2026-12-31T00:00:00Z"),
305
+ };
306
+ }
307
+
308
+ export function makeLayer(data: {
309
+ id: string;
310
+ scheduleId: string;
311
+ projectId: string;
312
+ name?: string | undefined;
313
+ order?: number | undefined;
314
+ startsAt: Date;
315
+ handOffTime: Date;
316
+ rotation?: Recurring | undefined;
317
+ restrictionTimes?: RestrictionTimes | undefined;
318
+ updatedAt?: Date | undefined;
319
+ }): FakeRow {
320
+ return {
321
+ _id: oid(data.id),
322
+ id: oid(data.id),
323
+ onCallDutyPolicyScheduleId: oid(data.scheduleId),
324
+ projectId: oid(data.projectId),
325
+ name: data.name ?? `Layer ${data.id}`,
326
+ description: "",
327
+ order: data.order ?? 1,
328
+ startsAt: data.startsAt,
329
+ handOffTime: data.handOffTime,
330
+ rotation: data.rotation ?? rotation(EventInterval.Day, 1),
331
+ restrictionTimes: data.restrictionTimes ?? noRestriction(),
332
+ updatedAt: data.updatedAt ?? DEFAULT_UPDATED_AT,
333
+ };
334
+ }
335
+
336
+ export function makeLayerUser(data: {
337
+ id: string;
338
+ scheduleId: string;
339
+ layerId: string;
340
+ projectId: string;
341
+ userId: string;
342
+ order?: number | undefined;
343
+ updatedAt?: Date | undefined;
344
+ }): FakeRow {
345
+ return {
346
+ _id: oid(data.id),
347
+ id: oid(data.id),
348
+ onCallDutyPolicyScheduleId: oid(data.scheduleId),
349
+ onCallDutyPolicyScheduleLayerId: oid(data.layerId),
350
+ projectId: oid(data.projectId),
351
+ userId: oid(data.userId),
352
+ user: { id: oid(data.userId), _id: oid(data.userId) },
353
+ order: data.order ?? 1,
354
+ updatedAt: data.updatedAt ?? DEFAULT_UPDATED_AT,
355
+ };
356
+ }
357
+
358
+ export function makeAttachment(data: {
359
+ id: string;
360
+ scheduleId: string;
361
+ projectId: string;
362
+ policyId: string;
363
+ policyName?: string | undefined;
364
+ ruleId: string;
365
+ ruleName?: string | undefined;
366
+ ruleOrder?: number | undefined;
367
+ updatedAt?: Date | undefined;
368
+ }): FakeRow {
369
+ return {
370
+ _id: oid(data.id),
371
+ id: oid(data.id),
372
+ onCallDutyPolicyScheduleId: oid(data.scheduleId),
373
+ projectId: oid(data.projectId),
374
+ onCallDutyPolicyId: oid(data.policyId),
375
+ onCallDutyPolicyEscalationRuleId: oid(data.ruleId),
376
+ onCallDutyPolicy: {
377
+ id: oid(data.policyId),
378
+ _id: oid(data.policyId),
379
+ name: data.policyName ?? `Policy ${data.policyId}`,
380
+ },
381
+ onCallDutyPolicyEscalationRule: {
382
+ id: oid(data.ruleId),
383
+ _id: oid(data.ruleId),
384
+ name: data.ruleName ?? `Rule ${data.ruleId}`,
385
+ order: data.ruleOrder ?? 1,
386
+ },
387
+ onCallDutyPolicySchedule: {
388
+ id: oid(data.scheduleId),
389
+ _id: oid(data.scheduleId),
390
+ name: `Schedule ${data.scheduleId}`,
391
+ },
392
+ updatedAt: data.updatedAt ?? DEFAULT_UPDATED_AT,
393
+ };
394
+ }
395
+
396
+ export function makeOverride(data: {
397
+ id: string;
398
+ projectId: string;
399
+ overrideUserId: string;
400
+ routeAlertsToUserId: string;
401
+ startsAt: Date;
402
+ endsAt: Date;
403
+ policyId?: string | undefined;
404
+ updatedAt?: Date | undefined;
405
+ }): FakeRow {
406
+ return {
407
+ _id: oid(data.id),
408
+ id: oid(data.id),
409
+ projectId: oid(data.projectId),
410
+ overrideUserId: oid(data.overrideUserId),
411
+ routeAlertsToUserId: oid(data.routeAlertsToUserId),
412
+ startsAt: data.startsAt,
413
+ endsAt: data.endsAt,
414
+ onCallDutyPolicyId: data.policyId ? oid(data.policyId) : null,
415
+ updatedAt: data.updatedAt ?? DEFAULT_UPDATED_AT,
416
+ };
417
+ }
418
+
419
+ export function makeUser(data: {
420
+ id: string;
421
+ name?: string | undefined;
422
+ email?: string | undefined;
423
+ timezone?: string | undefined;
424
+ }): FakeRow {
425
+ return {
426
+ _id: oid(data.id),
427
+ id: oid(data.id),
428
+ name: data.name,
429
+ email: data.email,
430
+ timezone: data.timezone,
431
+ };
432
+ }
433
+
434
+ export function makeProject(data: { id: string; name: string }): FakeRow {
435
+ return {
436
+ _id: oid(data.id),
437
+ id: oid(data.id),
438
+ name: data.name,
439
+ };
440
+ }