@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,368 @@
1
+ import AllModelTypes from "../../Models/DatabaseModels/Index";
2
+ import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
3
+ import OnCallDutyPolicySchedule from "../../Models/DatabaseModels/OnCallDutyPolicySchedule";
4
+ import Project from "../../Models/DatabaseModels/Project";
5
+ import User from "../../Models/DatabaseModels/User";
6
+ import UserNotificationSetting from "../../Models/DatabaseModels/UserNotificationSetting";
7
+ import UserOnCallShiftReminder, {
8
+ MAX_MINUTES_BEFORE_SHIFT,
9
+ MIN_MINUTES_BEFORE_SHIFT,
10
+ } from "../../Models/DatabaseModels/UserOnCallShiftReminder";
11
+ import UserOnCallShiftReminderLog, {
12
+ UserOnCallShiftReminderLogKind,
13
+ } from "../../Models/DatabaseModels/UserOnCallShiftReminderLog";
14
+ import { ColumnAccessControl } from "../../Types/BaseDatabase/AccessControl";
15
+ import TableColumnType from "../../Types/Database/TableColumnType";
16
+ import Permission from "../../Types/Permission";
17
+ import { describe, expect, test } from "@jest/globals";
18
+ import { getMetadataArgsStorage } from "typeorm";
19
+ import type { ColumnMetadataArgs } from "typeorm/metadata-args/ColumnMetadataArgs";
20
+ import type { IndexMetadataArgs } from "typeorm/metadata-args/IndexMetadataArgs";
21
+ import type { RelationMetadataArgs } from "typeorm/metadata-args/RelationMetadataArgs";
22
+
23
+ /*
24
+ * Two tables for shift reminders: what the user asked for
25
+ * (UserOnCallShiftReminder, user-owned like UserNotificationSetting) and
26
+ * what the worker did about it (UserOnCallShiftReminderLog, root-only, whose
27
+ * unique index IS the send-exactly-once guarantee). Both shapes are pinned
28
+ * here because both are invisible until they are wrong: a reminder table a
29
+ * user cannot write to, or a ledger a user CAN write to, or a unique index
30
+ * that misses one key column and lets a moved-by-a-second shift page twice.
31
+ */
32
+
33
+ type ModelType = { new (): BaseModel };
34
+
35
+ const reminder: UserOnCallShiftReminder = new UserOnCallShiftReminder();
36
+ const log: UserOnCallShiftReminderLog = new UserOnCallShiftReminderLog();
37
+ const notificationSetting: UserNotificationSetting =
38
+ new UserNotificationSetting();
39
+
40
+ function typeormColumn(
41
+ modelType: ModelType,
42
+ propertyName: string,
43
+ ): ColumnMetadataArgs {
44
+ const column: ColumnMetadataArgs | undefined =
45
+ getMetadataArgsStorage().columns.find((entry: ColumnMetadataArgs) => {
46
+ return entry.target === modelType && entry.propertyName === propertyName;
47
+ });
48
+
49
+ if (!column) {
50
+ throw new Error(`${modelType.name}.${propertyName} has no @Column`);
51
+ }
52
+
53
+ return column;
54
+ }
55
+
56
+ function uniqueIndexes(modelType: ModelType): Array<Array<string>> {
57
+ return getMetadataArgsStorage()
58
+ .indices.filter((entry: IndexMetadataArgs) => {
59
+ return entry.target === modelType && entry.unique === true;
60
+ })
61
+ .map((entry: IndexMetadataArgs) => {
62
+ return Array.isArray(entry.columns)
63
+ ? (entry.columns as Array<string>)
64
+ : [];
65
+ });
66
+ }
67
+
68
+ function relation(
69
+ modelType: ModelType,
70
+ propertyName: string,
71
+ ): RelationMetadataArgs {
72
+ const found: RelationMetadataArgs | undefined =
73
+ getMetadataArgsStorage().relations.find((entry: RelationMetadataArgs) => {
74
+ return entry.target === modelType && entry.propertyName === propertyName;
75
+ });
76
+
77
+ if (!found) {
78
+ throw new Error(`${modelType.name}.${propertyName} has no relation`);
79
+ }
80
+
81
+ return found;
82
+ }
83
+
84
+ describe("UserOnCallShiftReminder model", () => {
85
+ test("is registered in the model index", () => {
86
+ expect(
87
+ (AllModelTypes as Array<ModelType>).includes(UserOnCallShiftReminder),
88
+ ).toBe(true);
89
+ });
90
+
91
+ test("is served from its own CRUD route", () => {
92
+ expect(reminder.getCrudApiPath()?.toString()).toBe(
93
+ "/user-on-call-shift-reminder",
94
+ );
95
+ expect(reminder.tableName).toBe("UserOnCallShiftReminder");
96
+ });
97
+
98
+ test("is shaped like UserNotificationSetting: tenant scoped, owner-only, unpaid-tolerant", () => {
99
+ expect(reminder.getTenantColumn()).toBe("projectId");
100
+ expect(reminder.currentUserCanAccessColumnBy).toBe("userId");
101
+ expect(reminder.allowAccessIfSubscriptionIsUnpaid).toBe(true);
102
+
103
+ expect(reminder.createRecordPermissions).toEqual([Permission.CurrentUser]);
104
+ expect(reminder.readRecordPermissions).toEqual([Permission.CurrentUser]);
105
+ expect(reminder.updateRecordPermissions).toEqual([Permission.CurrentUser]);
106
+ expect(reminder.deleteRecordPermissions).toEqual([Permission.CurrentUser]);
107
+
108
+ expect(reminder.createRecordPermissions).toEqual(
109
+ notificationSetting.createRecordPermissions,
110
+ );
111
+ expect(reminder.readRecordPermissions).toEqual(
112
+ notificationSetting.readRecordPermissions,
113
+ );
114
+ expect(reminder.updateRecordPermissions).toEqual(
115
+ notificationSetting.updateRecordPermissions,
116
+ );
117
+ expect(reminder.deleteRecordPermissions).toEqual(
118
+ notificationSetting.deleteRecordPermissions,
119
+ );
120
+ expect(reminder.currentUserCanAccessColumnBy).toBe(
121
+ notificationSetting.currentUserCanAccessColumnBy,
122
+ );
123
+ });
124
+
125
+ test("is not plan gated (reminders follow the schedules the user is already on)", () => {
126
+ expect(reminder.getCreateBillingPlan()).toBeFalsy();
127
+ expect(reminder.getReadBillingPlan()).toBeFalsy();
128
+ expect(reminder.getUpdateBillingPlan()).toBeFalsy();
129
+ expect(reminder.getDeleteBillingPlan()).toBeFalsy();
130
+ });
131
+
132
+ test("is not documented", () => {
133
+ expect(reminder.enableDocumentation).toBeFalsy();
134
+ });
135
+
136
+ test("carries project, user and the lead time", () => {
137
+ const columns: Array<string> = reminder.getTableColumns().columns;
138
+
139
+ for (const column of [
140
+ "project",
141
+ "projectId",
142
+ "user",
143
+ "userId",
144
+ "minutesBeforeShift",
145
+ ]) {
146
+ expect(columns).toContain(column);
147
+ }
148
+
149
+ expect(reminder.getTableColumnMetadata("minutesBeforeShift").type).toBe(
150
+ TableColumnType.Number,
151
+ );
152
+ expect(reminder.getTableColumnMetadata("minutesBeforeShift").required).toBe(
153
+ true,
154
+ );
155
+ expect(
156
+ typeormColumn(UserOnCallShiftReminder, "minutesBeforeShift").options
157
+ .nullable,
158
+ ).toBe(false);
159
+ });
160
+
161
+ test("the owner may set the lead time on create and change it later; the owner columns are fixed", () => {
162
+ const lead: ColumnAccessControl | null =
163
+ reminder.getColumnAccessControlFor("minutesBeforeShift");
164
+
165
+ expect(lead).toEqual({
166
+ create: [Permission.CurrentUser],
167
+ read: [Permission.CurrentUser],
168
+ update: [Permission.CurrentUser],
169
+ });
170
+
171
+ for (const column of ["projectId", "userId", "project", "user"]) {
172
+ const accessControl: ColumnAccessControl | null =
173
+ reminder.getColumnAccessControlFor(column);
174
+
175
+ expect({ column, update: accessControl?.update }).toEqual({
176
+ column,
177
+ update: [],
178
+ });
179
+ expect({ column, create: accessControl?.create }).toEqual({
180
+ column,
181
+ create: [Permission.CurrentUser],
182
+ });
183
+ }
184
+ });
185
+
186
+ test("is unique per (project, user, lead) so a double-click is a no-op", () => {
187
+ expect(uniqueIndexes(UserOnCallShiftReminder)).toContainEqual([
188
+ "projectId",
189
+ "userId",
190
+ "minutesBeforeShift",
191
+ ]);
192
+ });
193
+
194
+ test("cascades away with its project and its user", () => {
195
+ expect(relation(UserOnCallShiftReminder, "project").options.onDelete).toBe(
196
+ "CASCADE",
197
+ );
198
+ expect(relation(UserOnCallShiftReminder, "user").options.onDelete).toBe(
199
+ "CASCADE",
200
+ );
201
+ expect(reminder.getTableColumnMetadata("project").modelType).toBe(Project);
202
+ expect(reminder.getTableColumnMetadata("user").modelType).toBe(User);
203
+ expect(
204
+ typeormColumn(UserOnCallShiftReminder, "userId").options.nullable,
205
+ ).toBe(false);
206
+ expect(
207
+ typeormColumn(UserOnCallShiftReminder, "projectId").options.nullable,
208
+ ).toBe(false);
209
+ });
210
+
211
+ test("publishes the lead-time bounds the service enforces", () => {
212
+ expect(MIN_MINUTES_BEFORE_SHIFT).toBe(15);
213
+ expect(MAX_MINUTES_BEFORE_SHIFT).toBe(20160);
214
+ expect(MAX_MINUTES_BEFORE_SHIFT).toBe(14 * 24 * 60);
215
+ });
216
+ });
217
+
218
+ describe("UserOnCallShiftReminderLog model", () => {
219
+ test("is registered in the model index", () => {
220
+ expect(
221
+ (AllModelTypes as Array<ModelType>).includes(UserOnCallShiftReminderLog),
222
+ ).toBe(true);
223
+ });
224
+
225
+ test("is tenant scoped and addressable, but every operation is denied through the API", () => {
226
+ expect(log.getTenantColumn()).toBe("projectId");
227
+ expect(log.tableName).toBe("UserOnCallShiftReminderLog");
228
+ expect(log.getCrudApiPath()?.toString()).toBe(
229
+ "/user-on-call-shift-reminder-log",
230
+ );
231
+
232
+ expect(log.createRecordPermissions).toEqual([]);
233
+ expect(log.readRecordPermissions).toEqual([]);
234
+ expect(log.updateRecordPermissions).toEqual([]);
235
+ expect(log.deleteRecordPermissions).toEqual([]);
236
+
237
+ // Not user-owned either: CurrentUser must never resolve to a row here.
238
+ expect(log.currentUserCanAccessColumnBy).toBeFalsy();
239
+ });
240
+
241
+ test("denies every column to every caller (root bypasses column checks)", () => {
242
+ for (const column of log.getTableColumns().columns) {
243
+ const accessControl: ColumnAccessControl | null =
244
+ log.getColumnAccessControlFor(column);
245
+
246
+ if (!accessControl) {
247
+ continue;
248
+ }
249
+
250
+ expect({ column, accessControl }).toEqual({
251
+ column,
252
+ accessControl: { create: [], read: [], update: [] },
253
+ });
254
+ }
255
+ });
256
+
257
+ test("is not documented", () => {
258
+ expect(log.enableDocumentation).toBeFalsy();
259
+ });
260
+
261
+ test("carries the full idempotency key plus claim/sent stamps", () => {
262
+ const columns: Array<string> = log.getTableColumns().columns;
263
+
264
+ for (const column of [
265
+ "project",
266
+ "projectId",
267
+ "user",
268
+ "userId",
269
+ "onCallDutyPolicySchedule",
270
+ "onCallDutyPolicyScheduleId",
271
+ "shiftStartsAt",
272
+ "minutesBeforeShift",
273
+ "kind",
274
+ "claimedAt",
275
+ "sentAt",
276
+ ]) {
277
+ expect(columns).toContain(column);
278
+ }
279
+
280
+ expect(log.getTableColumnMetadata("shiftStartsAt").type).toBe(
281
+ TableColumnType.Date,
282
+ );
283
+ expect(log.getTableColumnMetadata("claimedAt").type).toBe(
284
+ TableColumnType.Date,
285
+ );
286
+ expect(log.getTableColumnMetadata("sentAt").type).toBe(
287
+ TableColumnType.Date,
288
+ );
289
+ expect(log.getTableColumnMetadata("kind").type).toBe(
290
+ TableColumnType.ShortText,
291
+ );
292
+ expect(log.getTableColumnMetadata("minutesBeforeShift").type).toBe(
293
+ TableColumnType.Number,
294
+ );
295
+ });
296
+
297
+ test("the key columns are NOT NULL; sentAt is the only nullable stamp", () => {
298
+ for (const column of [
299
+ "projectId",
300
+ "userId",
301
+ "onCallDutyPolicyScheduleId",
302
+ "shiftStartsAt",
303
+ "minutesBeforeShift",
304
+ "kind",
305
+ "claimedAt",
306
+ ]) {
307
+ expect({
308
+ column,
309
+ nullable: typeormColumn(UserOnCallShiftReminderLog, column).options
310
+ .nullable,
311
+ }).toEqual({ column, nullable: false });
312
+ }
313
+
314
+ expect(
315
+ typeormColumn(UserOnCallShiftReminderLog, "sentAt").options.nullable,
316
+ ).toBe(true);
317
+ });
318
+
319
+ test("minutesBeforeShift defaults to 0 for change notices", () => {
320
+ expect(
321
+ typeormColumn(UserOnCallShiftReminderLog, "minutesBeforeShift").options
322
+ .default,
323
+ ).toBe(0);
324
+ expect(
325
+ log.getTableColumnMetadata("minutesBeforeShift").isDefaultValueColumn,
326
+ ).toBe(true);
327
+ expect(log.getTableColumnMetadata("minutesBeforeShift").defaultValue).toBe(
328
+ 0,
329
+ );
330
+ });
331
+
332
+ test("is unique on exactly (user, schedule, shiftStartsAt, lead, kind) - the send-once key", () => {
333
+ expect(uniqueIndexes(UserOnCallShiftReminderLog)).toEqual([
334
+ [
335
+ "userId",
336
+ "onCallDutyPolicyScheduleId",
337
+ "shiftStartsAt",
338
+ "minutesBeforeShift",
339
+ "kind",
340
+ ],
341
+ ]);
342
+ });
343
+
344
+ test("cascades away with its project, user and schedule", () => {
345
+ expect(
346
+ relation(UserOnCallShiftReminderLog, "project").options.onDelete,
347
+ ).toBe("CASCADE");
348
+ expect(relation(UserOnCallShiftReminderLog, "user").options.onDelete).toBe(
349
+ "CASCADE",
350
+ );
351
+ expect(
352
+ relation(UserOnCallShiftReminderLog, "onCallDutyPolicySchedule").options
353
+ .onDelete,
354
+ ).toBe("CASCADE");
355
+ expect(
356
+ log.getTableColumnMetadata("onCallDutyPolicySchedule").modelType,
357
+ ).toBe(OnCallDutyPolicySchedule);
358
+ });
359
+
360
+ test("names the three kinds of row the worker writes", () => {
361
+ expect(Object.values(UserOnCallShiftReminderLogKind).sort()).toEqual(
362
+ ["catch-up", "reassigned", "reminder"].sort(),
363
+ );
364
+ expect(UserOnCallShiftReminderLogKind.Reminder).toBe("reminder");
365
+ expect(UserOnCallShiftReminderLogKind.CatchUp).toBe("catch-up");
366
+ expect(UserOnCallShiftReminderLogKind.Reassigned).toBe("reassigned");
367
+ });
368
+ });
@@ -59,6 +59,13 @@ type MockRouter = {
59
59
  post: jest.Mock;
60
60
  put: jest.Mock;
61
61
  delete: jest.Mock;
62
+
63
+ /*
64
+ * Express's catch-every-method registration. Recorded with the method
65
+ * "ALL"; a test that replays the routes onto a real app should map it to
66
+ * `app.all`.
67
+ */
68
+ all: jest.Mock;
62
69
  routes: Route[];
63
70
  match: (method: string, uri: string) => Route;
64
71
  };
@@ -68,6 +75,7 @@ export const mockRouter: MockRouter = {
68
75
  post: jest.fn().mockImplementation(mockRouterForMethod("post")),
69
76
  put: jest.fn().mockImplementation(mockRouterForMethod("put")),
70
77
  delete: jest.fn().mockImplementation(mockRouterForMethod("delete")),
78
+ all: jest.fn().mockImplementation(mockRouterForMethod("all")),
71
79
  routes: [] as any as Route[],
72
80
  match: (method: string, uri: string) => {
73
81
  const route: Route | undefined = mockRouter.routes.find((route: Route) => {