@oneuptime/common 12.0.9 → 12.0.11

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 (262) hide show
  1. package/Models/DatabaseModels/NetworkDevice.ts +44 -0
  2. package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
  3. package/Models/DatabaseModels/RunnerJob.ts +17 -2
  4. package/Server/API/LlmProviderAPI.ts +8 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  7. package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +240 -0
  8. package/Server/Services/NetworkDeviceLinkService.ts +338 -2
  9. package/Server/Services/NetworkDeviceService.ts +265 -12
  10. package/Server/Services/RunnerJobService.ts +24 -1
  11. package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
  12. package/Server/Types/Database/JSONColumnQuery.ts +42 -4
  13. package/Server/Utils/LLM/LLMService.ts +108 -19
  14. package/Server/Utils/Monitor/MonitorAlert.ts +22 -115
  15. package/Server/Utils/Monitor/MonitorIncident.ts +7 -337
  16. package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +27 -2
  17. package/Server/Utils/Monitor/SeriesResourceLabels.ts +32 -0
  18. package/Server/Utils/Monitor/SeriesResourceLinker.ts +469 -0
  19. package/Server/Utils/SSRFProtection.ts +5 -4
  20. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
  21. package/Tests/App/AdminDashboard/AdminUsersBulkAddToProject.test.tsx +955 -0
  22. package/Tests/App/AdminDashboard/BulkAddUsersToProject.test.ts +778 -0
  23. package/Tests/App/AdminDashboard/BulkAddUsersToProjectModal.test.tsx +1051 -0
  24. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
  25. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +81 -6
  26. package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
  27. package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
  28. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
  29. package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
  30. package/Tests/App/Dashboard/UserElementEmail.test.tsx +447 -0
  31. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +284 -0
  32. package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
  33. package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
  34. package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
  35. package/Tests/Server/Services/NetworkDeviceLabelRuleRun.test.ts +618 -0
  36. package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
  37. package/Tests/Server/Services/NetworkSiteAssignmentRuleRun.test.ts +706 -0
  38. package/Tests/Server/Services/RunnerJobEnqueue.test.ts +412 -0
  39. package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
  40. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
  41. package/Tests/Server/Utils/AI/LLMServiceRequestPolicy.test.ts +2 -2
  42. package/Tests/Server/Utils/AI/LLMServiceRetryPolicy.test.ts +450 -0
  43. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +556 -0
  44. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +547 -0
  45. package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +30 -0
  46. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +699 -0
  47. package/Tests/Server/Utils/SeriesResourceLabels.test.ts +55 -0
  48. package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
  49. package/Tests/Types/API/URLMalformedTolerance.test.ts +301 -0
  50. package/Tests/Types/API/URLTelScheme.test.ts +261 -0
  51. package/Tests/Types/API/URLUnsupportedScheme.test.ts +148 -0
  52. package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
  53. package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
  54. package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
  55. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +184 -0
  56. package/Tests/Types/Runbook/RunbookStepType.test.ts +124 -0
  57. package/Tests/Types/Workflow/BaseModelComponents.test.ts +17 -0
  58. package/Tests/UI/Components/Button.test.tsx +279 -0
  59. package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
  60. package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
  61. package/Tests/UI/Components/DictionaryValuePlaceholderAndSuggestions.test.tsx +595 -0
  62. package/Tests/UI/Components/Icon.test.tsx +57 -0
  63. package/Tests/UI/Components/List.test.tsx +20 -3
  64. package/Tests/UI/Components/LogsSavedViewsDropdown.test.tsx +304 -0
  65. package/Tests/UI/Components/LogsViewerClearSavedView.test.tsx +179 -0
  66. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
  67. package/Tests/UI/Components/Pagination.test.tsx +721 -146
  68. package/Tests/UI/Components/PaginationIntegration.test.tsx +386 -0
  69. package/Tests/UI/Components/PaginationUtil.test.ts +417 -0
  70. package/Tests/UI/Components/TelemetrySavedViewsDropdown.test.tsx +343 -0
  71. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnControl.test.ts +289 -0
  72. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnOperators.test.ts +271 -0
  73. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRow.test.ts +119 -0
  74. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.test.ts +968 -0
  75. package/Tests/UI/Components/Workflow/ModelColumnEditor.render.test.tsx +766 -0
  76. package/Tests/UI/Utils/RunNetworkRule.test.ts +320 -0
  77. package/Tests/Utils/APIRetryPolicy.test.ts +823 -0
  78. package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
  79. package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
  80. package/Tests/Utils/NetworkAutomation/RuleRunSummary.test.ts +286 -0
  81. package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
  82. package/Types/API/Protocol.ts +9 -0
  83. package/Types/API/URL.ts +286 -20
  84. package/Types/Billing/PayAsYouGoPricing.ts +47 -0
  85. package/Types/CustomField/CustomFieldType.ts +28 -0
  86. package/Types/Monitor/MonitorType.ts +11 -0
  87. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  88. package/Types/NetworkAutomation/RuleRunResult.ts +121 -0
  89. package/Types/Runbook/RunbookStepType.ts +23 -0
  90. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -0
  91. package/UI/Components/Button/Button.tsx +14 -22
  92. package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
  93. package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
  94. package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
  95. package/UI/Components/Charts/Line/LineChart.tsx +4 -2
  96. package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
  97. package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
  98. package/UI/Components/Dictionary/Dictionary.tsx +4 -39
  99. package/UI/Components/Icon/Icon.tsx +1 -1
  100. package/UI/Components/LogsViewer/LogsViewer.tsx +5 -1
  101. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +8 -0
  102. package/UI/Components/LogsViewer/components/LogsPagination.tsx +29 -91
  103. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +2 -0
  104. package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +66 -1
  105. package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
  106. package/UI/Components/Pagination/Pagination.tsx +373 -328
  107. package/UI/Components/Pagination/PaginationUtil.ts +263 -0
  108. package/UI/Components/Pagination/Screenshots/README.md +15 -0
  109. package/UI/Components/Pagination/Screenshots/pagination-before-after.png +0 -0
  110. package/UI/Components/Pagination/Screenshots/pagination-go-to-page-modal.png +0 -0
  111. package/UI/Components/Pagination/Screenshots/pagination-long-list.png +0 -0
  112. package/UI/Components/Pagination/Screenshots/pagination-mobile.png +0 -0
  113. package/UI/Components/Pagination/Screenshots/pagination-telemetry.png +0 -0
  114. package/UI/Components/TelemetryViewer/components/SavedViewsDropdown.tsx +66 -1
  115. package/UI/Components/TelemetryViewer/components/TelemetryPagination.tsx +38 -92
  116. package/UI/Components/TextArea/TextArea.tsx +3 -0
  117. package/UI/Components/Workflow/ArgumentsForm.tsx +18 -1
  118. package/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.tsx +177 -0
  119. package/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.tsx +243 -0
  120. package/UI/Components/Workflow/ColumnEditor/ColumnControl.ts +255 -0
  121. package/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.tsx +178 -0
  122. package/UI/Components/Workflow/ColumnEditor/ColumnOperators.ts +206 -0
  123. package/UI/Components/Workflow/ColumnEditor/ColumnRow.ts +157 -0
  124. package/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.ts +517 -0
  125. package/UI/Components/Workflow/ColumnEditor/ColumnValueInput.tsx +482 -0
  126. package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +131 -0
  127. package/UI/Components/Workflow/ColumnEditor/ModelRecordForm.tsx +163 -0
  128. package/UI/Components/Workflow/ModelColumnEditor.tsx +284 -100
  129. package/UI/Utils/NetworkAutomation/RunNetworkRule.ts +122 -0
  130. package/Utils/API.ts +243 -36
  131. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
  132. package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
  133. package/Utils/NetworkAutomation/RuleRunSummary.ts +147 -0
  134. package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
  135. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  136. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
  137. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
  138. package/build/dist/Models/DatabaseModels/RunnerJob.js +17 -2
  139. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  140. package/build/dist/Server/API/LlmProviderAPI.js +9 -1
  141. package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
  142. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
  143. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js.map +1 -0
  144. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  146. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +202 -0
  147. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
  148. package/build/dist/Server/Services/NetworkDeviceLinkService.js +253 -1
  149. package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
  150. package/build/dist/Server/Services/NetworkDeviceService.js +223 -12
  151. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  152. package/build/dist/Server/Services/RunnerJobService.js +20 -2
  153. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  154. package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
  155. package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
  156. package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
  157. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
  158. package/build/dist/Server/Utils/LLM/LLMService.js +78 -22
  159. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  160. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -98
  161. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  162. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +7 -254
  163. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  164. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +14 -2
  165. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
  166. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +27 -0
  167. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  168. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +285 -0
  169. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -0
  170. package/build/dist/Server/Utils/SSRFProtection.js +5 -4
  171. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  172. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
  173. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  174. package/build/dist/Types/API/Protocol.js +9 -0
  175. package/build/dist/Types/API/Protocol.js.map +1 -1
  176. package/build/dist/Types/API/URL.js +241 -18
  177. package/build/dist/Types/API/URL.js.map +1 -1
  178. package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
  179. package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
  180. package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
  181. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  182. package/build/dist/Types/Monitor/MonitorType.js +10 -0
  183. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  184. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +39 -0
  185. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -0
  186. package/build/dist/Types/Runbook/RunbookStepType.js +21 -0
  187. package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
  188. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  189. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  190. package/build/dist/UI/Components/Button/Button.js +10 -16
  191. package/build/dist/UI/Components/Button/Button.js.map +1 -1
  192. package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
  193. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  194. package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
  195. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  196. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
  197. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
  198. package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
  199. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  200. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
  201. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  202. package/build/dist/UI/Components/Dictionary/Dictionary.js +4 -18
  203. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  204. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  205. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  206. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +3 -3
  207. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  208. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +5 -1
  209. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  210. package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +14 -44
  211. package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -1
  212. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
  213. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  214. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +20 -2
  215. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
  216. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
  217. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  218. package/build/dist/UI/Components/Pagination/Pagination.js +174 -199
  219. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  220. package/build/dist/UI/Components/Pagination/PaginationUtil.js +160 -0
  221. package/build/dist/UI/Components/Pagination/PaginationUtil.js.map +1 -0
  222. package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js +20 -2
  223. package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js.map +1 -1
  224. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js +23 -47
  225. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js.map +1 -1
  226. package/build/dist/UI/Components/TextArea/TextArea.js +1 -1
  227. package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
  228. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +14 -2
  229. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  230. package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js +86 -0
  231. package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js.map +1 -0
  232. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js +119 -0
  233. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js.map +1 -0
  234. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js +206 -0
  235. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js.map +1 -0
  236. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js +57 -0
  237. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js.map +1 -0
  238. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js +150 -0
  239. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js.map +1 -0
  240. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js +67 -0
  241. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js.map +1 -0
  242. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js +309 -0
  243. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js.map +1 -0
  244. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js +204 -0
  245. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js.map +1 -0
  246. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +66 -0
  247. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -0
  248. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js +83 -0
  249. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js.map +1 -0
  250. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +187 -68
  251. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -1
  252. package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js +75 -0
  253. package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js.map +1 -0
  254. package/build/dist/Utils/API.js +175 -34
  255. package/build/dist/Utils/API.js.map +1 -1
  256. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
  257. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
  258. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
  259. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  260. package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js +78 -0
  261. package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js.map +1 -0
  262. package/package.json +1 -1
