@oneuptime/common 12.0.2 → 12.0.3

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 (228) hide show
  1. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
  2. package/Models/DatabaseModels/Index.ts +2 -0
  3. package/Models/DatabaseModels/LogDropFilter.ts +9 -20
  4. package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
  5. package/Models/DatabaseModels/StatusPageResource.ts +73 -0
  6. package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
  7. package/Server/API/BaseAPI.ts +13 -2
  8. package/Server/API/DashboardAPI.ts +39 -0
  9. package/Server/EnvironmentConfig.ts +31 -1
  10. package/Server/Infrastructure/GlobalCache.ts +115 -0
  11. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  14. package/Server/Services/AlertStateTimelineService.ts +20 -0
  15. package/Server/Services/CephClusterService.ts +26 -86
  16. package/Server/Services/CloudResourceService.ts +23 -83
  17. package/Server/Services/DatabaseService.ts +111 -0
  18. package/Server/Services/DockerHostService.ts +19 -79
  19. package/Server/Services/DockerSwarmClusterService.ts +27 -88
  20. package/Server/Services/HostService.ts +46 -99
  21. package/Server/Services/IncidentStateTimelineService.ts +20 -0
  22. package/Server/Services/Index.ts +2 -0
  23. package/Server/Services/IoTFleetService.ts +19 -79
  24. package/Server/Services/KubernetesClusterService.ts +17 -78
  25. package/Server/Services/LabelService.ts +57 -10
  26. package/Server/Services/MetricTypeService.ts +84 -0
  27. package/Server/Services/MonitorService.ts +89 -14
  28. package/Server/Services/MonitorStatusTimelineService.ts +29 -0
  29. package/Server/Services/MonitorTemplateService.ts +65 -0
  30. package/Server/Services/PodmanHostService.ts +19 -79
  31. package/Server/Services/ProxmoxClusterService.ts +23 -83
  32. package/Server/Services/RumApplicationService.ts +19 -80
  33. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
  34. package/Server/Services/ServerlessFunctionService.ts +24 -85
  35. package/Server/Services/ServiceService.ts +299 -73
  36. package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
  37. package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
  38. package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
  39. package/Server/Services/TelemetryEntityService.ts +1 -0
  40. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
  41. package/Server/Utils/Monitor/MonitorResource.ts +63 -36
  42. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
  43. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
  44. package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
  45. package/Server/Utils/SingleFlight.ts +83 -0
  46. package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
  47. package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
  48. package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
  49. package/Server/Utils/Telemetry/Telemetry.ts +351 -122
  50. package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
  51. package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
  52. package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
  53. package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
  54. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
  55. package/Tests/Server/Infrastructure/GlobalCache.test.ts +226 -0
  56. package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
  57. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
  58. package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
  59. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +163 -15
  60. package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
  61. package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
  62. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
  63. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +305 -56
  64. package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
  65. package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
  66. package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
  67. package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
  68. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
  69. package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
  70. package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
  71. package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
  72. package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
  73. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
  74. package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
  75. package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
  76. package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
  77. package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
  78. package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
  79. package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
  80. package/Tests/Types/Decimal.test.ts +37 -0
  81. package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
  82. package/Tests/Types/Port.test.ts +38 -0
  83. package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
  84. package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
  85. package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
  86. package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
  87. package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
  88. package/Types/Dashboard/DashboardComponentType.ts +1 -0
  89. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  90. package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
  91. package/Types/Dashboard/DashboardTemplates.ts +397 -0
  92. package/Types/Database/BigIntColumnTransformer.ts +74 -0
  93. package/Types/Database/DatabaseProperty.ts +26 -0
  94. package/Types/Database/NumericColumnValue.ts +128 -0
  95. package/Types/Decimal.ts +16 -4
  96. package/Types/Permission.ts +48 -0
  97. package/Types/Port.ts +16 -4
  98. package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
  99. package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
  100. package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
  101. package/Utils/Dashboard/Components/Index.ts +7 -0
  102. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
  103. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  104. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  105. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  106. package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
  107. package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
  109. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
  110. package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
  111. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
  113. package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
  114. package/build/dist/Server/API/BaseAPI.js +11 -1
  115. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  116. package/build/dist/Server/API/DashboardAPI.js +39 -0
  117. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  118. package/build/dist/Server/EnvironmentConfig.js +27 -1
  119. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  120. package/build/dist/Server/Infrastructure/GlobalCache.js +91 -0
  121. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  122. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
  123. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  128. package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
  129. package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
  130. package/build/dist/Server/Services/CephClusterService.js +26 -76
  131. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  132. package/build/dist/Server/Services/CloudResourceService.js +23 -73
  133. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  134. package/build/dist/Server/Services/DatabaseService.js +86 -0
  135. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  136. package/build/dist/Server/Services/DockerHostService.js +19 -69
  137. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  138. package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -78
  139. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  140. package/build/dist/Server/Services/HostService.js +58 -102
  141. package/build/dist/Server/Services/HostService.js.map +1 -1
  142. package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
  143. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  144. package/build/dist/Server/Services/Index.js +2 -0
  145. package/build/dist/Server/Services/Index.js.map +1 -1
  146. package/build/dist/Server/Services/IoTFleetService.js +19 -69
  147. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  148. package/build/dist/Server/Services/KubernetesClusterService.js +17 -68
  149. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  150. package/build/dist/Server/Services/LabelService.js +42 -7
  151. package/build/dist/Server/Services/LabelService.js.map +1 -1
  152. package/build/dist/Server/Services/MetricTypeService.js +74 -0
  153. package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
  154. package/build/dist/Server/Services/MonitorService.js +81 -9
  155. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  156. package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
  157. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  158. package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
  159. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  160. package/build/dist/Server/Services/PodmanHostService.js +19 -69
  161. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  162. package/build/dist/Server/Services/ProxmoxClusterService.js +23 -73
  163. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  164. package/build/dist/Server/Services/RumApplicationService.js +19 -70
  165. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  166. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
  167. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  168. package/build/dist/Server/Services/ServerlessFunctionService.js +24 -75
  169. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  170. package/build/dist/Server/Services/ServiceService.js +236 -61
  171. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  172. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
  173. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
  174. package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
  175. package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
  176. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
  177. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
  178. package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
  179. package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
  180. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
  181. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  182. package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
  183. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  184. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
  185. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  186. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
  187. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
  188. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
  189. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
  190. package/build/dist/Server/Utils/SingleFlight.js +76 -0
  191. package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
  192. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
  193. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
  194. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
  195. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  196. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
  197. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
  198. package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
  199. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  200. package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
  201. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  202. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  203. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  204. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
  205. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
  206. package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
  207. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  208. package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
  209. package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
  210. package/build/dist/Types/Database/DatabaseProperty.js +25 -0
  211. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  212. package/build/dist/Types/Database/NumericColumnValue.js +97 -0
  213. package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
  214. package/build/dist/Types/Decimal.js +14 -3
  215. package/build/dist/Types/Decimal.js.map +1 -1
  216. package/build/dist/Types/Permission.js +42 -0
  217. package/build/dist/Types/Permission.js.map +1 -1
  218. package/build/dist/Types/Port.js +15 -5
  219. package/build/dist/Types/Port.js.map +1 -1
  220. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
  221. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
  222. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
  223. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
  224. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
  225. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
  226. package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
  227. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  228. package/package.json +1 -1
