@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,489 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
10
+ import { cleanup, render, screen, waitFor } from "@testing-library/react";
11
+ import React from "react";
12
+ import getJestMockFunction, { MockFunction } from "../../MockType";
13
+
14
+ /*
15
+ * The Growth-plan gate on calendar feeds.
16
+ *
17
+ * All three feed models carry @TableBillingAccessControl(Growth), and the
18
+ * public render path checks the plan again per project: a below-plan project
19
+ * is served an EMPTY calendar (decision 1.3), never a 404. Without a
20
+ * client-side gate the dashboard cheerfully mints a link that can never show
21
+ * a shift, and the settings card answers a save with a bare 402.
22
+ *
23
+ * What these tests pin:
24
+ * - on a below-plan Cloud project, Generate / Publish are replaced by an
25
+ * upgrade notice that links to Billing, and the settings card is gone;
26
+ * - a sufficient plan, an unknown plan and a self-hosted install (billing
27
+ * disabled) all leave the surfaces exactly as they were - the gate FAILS
28
+ * OPEN, because hiding the feature from somebody entitled to it is the
29
+ * worse mistake.
30
+ */
31
+
32
+ let billingEnabledForTest: boolean = false;
33
+ let currentPlanForTest: string | null = null;
34
+
35
+ const getMock: MockFunction = getJestMockFunction();
36
+ const postMock: MockFunction = getJestMockFunction();
37
+
38
+ const PLAN_ENV: Record<string, string> = {
39
+ SUBSCRIPTION_PLAN_BASIC: "Free,priceMonthlyId,priceYearlyId,0,0,1,0",
40
+ SUBSCRIPTION_PLAN_GROWTH: "Growth,priceMonthlyId2,priceYearlyId2,0,0,2,14",
41
+ SUBSCRIPTION_PLAN_SCALE: "Scale,priceMonthlyId3,priceYearlyId3,0,0,3,0",
42
+ };
43
+
44
+ jest.mock("../../../UI/Config", () => {
45
+ const actual: Record<string, unknown> = jest.requireActual(
46
+ "../../../UI/Config",
47
+ ) as Record<string, unknown>;
48
+
49
+ const mocked: Record<string, unknown> = { ...actual };
50
+
51
+ Object.defineProperty(mocked, "BILLING_ENABLED", {
52
+ get: (): boolean => {
53
+ return billingEnabledForTest;
54
+ },
55
+ });
56
+
57
+ mocked["getAllEnvVars"] = (): Record<string, string> => {
58
+ return PLAN_ENV;
59
+ };
60
+
61
+ return mocked;
62
+ });
63
+
64
+ jest.mock("../../../UI/Utils/Project", () => {
65
+ const actual: Record<string, any> = jest.requireActual(
66
+ "../../../UI/Utils/Project",
67
+ ) as Record<string, any>;
68
+
69
+ return {
70
+ __esModule: true,
71
+ ...actual,
72
+ default: {
73
+ ...actual["default"],
74
+ getCurrentPlan: (): string | null => {
75
+ return currentPlanForTest;
76
+ },
77
+ getCurrentProjectId: (): null => {
78
+ return null;
79
+ },
80
+ },
81
+ };
82
+ });
83
+
84
+ jest.mock("../../../UI/Utils/API/API", () => {
85
+ return {
86
+ __esModule: true,
87
+ default: {
88
+ get: (...args: Array<any>) => {
89
+ return getMock(...args);
90
+ },
91
+ post: (...args: Array<any>) => {
92
+ return postMock(...args);
93
+ },
94
+ getFriendlyMessage: (error: unknown): string => {
95
+ if (
96
+ error &&
97
+ typeof error === "object" &&
98
+ "message" in (error as Record<string, unknown>)
99
+ ) {
100
+ return String((error as Record<string, unknown>)["message"]);
101
+ }
102
+
103
+ return "Something went wrong";
104
+ },
105
+ },
106
+ };
107
+ });
108
+
109
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
110
+ return {
111
+ __esModule: true,
112
+ default: {
113
+ getCommonHeaders: (): Record<string, string> => {
114
+ return {};
115
+ },
116
+ updateById: async (): Promise<void> => {
117
+ return undefined;
118
+ },
119
+ deleteItem: async (): Promise<void> => {
120
+ return undefined;
121
+ },
122
+ getList: async (): Promise<Record<string, unknown>> => {
123
+ return { data: [], count: 0, skip: 0, limit: 10 };
124
+ },
125
+ },
126
+ };
127
+ });
128
+
129
+ jest.mock("../../../UI/Components/ModelDetail/CardModelDetail", () => {
130
+ return {
131
+ __esModule: true,
132
+ default: (): unknown => {
133
+ return React.createElement("div", {
134
+ "data-testid": "card-model-detail-stub",
135
+ });
136
+ },
137
+ };
138
+ });
139
+
140
+ jest.mock("../../../UI/Utils/User", () => {
141
+ return {
142
+ __esModule: true,
143
+ default: {
144
+ isMasterAdmin: (): boolean => {
145
+ return true;
146
+ },
147
+ getUserId: (): null => {
148
+ return null;
149
+ },
150
+ },
151
+ };
152
+ });
153
+
154
+ jest.mock("../../../UI/Utils/Permission", () => {
155
+ return {
156
+ __esModule: true,
157
+ default: {
158
+ getAllPermissions: (): Array<unknown> => {
159
+ return [];
160
+ },
161
+ getProjectPermissions: (): null => {
162
+ return null;
163
+ },
164
+ getGlobalPermissions: (): { globalPermissions: Array<unknown> } => {
165
+ return { globalPermissions: [] };
166
+ },
167
+ },
168
+ };
169
+ });
170
+
171
+ jest.mock("react-i18next", () => {
172
+ return {
173
+ useTranslation: () => {
174
+ return {
175
+ t: (key: string, options?: { defaultValue?: string }): string => {
176
+ return options?.defaultValue ?? key;
177
+ },
178
+ };
179
+ },
180
+ };
181
+ });
182
+
183
+ import ObjectID from "../../../Types/ObjectID";
184
+ import HTTPResponse from "../../../Types/API/HTTPResponse";
185
+ import Route from "../../../Types/API/Route";
186
+ import { JSONObject } from "../../../Types/JSON";
187
+ import { isCalendarFeedAccessibleOnCurrentPlan } from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/CalendarFeed/CalendarFeedPlanGate";
188
+ import PersonalCalendarFeedCard, {
189
+ PersonalCalendarFeedVariant,
190
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/CalendarFeed/PersonalCalendarFeedCard";
191
+ import SharedCalendarFeedCard, {
192
+ SharedCalendarFeedKind,
193
+ } from "../../../../App/FeatureSet/Dashboard/src/Components/OnCallPolicy/CalendarFeed/SharedCalendarFeedCard";
194
+ import PageMap from "../../../../App/FeatureSet/Dashboard/src/Utils/PageMap";
195
+ import RouteMap, {
196
+ RouteUtil,
197
+ } from "../../../../App/FeatureSet/Dashboard/src/Utils/RouteMap";
198
+
199
+ const PROJECT_ID: string = "8f2a1b3c-4d5e-4f60-9a7b-1c2d3e4f5a6b";
200
+ const FEED_ID: string = "11111111-1111-4111-8111-111111111111";
201
+ const SCHEDULE_ID: ObjectID = new ObjectID(
202
+ "33333333-3333-4333-8333-333333333333",
203
+ );
204
+ const NOW: Date = new Date("2026-08-31T12:00:00.000Z");
205
+
206
+ const HTTPS_URL: string =
207
+ "https://oneuptime.example.com/api/on-call-calendar/user/abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG/shifts.ics";
208
+
209
+ const EMPTY_STATUS_JSON: JSONObject = {
210
+ exists: false,
211
+ feedId: null,
212
+ isEnabled: false,
213
+ needsRegeneration: false,
214
+ tokenHint: null,
215
+ rotatedAt: null,
216
+ previousTokenExpiresAt: null,
217
+ lastFetchedAt: null,
218
+ lastFetchedClient: null,
219
+ fetchCount: 0,
220
+ lastRenderTruncated: false,
221
+ settings: { pastDays: 2, futureDays: 90 },
222
+ urls: null,
223
+ hostWarning: null,
224
+ protocolWarning: null,
225
+ };
226
+
227
+ const ACTIVE_STATUS_JSON: JSONObject = {
228
+ ...EMPTY_STATUS_JSON,
229
+ exists: true,
230
+ feedId: FEED_ID,
231
+ isEnabled: true,
232
+ tokenHint: "k3Qx",
233
+ rotatedAt: "2026-08-01T10:00:00.000Z",
234
+ urls: {
235
+ https: HTTPS_URL,
236
+ webcal: HTTPS_URL.replace("https:", "webcals:"),
237
+ googleAdd: `https://calendar.google.com/calendar/r?cid=${encodeURIComponent(HTTPS_URL)}`,
238
+ },
239
+ };
240
+
241
+ type OkFunction = (json: JSONObject) => HTTPResponse<JSONObject>;
242
+
243
+ const ok: OkFunction = (json: JSONObject): HTTPResponse<JSONObject> => {
244
+ return new HTTPResponse<JSONObject>(200, json, {});
245
+ };
246
+
247
+ function goToCalendarFeedPage(): void {
248
+ window.history.pushState(
249
+ {},
250
+ "",
251
+ `/dashboard/${PROJECT_ID}/user-settings/calendar-feed`,
252
+ );
253
+ }
254
+
255
+ describe("isCalendarFeedAccessibleOnCurrentPlan", () => {
256
+ afterEach(() => {
257
+ billingEnabledForTest = false;
258
+ currentPlanForTest = null;
259
+ });
260
+
261
+ test("a self-hosted install (billing disabled) is always accessible", () => {
262
+ billingEnabledForTest = false;
263
+ currentPlanForTest = "Free";
264
+
265
+ expect(isCalendarFeedAccessibleOnCurrentPlan()).toBe(true);
266
+ });
267
+
268
+ test("an unknown plan fails open rather than hiding the feature", () => {
269
+ billingEnabledForTest = true;
270
+ currentPlanForTest = null;
271
+
272
+ expect(isCalendarFeedAccessibleOnCurrentPlan()).toBe(true);
273
+ });
274
+
275
+ test("a plan name the environment does not describe fails open instead of throwing", () => {
276
+ billingEnabledForTest = true;
277
+ currentPlanForTest = "NoSuchPlan";
278
+
279
+ expect(isCalendarFeedAccessibleOnCurrentPlan()).toBe(true);
280
+ });
281
+
282
+ test("Growth and above are accessible, below-Growth is not", () => {
283
+ billingEnabledForTest = true;
284
+
285
+ currentPlanForTest = "Free";
286
+ expect(isCalendarFeedAccessibleOnCurrentPlan()).toBe(false);
287
+
288
+ currentPlanForTest = "Growth";
289
+ expect(isCalendarFeedAccessibleOnCurrentPlan()).toBe(true);
290
+
291
+ currentPlanForTest = "Scale";
292
+ expect(isCalendarFeedAccessibleOnCurrentPlan()).toBe(true);
293
+ });
294
+ });
295
+
296
+ describe("PersonalCalendarFeedCard below the plan", () => {
297
+ beforeEach(() => {
298
+ getMock.mockReset();
299
+ postMock.mockReset();
300
+ goToCalendarFeedPage();
301
+ billingEnabledForTest = true;
302
+ currentPlanForTest = "Free";
303
+ });
304
+
305
+ afterEach(() => {
306
+ cleanup();
307
+ billingEnabledForTest = false;
308
+ currentPlanForTest = null;
309
+ });
310
+
311
+ test("Generate is replaced by an upgrade notice pointing at Billing", async () => {
312
+ getMock.mockResolvedValue(ok(EMPTY_STATUS_JSON));
313
+
314
+ render(
315
+ <PersonalCalendarFeedCard
316
+ variant={PersonalCalendarFeedVariant.Full}
317
+ now={NOW}
318
+ />,
319
+ );
320
+
321
+ await waitFor(() => {
322
+ expect(
323
+ screen.getByTestId("personal-calendar-feed-empty"),
324
+ ).toBeInTheDocument();
325
+ });
326
+
327
+ expect(
328
+ screen.getByTestId("personal-calendar-feed-plan-gate"),
329
+ ).toHaveTextContent("Calendar feeds require the Growth plan");
330
+ // The button that would mint a permanently-empty link is not offered.
331
+ expect(
332
+ screen.queryByTestId("personal-calendar-feed-generate"),
333
+ ).not.toBeInTheDocument();
334
+ expect(postMock).not.toHaveBeenCalled();
335
+
336
+ const billingRoute: Route = RouteUtil.populateRouteParams(
337
+ RouteMap[PageMap.SETTINGS_BILLING] as Route,
338
+ );
339
+ expect(
340
+ screen.getByText("Upgrade your plan in Billing settings").closest("a"),
341
+ ).toHaveAttribute("href", billingRoute.toString());
342
+ });
343
+
344
+ test("an existing (downgraded) feed explains itself and hides the settings card", async () => {
345
+ getMock.mockResolvedValue(ok(ACTIVE_STATUS_JSON));
346
+
347
+ render(
348
+ <PersonalCalendarFeedCard
349
+ variant={PersonalCalendarFeedVariant.Full}
350
+ now={NOW}
351
+ />,
352
+ );
353
+
354
+ await waitFor(() => {
355
+ expect(
356
+ screen.getByTestId("personal-calendar-feed-active"),
357
+ ).toBeInTheDocument();
358
+ });
359
+
360
+ expect(
361
+ screen.getByTestId("personal-calendar-feed-plan-gate"),
362
+ ).toBeInTheDocument();
363
+ /*
364
+ * The settings PUT is Growth-gated too, so the form would answer every
365
+ * save with a 402. Hidden rather than shown-and-broken.
366
+ */
367
+ expect(
368
+ screen.queryByTestId("card-model-detail-stub"),
369
+ ).not.toBeInTheDocument();
370
+ });
371
+
372
+ test("on a sufficient plan nothing changes: Generate and the settings card are there", async () => {
373
+ currentPlanForTest = "Growth";
374
+ getMock.mockResolvedValue(ok(ACTIVE_STATUS_JSON));
375
+
376
+ render(
377
+ <PersonalCalendarFeedCard
378
+ variant={PersonalCalendarFeedVariant.Full}
379
+ now={NOW}
380
+ />,
381
+ );
382
+
383
+ await waitFor(() => {
384
+ expect(
385
+ screen.getByTestId("personal-calendar-feed-active"),
386
+ ).toBeInTheDocument();
387
+ });
388
+
389
+ expect(
390
+ screen.queryByTestId("personal-calendar-feed-plan-gate"),
391
+ ).not.toBeInTheDocument();
392
+ expect(screen.getByTestId("card-model-detail-stub")).toBeInTheDocument();
393
+ });
394
+
395
+ test("a self-hosted install never sees the gate, whatever plan string is around", async () => {
396
+ billingEnabledForTest = false;
397
+ getMock.mockResolvedValue(ok(EMPTY_STATUS_JSON));
398
+
399
+ render(
400
+ <PersonalCalendarFeedCard
401
+ variant={PersonalCalendarFeedVariant.Full}
402
+ now={NOW}
403
+ />,
404
+ );
405
+
406
+ await waitFor(() => {
407
+ expect(
408
+ screen.getByTestId("personal-calendar-feed-generate"),
409
+ ).toBeInTheDocument();
410
+ });
411
+
412
+ expect(
413
+ screen.queryByTestId("personal-calendar-feed-plan-gate"),
414
+ ).not.toBeInTheDocument();
415
+ });
416
+ });
417
+
418
+ describe("SharedCalendarFeedCard below the plan", () => {
419
+ beforeEach(() => {
420
+ getMock.mockReset();
421
+ postMock.mockReset();
422
+ goToCalendarFeedPage();
423
+ billingEnabledForTest = true;
424
+ currentPlanForTest = "Free";
425
+ });
426
+
427
+ afterEach(() => {
428
+ cleanup();
429
+ billingEnabledForTest = false;
430
+ currentPlanForTest = null;
431
+ });
432
+
433
+ test("Publish is replaced by the upgrade notice, even for a project admin", async () => {
434
+ getMock.mockResolvedValue(ok(EMPTY_STATUS_JSON));
435
+
436
+ render(
437
+ <SharedCalendarFeedCard
438
+ kind={SharedCalendarFeedKind.Schedule}
439
+ scheduleId={SCHEDULE_ID}
440
+ scheduleTimezone="Europe/Stockholm"
441
+ now={NOW}
442
+ />,
443
+ );
444
+
445
+ await waitFor(() => {
446
+ expect(
447
+ screen.getByTestId("schedule-shared-calendar-feed-empty"),
448
+ ).toBeInTheDocument();
449
+ });
450
+
451
+ expect(
452
+ screen.getByTestId("schedule-shared-calendar-feed-plan-gate"),
453
+ ).toHaveTextContent("Calendar feeds require the Growth plan");
454
+ expect(
455
+ screen.queryByTestId("schedule-shared-calendar-feed-publish"),
456
+ ).not.toBeInTheDocument();
457
+ expect(postMock).not.toHaveBeenCalled();
458
+ });
459
+
460
+ /*
461
+ * The status read itself is Growth-gated, so on a below-plan project the
462
+ * card's own load() 402s and `status` stays null. Relaying the server's
463
+ * sentence with no way to act on it is what the gate replaces.
464
+ */
465
+ test("a 402 on the status read renders the upgrade notice, not a bare server error", async () => {
466
+ getMock.mockRejectedValue(
467
+ new Error("Please upgrade your plan to Growth to access this feature"),
468
+ );
469
+
470
+ render(
471
+ <SharedCalendarFeedCard
472
+ kind={SharedCalendarFeedKind.Project}
473
+ now={NOW}
474
+ />,
475
+ );
476
+
477
+ await waitFor(() => {
478
+ expect(
479
+ screen.getByTestId("project-shared-calendar-feed-plan-gate"),
480
+ ).toBeInTheDocument();
481
+ });
482
+
483
+ expect(
484
+ screen.queryByText(
485
+ "Please upgrade your plan to Growth to access this feature",
486
+ ),
487
+ ).not.toBeInTheDocument();
488
+ });
489
+ });
@@ -0,0 +1,149 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ jest,
7
+ test,
8
+ } from "@jest/globals";
9
+ import "@testing-library/jest-dom";
10
+ import { cleanup } from "@testing-library/react";
11
+ import * as React from "react";
12
+
13
+ /*
14
+ * The two side-menu entries that make the calendar feeds reachable: the
15
+ * "Calendar" section on User Settings (placed before Workspace, per the
16
+ * design) and the "Calendar Feeds" item under On-Call Duty's Schedules
17
+ * section. Both are rendered for real against the real RouteMap, so an entry
18
+ * that points at the wrong route, lands in the wrong section, or is dropped
19
+ * in a menu reshuffle fails here rather than in somebody's browser.
20
+ */
21
+
22
+ jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
23
+ return {
24
+ __esModule: true,
25
+ default: {
26
+ count: () => {
27
+ return Promise.resolve(0);
28
+ },
29
+ },
30
+ };
31
+ });
32
+
33
+ import UserSettingsSideMenu from "../../../../App/FeatureSet/Dashboard/src/Pages/UserSettings/SideMenu";
34
+ import OnCallDutySideMenu from "../../../../App/FeatureSet/Dashboard/src/Pages/OnCallDuty/SideMenu";
35
+ import PageMap from "../../../../App/FeatureSet/Dashboard/src/Utils/PageMap";
36
+ import {
37
+ DESKTOP_WIDTH,
38
+ MenuLink,
39
+ PROJECT_ID,
40
+ goTo,
41
+ hrefsInMenu,
42
+ linksIn,
43
+ renderMenu,
44
+ routeFor,
45
+ sectionTitlesInOrder,
46
+ setViewportWidth,
47
+ } from "./SideMenuHarness";
48
+
49
+ describe("User Settings side menu - Calendar section", () => {
50
+ beforeEach(() => {
51
+ setViewportWidth(DESKTOP_WIDTH);
52
+ goTo(`/dashboard/${PROJECT_ID}/user-settings/notification-methods`);
53
+ });
54
+
55
+ afterEach(() => {
56
+ cleanup();
57
+ });
58
+
59
+ test("has a Calendar section placed right before Workspace", async () => {
60
+ await renderMenu(<UserSettingsSideMenu />);
61
+
62
+ const titles: Array<string> = sectionTitlesInOrder();
63
+ const calendarIndex: number = titles.indexOf("Calendar");
64
+ const workspaceIndex: number = titles.indexOf("Workspace");
65
+
66
+ expect(calendarIndex).toBeGreaterThan(-1);
67
+ expect(workspaceIndex).toBeGreaterThan(-1);
68
+ expect(calendarIndex).toBe(workspaceIndex - 1);
69
+ });
70
+
71
+ test("the Calendar section holds exactly the Calendar Feed page", async () => {
72
+ await renderMenu(<UserSettingsSideMenu />);
73
+
74
+ const links: Array<MenuLink> = linksIn("Calendar");
75
+
76
+ expect(links).toEqual([
77
+ {
78
+ title: "Calendar Feed",
79
+ href: routeFor(PageMap.USER_SETTINGS_ON_CALL_CALENDAR_FEED),
80
+ },
81
+ ]);
82
+ expect(links[0]!.href).toBe(
83
+ `/dashboard/${PROJECT_ID}/user-settings/calendar-feed`,
84
+ );
85
+ });
86
+
87
+ test("no other section links to the Calendar Feed page", async () => {
88
+ await renderMenu(<UserSettingsSideMenu />);
89
+
90
+ const href: string = routeFor(PageMap.USER_SETTINGS_ON_CALL_CALENDAR_FEED);
91
+
92
+ expect(
93
+ hrefsInMenu().filter((candidate: string): boolean => {
94
+ return candidate === href;
95
+ }).length,
96
+ ).toBe(1);
97
+ });
98
+ });
99
+
100
+ describe("On-Call Duty side menu - Calendar Feeds item", () => {
101
+ beforeEach(() => {
102
+ setViewportWidth(DESKTOP_WIDTH);
103
+ goTo(`/dashboard/${PROJECT_ID}/on-call-duty/policies`);
104
+ });
105
+
106
+ afterEach(() => {
107
+ cleanup();
108
+ });
109
+
110
+ test("lists Calendar Feeds next to the schedules it exports", async () => {
111
+ await renderMenu(<OnCallDutySideMenu />);
112
+
113
+ const links: Array<MenuLink> = linksIn("Schedules");
114
+ const titles: Array<string> = links.map((link: MenuLink): string => {
115
+ return link.title;
116
+ });
117
+
118
+ expect(titles).toContain("On-Call Schedules");
119
+ expect(titles).toContain("Calendar Feeds");
120
+ expect(titles.indexOf("Calendar Feeds")).toBe(
121
+ titles.indexOf("On-Call Schedules") + 1,
122
+ );
123
+
124
+ const calendarFeeds: MenuLink | undefined = links.find(
125
+ (link: MenuLink): boolean => {
126
+ return link.title === "Calendar Feeds";
127
+ },
128
+ );
129
+
130
+ expect(calendarFeeds?.href).toBe(
131
+ routeFor(PageMap.ON_CALL_DUTY_CALENDAR_FEEDS),
132
+ );
133
+ expect(calendarFeeds?.href).toBe(
134
+ `/dashboard/${PROJECT_ID}/on-call-duty/calendar-feeds`,
135
+ );
136
+ });
137
+
138
+ test("the Calendar Feeds page is linked exactly once", async () => {
139
+ await renderMenu(<OnCallDutySideMenu />);
140
+
141
+ const href: string = routeFor(PageMap.ON_CALL_DUTY_CALENDAR_FEEDS);
142
+
143
+ expect(
144
+ hrefsInMenu().filter((candidate: string): boolean => {
145
+ return candidate === href;
146
+ }).length,
147
+ ).toBe(1);
148
+ });
149
+ });