@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,357 @@
1
+ import { afterEach, describe, expect, it, jest } from "@jest/globals";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ /*
6
+ * The environment knobs the on-call calendar feeds read at boot, plus the
7
+ * ENCRYPTION_SECRET boot warning that ships with them (the feed tokens are
8
+ * one more thing that secret protects).
9
+ *
10
+ * EnvironmentConfig reads process.env at module load, so every case here
11
+ * sets the environment, resets the module registry and imports afresh.
12
+ */
13
+
14
+ interface EnvironmentConfigShape {
15
+ DisableOnCallCalendarFeed: boolean;
16
+ OnCallCalendarFeedRateLimitWindowSeconds: number;
17
+ OnCallCalendarFeedRateLimitPerTokenPerWindow: number;
18
+ OnCallCalendarFeedRateLimitPerIpPerWindow: number;
19
+ IsEncryptionSecretInsecure: boolean;
20
+ InsecureEncryptionSecretValues: Array<string>;
21
+ EncryptionSecretWarning: string | null;
22
+ }
23
+
24
+ /* <repo>/Common/Tests/Server -> <repo> */
25
+ const REPO_ROOT: string = path.resolve(__dirname, "../../..");
26
+
27
+ /*
28
+ * The value config.example.env ships for a variable. README's Docker Compose
29
+ * install is `cp config.example.env config.env`, and docker-compose.base.yml
30
+ * passes ENCRYPTION_SECRET through with no default, so whatever this file
31
+ * ships is what a stock self-hosted install actually encrypts with.
32
+ */
33
+ function readExampleEnvValue(name: string): string {
34
+ const example: string = fs.readFileSync(
35
+ path.join(REPO_ROOT, "config.example.env"),
36
+ "utf8",
37
+ );
38
+
39
+ const match: RegExpMatchArray | null = example.match(
40
+ new RegExp(`^${name}=(.*)$`, "m"),
41
+ );
42
+
43
+ return (match?.[1] || "").trim();
44
+ }
45
+
46
+ const MANAGED_KEYS: Array<string> = [
47
+ "DISABLE_ON_CALL_CALENDAR_FEED",
48
+ "ON_CALL_CALENDAR_FEED_RATE_LIMIT_WINDOW_SECONDS",
49
+ "ON_CALL_CALENDAR_FEED_RATE_LIMIT_PER_TOKEN_PER_WINDOW",
50
+ "ON_CALL_CALENDAR_FEED_RATE_LIMIT_PER_IP_PER_WINDOW",
51
+ "ENCRYPTION_SECRET",
52
+ ];
53
+
54
+ const originalEnv: NodeJS.ProcessEnv = { ...process.env };
55
+
56
+ async function load(
57
+ overrides: Record<string, string | undefined>,
58
+ ): Promise<EnvironmentConfigShape> {
59
+ for (const key of MANAGED_KEYS) {
60
+ delete process.env[key];
61
+ }
62
+
63
+ for (const [key, value] of Object.entries(overrides)) {
64
+ if (value === undefined) {
65
+ delete process.env[key];
66
+ } else {
67
+ process.env[key] = value;
68
+ }
69
+ }
70
+
71
+ jest.resetModules();
72
+
73
+ return (await import(
74
+ "../../Server/EnvironmentConfig"
75
+ )) as unknown as EnvironmentConfigShape;
76
+ }
77
+
78
+ afterEach(() => {
79
+ process.env = { ...originalEnv };
80
+ jest.resetModules();
81
+ });
82
+
83
+ describe("DISABLE_ON_CALL_CALENDAR_FEED", () => {
84
+ it("is off by default", async () => {
85
+ const config: EnvironmentConfigShape = await load({});
86
+
87
+ expect(config.DisableOnCallCalendarFeed).toBe(false);
88
+ });
89
+
90
+ it("is on for the literal true", async () => {
91
+ const config: EnvironmentConfigShape = await load({
92
+ DISABLE_ON_CALL_CALENDAR_FEED: "true",
93
+ });
94
+
95
+ expect(config.DisableOnCallCalendarFeed).toBe(true);
96
+ });
97
+
98
+ it("is off for anything that is not the literal true", async () => {
99
+ for (const value of ["false", "", "1", "yes", "TRUE", "True", " true"]) {
100
+ const config: EnvironmentConfigShape = await load({
101
+ DISABLE_ON_CALL_CALENDAR_FEED: value,
102
+ });
103
+
104
+ expect(config.DisableOnCallCalendarFeed).toBe(false);
105
+ }
106
+ });
107
+ });
108
+
109
+ describe("ON_CALL_CALENDAR_FEED_RATE_LIMIT_*", () => {
110
+ it("defaults to a 60 second window, 60 per token and 3000 per address", async () => {
111
+ const config: EnvironmentConfigShape = await load({});
112
+
113
+ expect(config.OnCallCalendarFeedRateLimitWindowSeconds).toBe(60);
114
+ expect(config.OnCallCalendarFeedRateLimitPerTokenPerWindow).toBe(60);
115
+ expect(config.OnCallCalendarFeedRateLimitPerIpPerWindow).toBe(3000);
116
+ });
117
+
118
+ it("reads each value independently", async () => {
119
+ const config: EnvironmentConfigShape = await load({
120
+ ON_CALL_CALENDAR_FEED_RATE_LIMIT_WINDOW_SECONDS: "120",
121
+ ON_CALL_CALENDAR_FEED_RATE_LIMIT_PER_TOKEN_PER_WINDOW: "10",
122
+ ON_CALL_CALENDAR_FEED_RATE_LIMIT_PER_IP_PER_WINDOW: "500",
123
+ });
124
+
125
+ expect(config.OnCallCalendarFeedRateLimitWindowSeconds).toBe(120);
126
+ expect(config.OnCallCalendarFeedRateLimitPerTokenPerWindow).toBe(10);
127
+ expect(config.OnCallCalendarFeedRateLimitPerIpPerWindow).toBe(500);
128
+ });
129
+
130
+ it("tolerates surrounding whitespace", async () => {
131
+ const config: EnvironmentConfigShape = await load({
132
+ ON_CALL_CALENDAR_FEED_RATE_LIMIT_PER_TOKEN_PER_WINDOW: " 25 ",
133
+ });
134
+
135
+ expect(config.OnCallCalendarFeedRateLimitPerTokenPerWindow).toBe(25);
136
+ });
137
+
138
+ it("treats a blank value as unset (compose passes ${VAR:-} through)", async () => {
139
+ const config: EnvironmentConfigShape = await load({
140
+ ON_CALL_CALENDAR_FEED_RATE_LIMIT_WINDOW_SECONDS: "",
141
+ ON_CALL_CALENDAR_FEED_RATE_LIMIT_PER_TOKEN_PER_WINDOW: " ",
142
+ ON_CALL_CALENDAR_FEED_RATE_LIMIT_PER_IP_PER_WINDOW: "",
143
+ });
144
+
145
+ expect(config.OnCallCalendarFeedRateLimitWindowSeconds).toBe(60);
146
+ expect(config.OnCallCalendarFeedRateLimitPerTokenPerWindow).toBe(60);
147
+ expect(config.OnCallCalendarFeedRateLimitPerIpPerWindow).toBe(3000);
148
+ });
149
+
150
+ it("falls back to the default for anything that is not a positive whole number", async () => {
151
+ for (const bad of [
152
+ "0",
153
+ "-5",
154
+ "1.5",
155
+ "abc",
156
+ "NaN",
157
+ "Infinity",
158
+ "1e3x",
159
+ "60s",
160
+ ]) {
161
+ const config: EnvironmentConfigShape = await load({
162
+ ON_CALL_CALENDAR_FEED_RATE_LIMIT_WINDOW_SECONDS: bad,
163
+ ON_CALL_CALENDAR_FEED_RATE_LIMIT_PER_TOKEN_PER_WINDOW: bad,
164
+ ON_CALL_CALENDAR_FEED_RATE_LIMIT_PER_IP_PER_WINDOW: bad,
165
+ });
166
+
167
+ expect(config.OnCallCalendarFeedRateLimitWindowSeconds).toBe(60);
168
+ expect(config.OnCallCalendarFeedRateLimitPerTokenPerWindow).toBe(60);
169
+ expect(config.OnCallCalendarFeedRateLimitPerIpPerWindow).toBe(3000);
170
+ }
171
+ });
172
+
173
+ it("does not round a fractional value into something the operator did not ask for", async () => {
174
+ const config: EnvironmentConfigShape = await load({
175
+ ON_CALL_CALENDAR_FEED_RATE_LIMIT_PER_IP_PER_WINDOW: "2999.9",
176
+ });
177
+
178
+ expect(config.OnCallCalendarFeedRateLimitPerIpPerWindow).toBe(3000);
179
+ });
180
+
181
+ it("accepts a large but finite value", async () => {
182
+ const config: EnvironmentConfigShape = await load({
183
+ ON_CALL_CALENDAR_FEED_RATE_LIMIT_PER_IP_PER_WINDOW: "1000000",
184
+ });
185
+
186
+ expect(config.OnCallCalendarFeedRateLimitPerIpPerWindow).toBe(1000000);
187
+ });
188
+ });
189
+
190
+ describe("ENCRYPTION_SECRET boot warning", () => {
191
+ it("warns when the secret is unset", async () => {
192
+ const config: EnvironmentConfigShape = await load({
193
+ ENCRYPTION_SECRET: undefined,
194
+ });
195
+
196
+ expect(config.IsEncryptionSecretInsecure).toBe(true);
197
+ expect(config.EncryptionSecretWarning).toEqual(expect.any(String));
198
+ });
199
+
200
+ it('warns when the secret is the code fallback "secret"', async () => {
201
+ const config: EnvironmentConfigShape = await load({
202
+ ENCRYPTION_SECRET: "secret",
203
+ });
204
+
205
+ expect(config.IsEncryptionSecretInsecure).toBe(true);
206
+ expect(config.EncryptionSecretWarning).not.toBeNull();
207
+ });
208
+
209
+ it("warns for the placeholder config.example.env ships", async () => {
210
+ /*
211
+ * The stock Docker Compose install never sees the string "secret": it
212
+ * copies config.example.env, which sets the placeholder below. Before this
213
+ * case the warning stayed silent on the single most common insecure
214
+ * configuration there is.
215
+ */
216
+ const config: EnvironmentConfigShape = await load({
217
+ ENCRYPTION_SECRET: "please-change-this-to-random-value",
218
+ });
219
+
220
+ expect(config.IsEncryptionSecretInsecure).toBe(true);
221
+ expect(config.EncryptionSecretWarning).not.toBeNull();
222
+ });
223
+
224
+ it("warns for whatever config.example.env actually ships today", async () => {
225
+ /*
226
+ * Drift guard: change the placeholder in config.example.env and this fails
227
+ * until the insecure-value list learns about the new one.
228
+ */
229
+ const shipped: string = readExampleEnvValue("ENCRYPTION_SECRET");
230
+
231
+ expect(shipped.length).toBeGreaterThan(0);
232
+
233
+ const config: EnvironmentConfigShape = await load({
234
+ ENCRYPTION_SECRET: shipped,
235
+ });
236
+
237
+ expect({ shipped, insecure: config.IsEncryptionSecretInsecure }).toEqual({
238
+ shipped,
239
+ insecure: true,
240
+ });
241
+ });
242
+
243
+ it("warns for a half-edited placeholder", async () => {
244
+ /*
245
+ * An operator who appended to the placeholder rather than replacing it is
246
+ * still running a key that starts with a public string.
247
+ */
248
+ for (const value of [
249
+ "please-change-this-to-random-value-2",
250
+ "please-change-this",
251
+ ]) {
252
+ const config: EnvironmentConfigShape = await load({
253
+ ENCRYPTION_SECRET: value,
254
+ });
255
+
256
+ expect({ value, insecure: config.IsEncryptionSecretInsecure }).toEqual({
257
+ value,
258
+ insecure: true,
259
+ });
260
+ }
261
+ });
262
+
263
+ it("lists both shipped placeholders, so callers can name them", async () => {
264
+ const config: EnvironmentConfigShape = await load({});
265
+
266
+ expect(config.InsecureEncryptionSecretValues).toEqual([
267
+ "secret",
268
+ "please-change-this-to-random-value",
269
+ ]);
270
+ });
271
+
272
+ it("warns when the secret is blank or whitespace", async () => {
273
+ for (const value of ["", " ", "\t"]) {
274
+ const config: EnvironmentConfigShape = await load({
275
+ ENCRYPTION_SECRET: value,
276
+ });
277
+
278
+ expect(config.IsEncryptionSecretInsecure).toBe(true);
279
+ }
280
+ });
281
+
282
+ it("warns when a placeholder is padded with whitespace", async () => {
283
+ for (const value of [" secret ", " please-change-this-to-random-value "]) {
284
+ const config: EnvironmentConfigShape = await load({
285
+ ENCRYPTION_SECRET: value,
286
+ });
287
+
288
+ expect({ value, insecure: config.IsEncryptionSecretInsecure }).toEqual({
289
+ value,
290
+ insecure: true,
291
+ });
292
+ }
293
+ });
294
+
295
+ it("is silent for a real secret", async () => {
296
+ const config: EnvironmentConfigShape = await load({
297
+ ENCRYPTION_SECRET: "b2f0c1e9d8a7f6e5d4c3b2a1908f7e6d5c4b3a2918f7e6d5",
298
+ });
299
+
300
+ expect(config.IsEncryptionSecretInsecure).toBe(false);
301
+ expect(config.EncryptionSecretWarning).toBeNull();
302
+ });
303
+
304
+ it("is case-sensitive about the placeholder, since the encryption key is", async () => {
305
+ /*
306
+ * "Secret" is a different (still terrible) key; the warning is about the
307
+ * exact values the repository ships, which are what an attacker would try
308
+ * first. Anything else is the operator's own choice.
309
+ */
310
+ for (const value of ["Secret", "Please-Change-This-To-Random-Value"]) {
311
+ const config: EnvironmentConfigShape = await load({
312
+ ENCRYPTION_SECRET: value,
313
+ });
314
+
315
+ expect({ value, insecure: config.IsEncryptionSecretInsecure }).toEqual({
316
+ value,
317
+ insecure: false,
318
+ });
319
+ }
320
+ });
321
+
322
+ it("says what is at stake and what to do, without echoing any secret", async () => {
323
+ const config: EnvironmentConfigShape = await load({
324
+ ENCRYPTION_SECRET: "secret",
325
+ });
326
+
327
+ const warning: string = config.EncryptionSecretWarning || "";
328
+
329
+ expect(warning).toContain("ENCRYPTION_SECRET");
330
+ expect(warning).toMatch(/encrypted/i);
331
+ expect(warning).toMatch(/calendar feed/i);
332
+ expect(warning).toMatch(/config\.env|Helm/);
333
+ /*
334
+ * It names both shipped placeholders in quotes as a fact, never an
335
+ * operator's own value. An operator who reads "secret" alone and sees the
336
+ * compose placeholder in their config.env would wrongly conclude the
337
+ * warning is not about them.
338
+ */
339
+ expect(warning).toContain('"secret"');
340
+ expect(warning).toContain('"please-change-this-to-random-value"');
341
+ expect(warning).toContain("config.example.env");
342
+ expect(warning).not.toContain("b2f0c1e9");
343
+ });
344
+
345
+ it("still exposes a usable EncryptionSecret when warning, so nothing refuses to boot", async () => {
346
+ const config: EnvironmentConfigShape & {
347
+ EncryptionSecret: { toString: () => string };
348
+ } = (await load({
349
+ ENCRYPTION_SECRET: undefined,
350
+ })) as EnvironmentConfigShape & {
351
+ EncryptionSecret: { toString: () => string };
352
+ };
353
+
354
+ expect(config.EncryptionSecret.toString()).toBe("secret");
355
+ expect(config.IsEncryptionSecretInsecure).toBe(true);
356
+ });
357
+ });