@@ -0,0 +1,228 @@
1
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
2
+ import MonitorLabelRuleEngineService from "../../../Server/Services/MonitorLabelRuleEngineService";
3
+ import MonitorOwnerRuleEngineService from "../../../Server/Services/MonitorOwnerRuleEngineService";
4
+ import MonitorService from "../../../Server/Services/MonitorService";
5
+ import ServiceLevelObjectiveMonitorRuleEngineService from "../../../Server/Services/ServiceLevelObjectiveMonitorRuleEngineService";
6
+ import StatusPageMonitorRuleEngineService from "../../../Server/Services/StatusPageMonitorRuleEngineService";
7
+ import CreateBy from "../../../Server/Types/Database/CreateBy";
8
+ import { OnCreate } from "../../../Server/Types/Database/Hooks";
9
+ import MonitorType from "../../../Types/Monitor/MonitorType";
10
+ import ObjectID from "../../../Types/ObjectID";
11
+ import { describe, expect, it, beforeEach, afterEach } from "@jest/globals";
12
+
13
+ /*
14
+ * Contract under test - the create-side half of the status page monitor rule
15
+ * trigger, which is how monitors normally arrive on a status page. A rule that
16
+ * only ever noticed EDITS would leave every newly created monitor off the page
17
+ * until somebody happened to touch it again.
18
+ *
19
+ * MonitorService.onCreateSuccess runs its slow work on a detached promise
20
+ * chain (the create response must not wait for it), so these tests call the
21
+ * hook and then wait for the effect rather than awaiting the hook alone.
22
+ *
23
+ * Ordering matters as much as the call itself: the sync has to run AFTER
24
+ * MonitorLabelRuleEngineService, because a monitor created with no labels of
25
+ * its own can still earn them from a label rule, and those labels are exactly
26
+ * what a status page rule is likely to match on.
27
+ */
28
+
29
+ const PROJECT_ID: ObjectID = new ObjectID(
30
+ "22222222-2222-4222-8222-222222222222",
31
+ );
32
+ const MONITOR_ID: ObjectID = new ObjectID(
33
+ "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
34
+ );
35
+ const MONITOR_STATUS_ID: ObjectID = new ObjectID(
36
+ "55555555-5555-4555-8555-555555555555",
37
+ );
38
+
39
+ // Calls a protected hook without widening the service's public surface.
40
+ function callHook(
41
+ service: unknown,
42
+ name: string,
43
+ ...args: Array<unknown>
44
+ ): Promise<unknown> {
45
+ const hooks: Record<
46
+ string,
47
+ (...hookArgs: Array<unknown>) => Promise<unknown>
48
+ > = service as Record<
49
+ string,
50
+ (...hookArgs: Array<unknown>) => Promise<unknown>
51
+ >;
52
+
53
+ return hooks[name]!.apply(service, args);
54
+ }
55
+
56
+ function createdMonitor(): Monitor {
57
+ return {
58
+ id: MONITOR_ID,
59
+ _id: MONITOR_ID.toString(),
60
+ projectId: PROJECT_ID,
61
+ name: "Checkout API",
62
+ description: "tier-1",
63
+ // Manual monitors take no probes, which keeps the inline probe step out.
64
+ monitorType: MonitorType.Manual,
65
+ currentMonitorStatusId: MONITOR_STATUS_ID,
66
+ } as unknown as Monitor;
67
+ }
68
+
69
+ function onCreate(): OnCreate<Monitor> {
70
+ return {
71
+ createBy: {
72
+ data: createdMonitor(),
73
+ props: { isRoot: true, tenantId: PROJECT_ID },
74
+ } as unknown as CreateBy<Monitor>,
75
+ carryForward: null,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * The chain is detached, so poll for the effect instead of awaiting the hook.
81
+ * Returns as soon as the predicate holds, or gives up after ~2s so a genuine
82
+ * regression fails loudly rather than hanging the suite.
83
+ */
84
+ async function waitFor(predicate: () => boolean): Promise<void> {
85
+ for (let attempt: number = 0; attempt < 200; attempt++) {
86
+ if (predicate()) {
87
+ return;
88
+ }
89
+
90
+ await new Promise<void>((resolve: () => void) => {
91
+ setTimeout(resolve, 10);
92
+ });
93
+ }
94
+ }
95
+
96
+ describe("MonitorService.onCreateSuccess - putting a new monitor on the status pages that want it", () => {
97
+ let syncRulesForMonitorSpy: jest.SpyInstance;
98
+ let applyLabelRulesSpy: jest.SpyInstance;
99
+ const callOrder: Array<string> = [];
100
+
101
+ beforeEach(() => {
102
+ callOrder.length = 0;
103
+
104
+ applyLabelRulesSpy = jest
105
+ .spyOn(MonitorLabelRuleEngineService, "applyRulesToMonitor")
106
+ .mockImplementation(async () => {
107
+ callOrder.push("labelRules");
108
+ });
109
+
110
+ syncRulesForMonitorSpy = jest
111
+ .spyOn(StatusPageMonitorRuleEngineService, "syncRulesForMonitor")
112
+ .mockImplementation(async () => {
113
+ callOrder.push("statusPageRules");
114
+ return [];
115
+ });
116
+
117
+ jest
118
+ .spyOn(MonitorOwnerRuleEngineService, "applyRulesToMonitor")
119
+ .mockResolvedValue(undefined);
120
+ jest
121
+ .spyOn(
122
+ ServiceLevelObjectiveMonitorRuleEngineService,
123
+ "syncSlosForMonitor",
124
+ )
125
+ .mockResolvedValue([]);
126
+ jest
127
+ .spyOn(MonitorService, "findOneById")
128
+ .mockResolvedValue(createdMonitor());
129
+ /*
130
+ * Private/protected members of the service under test, spied so the hook
131
+ * never reaches the network or the database. Billing is not stubbed: the
132
+ * step is gated on IsBillingEnabled, which is off in tests.
133
+ */
134
+ const service: never = MonitorService as never;
135
+
136
+ jest
137
+ .spyOn(service, "handleWorkspaceOperationsAsync" as never)
138
+ .mockResolvedValue(undefined as never);
139
+ jest
140
+ .spyOn(service, "refreshMonitorProbeStatus" as never)
141
+ .mockResolvedValue(undefined as never);
142
+ jest
143
+ .spyOn(service, "changeMonitorStatus" as never)
144
+ .mockResolvedValue(undefined as never);
145
+ });
146
+
147
+ afterEach(() => {
148
+ jest.restoreAllMocks();
149
+ });
150
+
151
+ it("runs the status page monitor rules for a monitor that was just created", async () => {
152
+ await callHook(
153
+ MonitorService,
154
+ "onCreateSuccess",
155
+ onCreate(),
156
+ createdMonitor(),
157
+ );
158
+
159
+ await waitFor(() => {
160
+ return syncRulesForMonitorSpy.mock.calls.length > 0;
161
+ });
162
+
163
+ expect(syncRulesForMonitorSpy).toHaveBeenCalledWith({
164
+ monitorId: MONITOR_ID,
165
+ projectId: PROJECT_ID,
166
+ });
167
+ });
168
+
169
+ /*
170
+ * A monitor created with no labels can be given some by a MonitorLabelRule.
171
+ * Running the status page rules first would evaluate them against a monitor
172
+ * that has not earned its labels yet, and it would land on no page at all.
173
+ */
174
+ it("runs them after the label rules, so rule-added labels are visible", async () => {
175
+ await callHook(
176
+ MonitorService,
177
+ "onCreateSuccess",
178
+ onCreate(),
179
+ createdMonitor(),
180
+ );
181
+
182
+ await waitFor(() => {
183
+ return callOrder.includes("statusPageRules");
184
+ });
185
+
186
+ expect(callOrder.indexOf("labelRules")).toBeGreaterThanOrEqual(0);
187
+ expect(callOrder.indexOf("statusPageRules")).toBeGreaterThan(
188
+ callOrder.indexOf("labelRules"),
189
+ );
190
+ });
191
+
192
+ it("does not fail the monitor create when the sync throws", async () => {
193
+ syncRulesForMonitorSpy.mockRejectedValue(new Error("db down"));
194
+
195
+ await expect(
196
+ callHook(MonitorService, "onCreateSuccess", onCreate(), createdMonitor()),
197
+ ).resolves.toBeDefined();
198
+
199
+ await waitFor(() => {
200
+ return syncRulesForMonitorSpy.mock.calls.length > 0;
201
+ });
202
+
203
+ expect(syncRulesForMonitorSpy).toHaveBeenCalled();
204
+ });
205
+
206
+ /*
207
+ * The label rule step is wrapped in its own try/catch that returns early on
208
+ * failure. If that early return also skipped the status page sync, a project
209
+ * with a broken label rule would quietly stop putting new monitors on its
210
+ * status pages.
211
+ */
212
+ it("still runs them when the label rules step fails", async () => {
213
+ applyLabelRulesSpy.mockRejectedValue(new Error("label rules down"));
214
+
215
+ await callHook(
216
+ MonitorService,
217
+ "onCreateSuccess",
218
+ onCreate(),
219
+ createdMonitor(),
220
+ );
221
+
222
+ await waitFor(() => {
223
+ return syncRulesForMonitorSpy.mock.calls.length > 0;
224
+ });
225
+
226
+ expect(syncRulesForMonitorSpy).toHaveBeenCalled();
227
+ });
228
+ });
@@ -0,0 +1,267 @@
1
+ import {
2
+ afterAll,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ jest,
8
+ } from "@jest/globals";
9
+ import APIKeyAccessPermission from "../../../../Server/Utils/APIKey/AccessPermission";
10
+ import ApiKeyPermissionService from "../../../../Server/Services/ApiKeyPermissionService";
11
+ import APIKeyPermission from "../../../../Models/DatabaseModels/ApiKeyPermission";
12
+ import Label from "../../../../Models/DatabaseModels/Label";
13
+ import ObjectID from "../../../../Types/ObjectID";
14
+ import Permission, {
15
+ UserGlobalAccessPermission,
16
+ UserPermission,
17
+ UserTenantAccessPermission,
18
+ } from "../../../../Types/Permission";
19
+ import { getJestSpyOn } from "../../../Spy";
20
+
21
+ /*
22
+ * This class turns an API key into the permission set a request runs under, so
23
+ * every method here is a privilege-granting decision. The security invariant
24
+ * that must hold across all of them: a *default* (ordinary) API key is never
25
+ * silently handed ProjectOwner, while a *master* key deliberately is. A
26
+ * regression that leaks ProjectOwner into the default path would give every API
27
+ * key full control of the project; one that dropped it from the master path
28
+ * would break trusted internal automation. Both directions are pinned below.
29
+ *
30
+ * getApiTenantAccessPermission also reshapes stored permission rows into the
31
+ * runtime UserPermission shape (labels -> labelIds, block flag preserved). That
32
+ * mapping is where a dropped label would quietly widen a scoped permission, so
33
+ * it is tested against a spied service rather than a live database.
34
+ */
35
+
36
+ const projectId: ObjectID = ObjectID.generate();
37
+ const apiKeyId: ObjectID = ObjectID.generate();
38
+
39
+ const permissionValues: (
40
+ permissions: Array<UserPermission>,
41
+ ) => Array<Permission> = (
42
+ permissions: Array<UserPermission>,
43
+ ): Array<Permission> => {
44
+ return permissions.map((p: UserPermission) => {
45
+ return p.permission;
46
+ });
47
+ };
48
+
49
+ describe("APIKeyAccessPermission.getDefaultApiGlobalPermission", () => {
50
+ it("grants Public, User and CurrentUser for the project", async () => {
51
+ const perm: UserGlobalAccessPermission =
52
+ await APIKeyAccessPermission.getDefaultApiGlobalPermission(projectId);
53
+
54
+ expect(perm._type).toBe("UserGlobalAccessPermission");
55
+ expect(
56
+ perm.projectIds.map((p: ObjectID) => {
57
+ return p.toString();
58
+ }),
59
+ ).toEqual([projectId.toString()]);
60
+ expect(perm.globalPermissions).toEqual([
61
+ Permission.Public,
62
+ Permission.User,
63
+ Permission.CurrentUser,
64
+ ]);
65
+ });
66
+
67
+ it("does NOT grant ProjectOwner to an ordinary key", async () => {
68
+ // The core boundary: a default key must never be an owner.
69
+ const perm: UserGlobalAccessPermission =
70
+ await APIKeyAccessPermission.getDefaultApiGlobalPermission(projectId);
71
+ expect(perm.globalPermissions).not.toContain(Permission.ProjectOwner);
72
+ });
73
+ });
74
+
75
+ describe("APIKeyAccessPermission.getMasterKeyApiGlobalPermission", () => {
76
+ it("grants everything the default key has, plus ProjectOwner", async () => {
77
+ const master: UserGlobalAccessPermission =
78
+ await APIKeyAccessPermission.getMasterKeyApiGlobalPermission(projectId);
79
+ const def: UserGlobalAccessPermission =
80
+ await APIKeyAccessPermission.getDefaultApiGlobalPermission(projectId);
81
+
82
+ // Superset: no default grant is lost when elevating to master.
83
+ for (const p of def.globalPermissions) {
84
+ expect(master.globalPermissions).toContain(p);
85
+ }
86
+ // The one deliberate elevation.
87
+ expect(master.globalPermissions).toContain(Permission.ProjectOwner);
88
+ });
89
+
90
+ it("scopes to exactly the one project", async () => {
91
+ const master: UserGlobalAccessPermission =
92
+ await APIKeyAccessPermission.getMasterKeyApiGlobalPermission(projectId);
93
+ expect(
94
+ master.projectIds.map((p: ObjectID) => {
95
+ return p.toString();
96
+ }),
97
+ ).toEqual([projectId.toString()]);
98
+ });
99
+ });
100
+
101
+ describe("APIKeyAccessPermission.getMasterApiTenantAccessPermission", () => {
102
+ it("elevates the default tenant permission with ProjectOwner", async () => {
103
+ const perm: UserTenantAccessPermission =
104
+ await APIKeyAccessPermission.getMasterApiTenantAccessPermission(
105
+ projectId,
106
+ );
107
+
108
+ expect(perm._type).toBe("UserTenantAccessPermission");
109
+ expect(perm.projectId.toString()).toBe(projectId.toString());
110
+ expect(permissionValues(perm.permissions)).toContain(
111
+ Permission.ProjectOwner,
112
+ );
113
+ // The default tenant baseline is retained alongside the elevation.
114
+ expect(permissionValues(perm.permissions)).toContain(
115
+ Permission.CurrentUser,
116
+ );
117
+ });
118
+ });
119
+
120
+ describe("APIKeyAccessPermission.getApiTenantAccessPermission", () => {
121
+ const spyFindBy: jest.SpyInstance = getJestSpyOn(
122
+ ApiKeyPermissionService,
123
+ "findBy",
124
+ );
125
+
126
+ beforeEach(() => {
127
+ jest.clearAllMocks();
128
+ });
129
+
130
+ afterAll(() => {
131
+ jest.restoreAllMocks();
132
+ });
133
+
134
+ const makeRow: (data: {
135
+ permission: Permission;
136
+ labelIds?: Array<ObjectID>;
137
+ isBlockPermission?: boolean;
138
+ }) => APIKeyPermission = (data: {
139
+ permission: Permission;
140
+ labelIds?: Array<ObjectID>;
141
+ isBlockPermission?: boolean;
142
+ }): APIKeyPermission => {
143
+ const row: APIKeyPermission = new APIKeyPermission();
144
+ row.permission = data.permission;
145
+ if (data.labelIds) {
146
+ row.labels = data.labelIds.map((id: ObjectID) => {
147
+ const label: Label = new Label();
148
+ label.id = id;
149
+ return label;
150
+ });
151
+ }
152
+ row.isBlockPermission = data.isBlockPermission ?? false;
153
+ return row;
154
+ };
155
+
156
+ it("queries the permissions for exactly this api key", async () => {
157
+ spyFindBy.mockResolvedValue([] as never);
158
+
159
+ await APIKeyAccessPermission.getApiTenantAccessPermission(
160
+ projectId,
161
+ apiKeyId,
162
+ );
163
+
164
+ expect(spyFindBy).toHaveBeenCalledTimes(1);
165
+ const arg: { query: { apiKeyId: ObjectID } } = spyFindBy.mock
166
+ .calls[0]![0] as { query: { apiKeyId: ObjectID } };
167
+ expect(arg.query.apiKeyId.toString()).toBe(apiKeyId.toString());
168
+ });
169
+
170
+ it("returns just the default baseline when the key has no permissions", async () => {
171
+ spyFindBy.mockResolvedValue([] as never);
172
+
173
+ const perm: UserTenantAccessPermission =
174
+ await APIKeyAccessPermission.getApiTenantAccessPermission(
175
+ projectId,
176
+ apiKeyId,
177
+ );
178
+
179
+ // No stored rows -> no ProjectOwner. An empty key is not an owner.
180
+ expect(permissionValues(perm.permissions)).not.toContain(
181
+ Permission.ProjectOwner,
182
+ );
183
+ expect(permissionValues(perm.permissions)).toContain(
184
+ Permission.CurrentUser,
185
+ );
186
+ });
187
+
188
+ it("maps each stored permission row onto the tenant permission", async () => {
189
+ const labelId: ObjectID = ObjectID.generate();
190
+ spyFindBy.mockResolvedValue([
191
+ makeRow({
192
+ permission: Permission.CreateProjectIncident,
193
+ labelIds: [labelId],
194
+ isBlockPermission: false,
195
+ }),
196
+ ] as never);
197
+
198
+ const perm: UserTenantAccessPermission =
199
+ await APIKeyAccessPermission.getApiTenantAccessPermission(
200
+ projectId,
201
+ apiKeyId,
202
+ );
203
+
204
+ const mapped: UserPermission | undefined = perm.permissions.find(
205
+ (p: UserPermission) => {
206
+ return p.permission === Permission.CreateProjectIncident;
207
+ },
208
+ );
209
+ expect(mapped).toBeDefined();
210
+ /*
211
+ * The label scope must survive the mapping, or the permission silently
212
+ * widens from "these labels" to "all".
213
+ */
214
+ expect(
215
+ mapped!.labelIds.map((l: ObjectID) => {
216
+ return l.toString();
217
+ }),
218
+ ).toEqual([labelId.toString()]);
219
+ });
220
+
221
+ it("preserves the block flag so a deny row stays a deny row", async () => {
222
+ spyFindBy.mockResolvedValue([
223
+ makeRow({
224
+ permission: Permission.CreateProjectIncident,
225
+ labelIds: [],
226
+ isBlockPermission: true,
227
+ }),
228
+ ] as never);
229
+
230
+ const perm: UserTenantAccessPermission =
231
+ await APIKeyAccessPermission.getApiTenantAccessPermission(
232
+ projectId,
233
+ apiKeyId,
234
+ );
235
+
236
+ const mapped: UserPermission | undefined = perm.permissions.find(
237
+ (p: UserPermission) => {
238
+ return p.permission === Permission.CreateProjectIncident;
239
+ },
240
+ );
241
+ // Flipping a block permission to an allow would invert an admin's intent.
242
+ expect(mapped!.isBlockPermission).toBe(true);
243
+ });
244
+
245
+ it("treats a row with no labels as an empty (unscoped) label set", async () => {
246
+ const row: APIKeyPermission = new APIKeyPermission();
247
+ row.permission = Permission.CreateProjectIncident;
248
+ // labels intentionally left undefined.
249
+ row.isBlockPermission = false;
250
+ spyFindBy.mockResolvedValue([row] as never);
251
+
252
+ const perm: UserTenantAccessPermission =
253
+ await APIKeyAccessPermission.getApiTenantAccessPermission(
254
+ projectId,
255
+ apiKeyId,
256
+ );
257
+
258
+ const mapped: UserPermission | undefined = perm.permissions.find(
259
+ (p: UserPermission) => {
260
+ return p.permission === Permission.CreateProjectIncident;
261
+ },
262
+ );
263
+ expect(mapped).toBeDefined();
264
+ // No crash on undefined labels, and it becomes [] rather than undefined.
265
+ expect(mapped!.labelIds).toEqual([]);
266
+ });
267
+ });