@@ -0,0 +1,699 @@
1
+ /*
2
+ * PasswordHash carries a pre-existing TS5.9 diagnostic that fails any suite
3
+ * whose runtime require graph reaches it (DatabaseService, the base class of
4
+ * every concrete service, imports it). Nothing password-related is under
5
+ * test here, so the module is replaced WITH A FACTORY — an automock would
6
+ * still require (and type-check) the real file.
7
+ */
8
+ jest.mock("../../../../Server/Utils/PasswordHash", () => {
9
+ return {
10
+ __esModule: true,
11
+ default: {
12
+ hash: jest.fn(),
13
+ verify: jest.fn(),
14
+ generateSalt: jest.fn(),
15
+ needsUpgrade: jest.fn(),
16
+ applyPepper: jest.fn(),
17
+ },
18
+ };
19
+ });
20
+
21
+ import Alert from "../../../../Models/DatabaseModels/Alert";
22
+ import Host from "../../../../Models/DatabaseModels/Host";
23
+ import Incident from "../../../../Models/DatabaseModels/Incident";
24
+ import CephClusterService from "../../../../Server/Services/CephClusterService";
25
+ import DockerHostService from "../../../../Server/Services/DockerHostService";
26
+ import DockerSwarmClusterService from "../../../../Server/Services/DockerSwarmClusterService";
27
+ import HostService from "../../../../Server/Services/HostService";
28
+ import IoTFleetService from "../../../../Server/Services/IoTFleetService";
29
+ import KubernetesClusterService from "../../../../Server/Services/KubernetesClusterService";
30
+ import PodmanHostService from "../../../../Server/Services/PodmanHostService";
31
+ import ProxmoxClusterService from "../../../../Server/Services/ProxmoxClusterService";
32
+ import ServiceService from "../../../../Server/Services/ServiceService";
33
+ import SeriesResourceLinker, {
34
+ SeriesLinkableModel,
35
+ SeriesResolvedResourceIds,
36
+ } from "../../../../Server/Utils/Monitor/SeriesResourceLinker";
37
+ import Includes from "../../../../Types/BaseDatabase/Includes";
38
+ import { JSONObject } from "../../../../Types/JSON";
39
+ import ObjectID from "../../../../Types/ObjectID";
40
+ import { MonitorClusterContext } from "../../../../Server/Utils/Monitor/MonitorClusterContext";
41
+ import {
42
+ afterEach,
43
+ beforeEach,
44
+ describe,
45
+ expect,
46
+ jest,
47
+ test,
48
+ } from "@jest/globals";
49
+
50
+ /*
51
+ * SeriesResourceLinker turns a breaching series' labels into the real
52
+ * resources an alert/incident is about. It is the module behind the
53
+ * "Affected Resources" card, the per-resource Activity tabs, the sidebar
54
+ * badge counts, and resource-owner/label inheritance.
55
+ *
56
+ * The regression that motivated it: a metric monitor grouped by
57
+ * `oneuptime.host.name` (the stamp ingest puts on EVERY host metric row)
58
+ * opened alerts that linked no host at all, because the alert path only
59
+ * ever read the two `host.name` spellings. Test 1 below is that bug.
60
+ */
61
+
62
+ const PROJECT_ID: ObjectID = new ObjectID(
63
+ "11111111-1111-4111-8111-111111111111",
64
+ );
65
+ const OTHER_PROJECT_ID: ObjectID = new ObjectID(
66
+ "99999999-9999-4999-8999-999999999999",
67
+ );
68
+
69
+ /*
70
+ * Every findBy the linker can issue, keyed by the relation it feeds, so a
71
+ * test can say "this one returns a row" and assert every other one was
72
+ * never called.
73
+ */
74
+ interface ResourceServiceUnderTest {
75
+ relation: keyof SeriesLinkableModel;
76
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
+ service: any;
78
+ nameColumn: string;
79
+ }
80
+
81
+ const RESOURCE_SERVICES: Array<ResourceServiceUnderTest> = [
82
+ { relation: "hosts", service: HostService, nameColumn: "hostIdentifier" },
83
+ {
84
+ relation: "dockerHosts",
85
+ service: DockerHostService,
86
+ nameColumn: "hostIdentifier",
87
+ },
88
+ {
89
+ relation: "podmanHosts",
90
+ service: PodmanHostService,
91
+ nameColumn: "hostIdentifier",
92
+ },
93
+ {
94
+ relation: "kubernetesClusters",
95
+ service: KubernetesClusterService,
96
+ nameColumn: "clusterIdentifier",
97
+ },
98
+ { relation: "services", service: ServiceService, nameColumn: "name" },
99
+ {
100
+ relation: "proxmoxClusters",
101
+ service: ProxmoxClusterService,
102
+ nameColumn: "name",
103
+ },
104
+ { relation: "cephClusters", service: CephClusterService, nameColumn: "name" },
105
+ {
106
+ relation: "dockerSwarmClusters",
107
+ service: DockerSwarmClusterService,
108
+ nameColumn: "name",
109
+ },
110
+ { relation: "iotFleets", service: IoTFleetService, nameColumn: "name" },
111
+ ];
112
+
113
+ /*
114
+ * Jest's spy typing fights the loosely-typed findBy signature the linker
115
+ * deliberately abstracts over, so the captured spies are held loosely and
116
+ * asserted through the helpers below.
117
+ */
118
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
119
+ type SpyLike = any;
120
+
121
+ const spies: Map<keyof SeriesLinkableModel, SpyLike> = new Map<
122
+ keyof SeriesLinkableModel,
123
+ SpyLike
124
+ >();
125
+
126
+ /*
127
+ * Rows the fake findBy should return, per relation. A relation with no
128
+ * entry resolves to [] — i.e. "the project has no such resource".
129
+ */
130
+ let rowsByRelation: Map<keyof SeriesLinkableModel, Array<{ _id: string }>>;
131
+
132
+ /*
133
+ * Every query object the linker passed, per relation, so tests can assert
134
+ * on project scoping, the name column, and Includes contents.
135
+ */
136
+ let queriesByRelation: Map<keyof SeriesLinkableModel, Array<JSONObject>>;
137
+
138
+ function relationQueries(
139
+ relation: keyof SeriesLinkableModel,
140
+ ): Array<JSONObject> {
141
+ return queriesByRelation.get(relation) || [];
142
+ }
143
+
144
+ function callCount(relation: keyof SeriesLinkableModel): number {
145
+ return spies.get(relation).mock.calls.length;
146
+ }
147
+
148
+ function idsOn(
149
+ model: SeriesLinkableModel,
150
+ relation: keyof SeriesLinkableModel,
151
+ ): Array<string> {
152
+ const items: Array<{ _id?: string | undefined }> | undefined = model[
153
+ relation
154
+ ] as Array<{ _id?: string | undefined }> | undefined;
155
+
156
+ if (!items) {
157
+ return [];
158
+ }
159
+
160
+ return items.map((item: { _id?: string | undefined }): string => {
161
+ return String(item._id);
162
+ });
163
+ }
164
+
165
+ /*
166
+ * Pull the values out of the Includes the linker built for a query, so a
167
+ * test can assert on the exact identifiers it looked up.
168
+ */
169
+ function includesValues(value: unknown): Array<string> {
170
+ const includes: Includes = value as Includes;
171
+ const values: Array<string | ObjectID | number> = (includes.values ||
172
+ []) as Array<string | ObjectID | number>;
173
+
174
+ return values.map((v: string | ObjectID | number): string => {
175
+ return v.toString();
176
+ });
177
+ }
178
+
179
+ function emptyClusterContext(): MonitorClusterContext {
180
+ return {
181
+ proxmoxClusterIds: [],
182
+ cephClusterIds: [],
183
+ dockerSwarmClusterIds: [],
184
+ iotFleetIds: [],
185
+ };
186
+ }
187
+
188
+ /*
189
+ * Stand one resource service's findBy up as a fake that records the query
190
+ * it was handed and answers with whatever the running test staged.
191
+ */
192
+ function installFindBySpy(resource: ResourceServiceUnderTest): void {
193
+ const spy: SpyLike = jest
194
+ .spyOn(resource.service, "findBy")
195
+ .mockImplementation((args: unknown): Promise<Array<{ _id: string }>> => {
196
+ const call: { query?: JSONObject } = args as { query?: JSONObject };
197
+ const recorded: Array<JSONObject> =
198
+ queriesByRelation.get(resource.relation) || [];
199
+ recorded.push(call.query || {});
200
+ queriesByRelation.set(resource.relation, recorded);
201
+
202
+ return Promise.resolve(rowsByRelation.get(resource.relation) || []);
203
+ });
204
+
205
+ spies.set(resource.relation, spy);
206
+ }
207
+
208
+ beforeEach(() => {
209
+ rowsByRelation = new Map<keyof SeriesLinkableModel, Array<{ _id: string }>>();
210
+ queriesByRelation = new Map<keyof SeriesLinkableModel, Array<JSONObject>>();
211
+ spies.clear();
212
+
213
+ for (const resource of RESOURCE_SERVICES) {
214
+ installFindBySpy(resource);
215
+ }
216
+ });
217
+
218
+ afterEach(() => {
219
+ jest.restoreAllMocks();
220
+ });
221
+
222
+ describe("SeriesResourceLinker.linkSeriesResourcesToModel", () => {
223
+ test("links the host a series identifies by the oneuptime.host.name stamp", async () => {
224
+ /*
225
+ * THE REGRESSION. Ingest stamps `oneuptime.host.name` on every host
226
+ * metric row, and the group-by dropdown offers it, so this is the
227
+ * most common way a grouped host monitor identifies its series.
228
+ */
229
+ rowsByRelation.set("hosts", [{ _id: "host-1" }]);
230
+
231
+ const alert: Alert = new Alert();
232
+
233
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
234
+ model: alert,
235
+ seriesLabels: { "oneuptime.host.name": "prodgreen000002" },
236
+ projectId: PROJECT_ID,
237
+ });
238
+
239
+ expect(idsOn(alert, "hosts")).toEqual(["host-1"]);
240
+ expect(callCount("hosts")).toBe(1);
241
+
242
+ const query: JSONObject = relationQueries("hosts")[0]!;
243
+ expect(includesValues(query["hostIdentifier"])).toEqual([
244
+ "prodgreen000002",
245
+ ]);
246
+ });
247
+
248
+ test.each([
249
+ ["resource.oneuptime.host.name"],
250
+ ["oneuptime.host.name"],
251
+ ["resource.host.name"],
252
+ ["host.name"],
253
+ ])("resolves the host from the %s spelling", async (key: string) => {
254
+ rowsByRelation.set("hosts", [{ _id: "host-1" }]);
255
+
256
+ const alert: Alert = new Alert();
257
+
258
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
259
+ model: alert,
260
+ seriesLabels: { [key]: "web-1" },
261
+ projectId: PROJECT_ID,
262
+ });
263
+
264
+ expect(idsOn(alert, "hosts")).toEqual(["host-1"]);
265
+ expect(
266
+ includesValues(relationQueries("hosts")[0]!["hostIdentifier"]),
267
+ ).toEqual(["web-1"]);
268
+ });
269
+
270
+ test("resolves a host id stamp through the _id column, not the name column", async () => {
271
+ rowsByRelation.set("hosts", [{ _id: "host-1" }]);
272
+
273
+ const alert: Alert = new Alert();
274
+
275
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
276
+ model: alert,
277
+ seriesLabels: { "oneuptime.host.id": "host-1" },
278
+ projectId: PROJECT_ID,
279
+ });
280
+
281
+ expect(callCount("hosts")).toBe(1);
282
+
283
+ const query: JSONObject = relationQueries("hosts")[0]!;
284
+ expect(includesValues(query["_id"])).toEqual(["host-1"]);
285
+ expect(query["hostIdentifier"]).toBeUndefined();
286
+ expect(idsOn(alert, "hosts")).toEqual(["host-1"]);
287
+ });
288
+
289
+ test("queries by id AND name when the series carries both, deduping the result", async () => {
290
+ // Both lookups find the same row; the alert must link it once.
291
+ rowsByRelation.set("hosts", [{ _id: "host-1" }]);
292
+
293
+ const alert: Alert = new Alert();
294
+
295
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
296
+ model: alert,
297
+ seriesLabels: {
298
+ "oneuptime.host.id": "host-1",
299
+ "oneuptime.host.name": "web-1",
300
+ },
301
+ projectId: PROJECT_ID,
302
+ });
303
+
304
+ expect(callCount("hosts")).toBe(2);
305
+ expect(idsOn(alert, "hosts")).toEqual(["host-1"]);
306
+ });
307
+
308
+ test("looks up every value of a multi-valued label in one query", async () => {
309
+ rowsByRelation.set("hosts", [{ _id: "host-1" }, { _id: "host-2" }]);
310
+
311
+ const alert: Alert = new Alert();
312
+
313
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
314
+ model: alert,
315
+ seriesLabels: { "host.name": ["web-1", "web-2"] },
316
+ projectId: PROJECT_ID,
317
+ });
318
+
319
+ expect(callCount("hosts")).toBe(1);
320
+ expect(
321
+ includesValues(relationQueries("hosts")[0]!["hostIdentifier"]).sort(),
322
+ ).toEqual(["web-1", "web-2"]);
323
+ expect(idsOn(alert, "hosts").sort()).toEqual(["host-1", "host-2"]);
324
+ });
325
+
326
+ test.each([
327
+ ["dockerHosts", "oneuptime.docker.host.name", "hostIdentifier"],
328
+ ["podmanHosts", "oneuptime.podman.host.name", "hostIdentifier"],
329
+ ["kubernetesClusters", "k8s.cluster.name", "clusterIdentifier"],
330
+ ["services", "service.name", "name"],
331
+ ["services", "oneuptime.service.name", "name"],
332
+ ["proxmoxClusters", "proxmox.cluster.name", "name"],
333
+ ["cephClusters", "ceph.cluster.name", "name"],
334
+ ["dockerSwarmClusters", "docker.swarm.cluster.name", "name"],
335
+ ["iotFleets", "iot.fleet.name", "name"],
336
+ ])(
337
+ "resolves %s from %s against the %s column",
338
+ async (relation: string, labelKey: string, nameColumn: string) => {
339
+ const typedRelation: keyof SeriesLinkableModel =
340
+ relation as keyof SeriesLinkableModel;
341
+
342
+ rowsByRelation.set(typedRelation, [{ _id: "resource-1" }]);
343
+
344
+ const alert: Alert = new Alert();
345
+
346
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
347
+ model: alert,
348
+ seriesLabels: { [labelKey]: "the-resource" },
349
+ projectId: PROJECT_ID,
350
+ });
351
+
352
+ expect(idsOn(alert, typedRelation)).toEqual(["resource-1"]);
353
+
354
+ const query: JSONObject = relationQueries(typedRelation)[0]!;
355
+ expect(includesValues(query[nameColumn])).toEqual(["the-resource"]);
356
+ },
357
+ );
358
+
359
+ test("a bare host name never resolves a docker or podman host", async () => {
360
+ /*
361
+ * Docker/Podman hosts are deliberately addressed only by their own
362
+ * `oneuptime.*.host.name` stamps. Reading raw host.name for them
363
+ * would attach a container host to an alert about a bare-metal box
364
+ * that merely shares its name.
365
+ */
366
+ rowsByRelation.set("hosts", [{ _id: "host-1" }]);
367
+
368
+ const alert: Alert = new Alert();
369
+
370
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
371
+ model: alert,
372
+ seriesLabels: { "host.name": "shared-name" },
373
+ projectId: PROJECT_ID,
374
+ });
375
+
376
+ expect(callCount("dockerHosts")).toBe(0);
377
+ expect(callCount("podmanHosts")).toBe(0);
378
+ expect(alert.dockerHosts).toBeUndefined();
379
+ expect(alert.podmanHosts).toBeUndefined();
380
+ });
381
+
382
+ test("links every resource type a single series identifies", async () => {
383
+ rowsByRelation.set("hosts", [{ _id: "host-1" }]);
384
+ rowsByRelation.set("kubernetesClusters", [{ _id: "cluster-1" }]);
385
+ rowsByRelation.set("services", [{ _id: "service-1" }]);
386
+
387
+ const alert: Alert = new Alert();
388
+
389
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
390
+ model: alert,
391
+ seriesLabels: {
392
+ "oneuptime.host.name": "web-1",
393
+ "k8s.cluster.name": "prod",
394
+ "service.name": "checkout",
395
+ },
396
+ projectId: PROJECT_ID,
397
+ });
398
+
399
+ expect(idsOn(alert, "hosts")).toEqual(["host-1"]);
400
+ expect(idsOn(alert, "kubernetesClusters")).toEqual(["cluster-1"]);
401
+ expect(idsOn(alert, "services")).toEqual(["service-1"]);
402
+ });
403
+
404
+ test("issues no query at all for labels that identify nothing", async () => {
405
+ const alert: Alert = new Alert();
406
+
407
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
408
+ model: alert,
409
+ seriesLabels: { "unrelated.label": "x", "http.status_code": "500" },
410
+ projectId: PROJECT_ID,
411
+ });
412
+
413
+ for (const resource of RESOURCE_SERVICES) {
414
+ expect(callCount(resource.relation)).toBe(0);
415
+ expect(alert[resource.relation]).toBeUndefined();
416
+ }
417
+ });
418
+
419
+ test("issues no query for an empty label set", async () => {
420
+ const alert: Alert = new Alert();
421
+
422
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
423
+ model: alert,
424
+ seriesLabels: {},
425
+ projectId: PROJECT_ID,
426
+ });
427
+
428
+ for (const resource of RESOURCE_SERVICES) {
429
+ expect(callCount(resource.relation)).toBe(0);
430
+ }
431
+ });
432
+
433
+ test("scopes every lookup to the passed project", async () => {
434
+ /*
435
+ * Tenant isolation. These lookups run with isRoot, which bypasses
436
+ * row-level access control, so the project scope is the only thing
437
+ * stopping a stale or hostile id stamp from pulling in another
438
+ * tenant's row.
439
+ */
440
+ const alert: Alert = new Alert();
441
+
442
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
443
+ model: alert,
444
+ seriesLabels: {
445
+ "oneuptime.host.id": "host-1",
446
+ "oneuptime.host.name": "web-1",
447
+ "oneuptime.docker.host.name": "docker-1",
448
+ "oneuptime.podman.host.name": "podman-1",
449
+ "k8s.cluster.name": "k8s-1",
450
+ "service.name": "svc-1",
451
+ "proxmox.cluster.name": "pve-1",
452
+ "ceph.cluster.name": "ceph-1",
453
+ "docker.swarm.cluster.name": "swarm-1",
454
+ "iot.fleet.name": "fleet-1",
455
+ },
456
+ projectId: PROJECT_ID,
457
+ });
458
+
459
+ let assertedQueries: number = 0;
460
+
461
+ for (const resource of RESOURCE_SERVICES) {
462
+ const queries: Array<JSONObject> = relationQueries(resource.relation);
463
+ expect(queries.length).toBeGreaterThan(0);
464
+
465
+ for (const query of queries) {
466
+ expect(query["projectId"]).toBe(PROJECT_ID);
467
+ assertedQueries++;
468
+ }
469
+ }
470
+
471
+ // 9 types, and hosts get an extra query for the id stamp.
472
+ expect(assertedQueries).toBe(10);
473
+ });
474
+
475
+ test("does not link a resource that belongs to another project", async () => {
476
+ /*
477
+ * The service is queried with the alert's own project, so a row from
478
+ * elsewhere simply is not returned — the relation stays unset rather
479
+ * than linking across the tenant boundary.
480
+ */
481
+ const alert: Alert = new Alert();
482
+
483
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
484
+ model: alert,
485
+ seriesLabels: { "oneuptime.host.name": "web-1" },
486
+ projectId: OTHER_PROJECT_ID,
487
+ });
488
+
489
+ expect(relationQueries("hosts")[0]!["projectId"]).toBe(OTHER_PROJECT_ID);
490
+ expect(alert.hosts).toBeUndefined();
491
+ });
492
+
493
+ test("leaves the relation unset when the resource no longer exists", async () => {
494
+ // findBy returns [] — the host was deleted, or never discovered.
495
+ const alert: Alert = new Alert();
496
+
497
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
498
+ model: alert,
499
+ seriesLabels: { "oneuptime.host.name": "ghost-host" },
500
+ projectId: PROJECT_ID,
501
+ });
502
+
503
+ expect(callCount("hosts")).toBe(1);
504
+ expect(alert.hosts).toBeUndefined();
505
+ });
506
+
507
+ test("preserves resources already attached to the model", async () => {
508
+ rowsByRelation.set("hosts", [{ _id: "host-2" }]);
509
+
510
+ const alert: Alert = new Alert();
511
+ const preExisting: Host = new Host();
512
+ preExisting._id = "host-1";
513
+ alert.hosts = [preExisting];
514
+
515
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
516
+ model: alert,
517
+ seriesLabels: { "oneuptime.host.name": "web-2" },
518
+ projectId: PROJECT_ID,
519
+ });
520
+
521
+ expect(idsOn(alert, "hosts")).toEqual(["host-1", "host-2"]);
522
+ });
523
+
524
+ test("does not attach the same resource twice", async () => {
525
+ rowsByRelation.set("hosts", [{ _id: "host-1" }]);
526
+
527
+ const alert: Alert = new Alert();
528
+ const preExisting: Host = new Host();
529
+ preExisting._id = "host-1";
530
+ alert.hosts = [preExisting];
531
+
532
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
533
+ model: alert,
534
+ seriesLabels: { "oneuptime.host.name": "web-1" },
535
+ projectId: PROJECT_ID,
536
+ });
537
+
538
+ expect(idsOn(alert, "hosts")).toEqual(["host-1"]);
539
+ });
540
+
541
+ test("links an incident exactly as it links an alert", async () => {
542
+ /*
543
+ * The whole point of the shared util: the two creation paths must
544
+ * not drift apart again.
545
+ */
546
+ rowsByRelation.set("hosts", [{ _id: "host-1" }]);
547
+ rowsByRelation.set("services", [{ _id: "service-1" }]);
548
+
549
+ const labels: JSONObject = {
550
+ "oneuptime.host.name": "web-1",
551
+ "service.name": "checkout",
552
+ };
553
+
554
+ const alert: Alert = new Alert();
555
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
556
+ model: alert,
557
+ seriesLabels: labels,
558
+ projectId: PROJECT_ID,
559
+ });
560
+
561
+ const incident: Incident = new Incident();
562
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
563
+ model: incident,
564
+ seriesLabels: labels,
565
+ projectId: PROJECT_ID,
566
+ });
567
+
568
+ expect(idsOn(incident, "hosts")).toEqual(idsOn(alert, "hosts"));
569
+ expect(idsOn(incident, "services")).toEqual(idsOn(alert, "services"));
570
+ });
571
+ });
572
+
573
+ describe("SeriesResourceLinker.resolveResourcesFromSeriesLabels", () => {
574
+ test("returns ids per type without touching a model", async () => {
575
+ rowsByRelation.set("hosts", [{ _id: "host-1" }]);
576
+ rowsByRelation.set("iotFleets", [{ _id: "fleet-1" }]);
577
+
578
+ const resolved: SeriesResolvedResourceIds =
579
+ await SeriesResourceLinker.resolveResourcesFromSeriesLabels({
580
+ seriesLabels: {
581
+ "oneuptime.host.name": "web-1",
582
+ "iot.fleet.name": "fleet-a",
583
+ },
584
+ projectId: PROJECT_ID,
585
+ });
586
+
587
+ expect(resolved.hostIds).toEqual(["host-1"]);
588
+ expect(resolved.iotFleetIds).toEqual(["fleet-1"]);
589
+ expect(resolved.serviceIds).toEqual([]);
590
+ expect(resolved.dockerHostIds).toEqual([]);
591
+ expect(resolved.podmanHostIds).toEqual([]);
592
+ expect(resolved.kubernetesClusterIds).toEqual([]);
593
+ expect(resolved.proxmoxClusterIds).toEqual([]);
594
+ expect(resolved.cephClusterIds).toEqual([]);
595
+ expect(resolved.dockerSwarmClusterIds).toEqual([]);
596
+ });
597
+ });
598
+
599
+ describe("SeriesResourceLinker.attachClusterContext", () => {
600
+ test("attaches the monitor-step clusters", () => {
601
+ const alert: Alert = new Alert();
602
+
603
+ SeriesResourceLinker.attachClusterContext({
604
+ model: alert,
605
+ clusterContext: {
606
+ proxmoxClusterIds: ["pve-1"],
607
+ cephClusterIds: ["ceph-1"],
608
+ dockerSwarmClusterIds: ["swarm-1"],
609
+ iotFleetIds: ["fleet-1"],
610
+ },
611
+ });
612
+
613
+ expect(idsOn(alert, "proxmoxClusters")).toEqual(["pve-1"]);
614
+ expect(idsOn(alert, "cephClusters")).toEqual(["ceph-1"]);
615
+ expect(idsOn(alert, "dockerSwarmClusters")).toEqual(["swarm-1"]);
616
+ expect(idsOn(alert, "iotFleets")).toEqual(["fleet-1"]);
617
+ });
618
+
619
+ test("merges with the label path instead of overwriting it", async () => {
620
+ /*
621
+ * The alert path used to ASSIGN the step-config clusters, which was
622
+ * only safe while its label path could not resolve them. Now that it
623
+ * can, an overwrite would silently drop the cluster the series
624
+ * itself named.
625
+ */
626
+ rowsByRelation.set("proxmoxClusters", [{ _id: "pve-from-label" }]);
627
+
628
+ const alert: Alert = new Alert();
629
+
630
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
631
+ model: alert,
632
+ seriesLabels: { "proxmox.cluster.name": "pve-a" },
633
+ projectId: PROJECT_ID,
634
+ });
635
+
636
+ SeriesResourceLinker.attachClusterContext({
637
+ model: alert,
638
+ clusterContext: {
639
+ ...emptyClusterContext(),
640
+ proxmoxClusterIds: ["pve-from-step-config"],
641
+ },
642
+ });
643
+
644
+ expect(idsOn(alert, "proxmoxClusters")).toEqual([
645
+ "pve-from-label",
646
+ "pve-from-step-config",
647
+ ]);
648
+ });
649
+
650
+ test("dedupes a cluster both paths resolved", async () => {
651
+ rowsByRelation.set("cephClusters", [{ _id: "ceph-1" }]);
652
+
653
+ const alert: Alert = new Alert();
654
+
655
+ await SeriesResourceLinker.linkSeriesResourcesToModel({
656
+ model: alert,
657
+ seriesLabels: { "ceph.cluster.name": "ceph-a" },
658
+ projectId: PROJECT_ID,
659
+ });
660
+
661
+ SeriesResourceLinker.attachClusterContext({
662
+ model: alert,
663
+ clusterContext: {
664
+ ...emptyClusterContext(),
665
+ cephClusterIds: ["ceph-1"],
666
+ },
667
+ });
668
+
669
+ expect(idsOn(alert, "cephClusters")).toEqual(["ceph-1"]);
670
+ });
671
+
672
+ test("leaves every relation untouched when the monitor has no clusters", () => {
673
+ const alert: Alert = new Alert();
674
+
675
+ SeriesResourceLinker.attachClusterContext({
676
+ model: alert,
677
+ clusterContext: emptyClusterContext(),
678
+ });
679
+
680
+ expect(alert.proxmoxClusters).toBeUndefined();
681
+ expect(alert.cephClusters).toBeUndefined();
682
+ expect(alert.dockerSwarmClusters).toBeUndefined();
683
+ expect(alert.iotFleets).toBeUndefined();
684
+ });
685
+
686
+ test("attaches to an incident the same way", () => {
687
+ const incident: Incident = new Incident();
688
+
689
+ SeriesResourceLinker.attachClusterContext({
690
+ model: incident,
691
+ clusterContext: {
692
+ ...emptyClusterContext(),
693
+ dockerSwarmClusterIds: ["swarm-1"],
694
+ },
695
+ });
696
+
697
+ expect(idsOn(incident, "dockerSwarmClusters")).toEqual(["swarm-1"]);
698
+ });
699
+ });