@oneuptime/common 12.0.34 → 13.0.0

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 (259) hide show
  1. package/Models/DatabaseModels/GlobalConfig.ts +12 -12
  2. package/Models/DatabaseModels/InventoryItem.ts +44 -1
  3. package/Models/DatabaseModels/NetworkDevice.ts +126 -0
  4. package/Server/EnvironmentConfig.ts +47 -16
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.ts +25 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  7. package/Server/Infrastructure/Redis.ts +8 -8
  8. package/Server/Infrastructure/Status.ts +10 -4
  9. package/Server/Services/DatabaseService.ts +65 -13
  10. package/Server/Services/InventoryItemService.ts +31 -1
  11. package/Server/Services/MonitorTemplateService.ts +140 -19
  12. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +349 -41
  13. package/Server/Services/NetworkDeviceDiscoveryScanService.ts +87 -2
  14. package/Server/Services/NetworkDeviceService.ts +69 -0
  15. package/Server/Services/UserNotificationRuleService.ts +76 -25
  16. package/Server/Types/Database/Permissions/ReadPermission.ts +45 -5
  17. package/Server/Types/Database/QueryUtil.ts +56 -0
  18. package/Server/Types/Markdown.ts +93 -1
  19. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +82 -17
  20. package/Server/Utils/EmailRollup/EmailRollupFlushRunner.ts +10 -1
  21. package/Server/Utils/EmailRollup/EmailRollupRenderer.ts +2 -2
  22. package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +82 -35
  23. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +31 -5
  24. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +202 -37
  25. package/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.ts +82 -9
  26. package/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.ts +16 -7
  27. package/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.ts +29 -17
  28. package/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.ts +107 -48
  29. package/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.ts +158 -0
  30. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +27 -0
  31. package/Server/Utils/Telemetry/EntityRegistry.ts +11 -1
  32. package/Server/Utils/Telemetry/TelemetryEntity.ts +83 -9
  33. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +32 -0
  34. package/Tests/App/Dashboard/AdoptedDeviceDrawer.test.tsx +374 -0
  35. package/Tests/App/Dashboard/DashboardLabelVariables.test.tsx +580 -0
  36. package/Tests/App/Dashboard/DashboardMonitorLabelVariable.test.tsx +438 -0
  37. package/Tests/App/Dashboard/DashboardVariableAllContract.test.tsx +5 -2
  38. package/Tests/App/Dashboard/DeviceAttachmentCard.test.tsx +273 -0
  39. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +505 -0
  40. package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +163 -0
  41. package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +401 -0
  42. package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +232 -0
  43. package/Tests/App/Dashboard/InventoryItems.test.tsx +201 -0
  44. package/Tests/App/Dashboard/InventoryTypeAndStatusFacets.test.tsx +798 -0
  45. package/Tests/App/Dashboard/NetworkTopologyLiveView.test.tsx +320 -0
  46. package/Tests/App/Dashboard/NetworkTopologyToolbar.test.tsx +275 -0
  47. package/Tests/App/Dashboard/OnCallCalendarFeedEmptyState.test.tsx +975 -0
  48. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +63 -0
  49. package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +626 -0
  50. package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +348 -0
  51. package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +331 -0
  52. package/Tests/App/Dashboard/UserSettingsEmailPreferences.test.tsx +603 -0
  53. package/Tests/App/Dashboard/UserSettingsNotificationSettings.test.tsx +324 -0
  54. package/Tests/App/PublicDashboard/DashboardVariableSelector.test.tsx +13 -4
  55. package/Tests/App/StatusPage/StatusPageOidcOrigin.test.tsx +223 -0
  56. package/Tests/Models/InventoryItemStatus.test.ts +147 -0
  57. package/Tests/Models/NetworkDeviceMacAddressColumn.test.ts +294 -0
  58. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +129 -0
  59. package/Tests/Server/EnvironmentConfigValkey.test.ts +227 -0
  60. package/Tests/Server/Infrastructure/Postgres/AddMacAddressToNetworkDeviceMigration.test.ts +365 -0
  61. package/Tests/Server/Infrastructure/Postgres/CustomFieldValueMappingMigration.test.ts +62 -23
  62. package/Tests/Server/Infrastructure/ValkeyDeploymentConfig.test.ts +265 -0
  63. package/Tests/Server/Middleware/HttpMetricsMiddleware.test.ts +382 -0
  64. package/Tests/Server/Middleware/SCIMAuthorization.test.ts +525 -0
  65. package/Tests/Server/Services/DatabaseServiceAggregateBy.test.ts +291 -56
  66. package/Tests/Server/Services/DatabaseServiceSortTiebreaker.test.ts +151 -0
  67. package/Tests/Server/Services/InventoryItemManualCreate.test.ts +105 -0
  68. package/Tests/Server/Services/InventoryItemPromotionGate.test.ts +178 -0
  69. package/Tests/Server/Services/MonitorTemplateServiceCustomFieldSync.test.ts +502 -0
  70. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +1133 -81
  71. package/Tests/Server/Services/NetworkDeviceDiscoveryScanRegistration.test.ts +520 -0
  72. package/Tests/Server/Services/NetworkDeviceMacAddressNormalization.test.ts +572 -0
  73. package/Tests/Server/Types/Database/InventoryStatusPostgres.test.ts +368 -0
  74. package/Tests/Server/Types/Database/Permissions/ReadBlockPermission.test.ts +261 -14
  75. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroups.test.ts +169 -0
  76. package/Tests/Server/Types/Database/QueryUtilIncludesAnyOfGroupsPostgres.test.ts +297 -0
  77. package/Tests/Server/Types/Markdown.test.ts +194 -1
  78. package/Tests/Server/Types/Workflow/Components/IncomingWebhookUtils.test.ts +279 -0
  79. package/Tests/Server/Types/Workflow/Components/TextToJson.test.ts +212 -0
  80. package/Tests/Server/Utils/CustomField/CustomFieldMappingRegistry.test.ts +436 -0
  81. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +214 -0
  82. package/Tests/Server/Utils/EmailRollup/EmailRollupFlushRunnerBehaviour.test.ts +1 -1
  83. package/Tests/Server/Utils/EmailRollup/EmailRollupRenderer.test.ts +10 -3
  84. package/Tests/Server/Utils/EventLoop.test.ts +359 -0
  85. package/Tests/Server/Utils/JsonWebToken.test.ts +30 -0
  86. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +242 -0
  87. package/Tests/Server/Utils/Monitor/Criteria/CompareCriteriaAggregation.test.ts +13 -7
  88. package/Tests/Server/Utils/Monitor/Criteria/DatabaseMonitorCriteria.test.ts +29 -0
  89. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +30 -6
  90. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +246 -0
  91. package/Tests/Server/Utils/Monitor/MonitorCriteriaEmailEvalLogAgreement.test.ts +280 -0
  92. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +224 -10
  93. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +570 -0
  94. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilderUnits.test.ts +30 -5
  95. package/Tests/Server/Utils/Monitor/MonitorCriteriaMessageBuilderUnits.test.ts +68 -21
  96. package/Tests/Server/Utils/Monitor/MonitorCriteriaObservationBuilderUnits.test.ts +36 -19
  97. package/Tests/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.test.ts +573 -0
  98. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +317 -0
  99. package/Tests/Server/Utils/Telemetry/ContextSpanProcessor.test.ts +281 -0
  100. package/Tests/Server/Utils/Telemetry/InventoryDuplicateIdentity.test.ts +316 -0
  101. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +111 -8
  102. package/Tests/Types/Database/IncludesAnyOfGroups.test.ts +106 -0
  103. package/Tests/Types/JSONFunctions.test.ts +40 -0
  104. package/Tests/Types/NetworkAutomation/AutoImportRunMerge.test.ts +235 -0
  105. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +66 -0
  106. package/Tests/Types/NetworkAutomation/RuleRunResultDescribe.test.ts +309 -1
  107. package/Tests/Types/Telemetry/InventoryLiveness.test.ts +99 -0
  108. package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +160 -0
  109. package/Tests/UI/ReactRouterSingletonBuild.test.ts +133 -0
  110. package/Tests/UI/Rum/ReplayHeader.test.tsx +10 -2
  111. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +514 -0
  112. package/Tests/UI/Rum/ReplayTimeFormat.test.ts +94 -0
  113. package/Tests/UI/Rum/ReplayUi.test.tsx +559 -0
  114. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
  115. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +1 -0
  116. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +11 -1
  117. package/Tests/Utils/Dashboard/LabelVariable.test.ts +257 -0
  118. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +144 -14
  119. package/Tests/Utils/MetricUnitUtil.test.ts +81 -0
  120. package/Tests/Utils/Monitor/DeviceMacLearningUtil.test.ts +497 -0
  121. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +546 -0
  122. package/Tests/Utils/Monitor/MonitorTemplateCustomFieldUtil.test.ts +218 -0
  123. package/Tests/Utils/Monitor/NetworkTopologyEndpointAdoption.test.ts +1755 -0
  124. package/Tests/Utils/NetworkAutomation/AutoImportRunChain.test.ts +320 -0
  125. package/Types/BaseDatabase/IncludesAnyOfGroups.ts +77 -0
  126. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  127. package/Types/Dashboard/DashboardComponents/DashboardMonitorListComponent.ts +1 -0
  128. package/Types/Dashboard/DashboardVariable.ts +8 -0
  129. package/Types/JSON.ts +4 -0
  130. package/Types/JSONFunctions.ts +16 -0
  131. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  132. package/Types/NetworkAutomation/RuleRunResult.ts +402 -13
  133. package/Types/SerializableObjectDictionary.ts +4 -0
  134. package/Types/Telemetry/InventoryLiveness.ts +56 -0
  135. package/UI/Components/CSVFileUpload/CSVFileUpload.tsx +9 -2
  136. package/UI/Components/CustomFields/CustomFieldsDetail.tsx +46 -21
  137. package/UI/Components/Dashboard/DashboardVariableControl.tsx +285 -0
  138. package/UI/esbuild-config.js +6 -0
  139. package/Utils/Dashboard/Components/DashboardMonitorListComponent.ts +12 -1
  140. package/Utils/Dashboard/LabelVariable.ts +152 -0
  141. package/Utils/Dashboard/VariableUrlState.ts +27 -5
  142. package/Utils/MetricUnitUtil.ts +34 -0
  143. package/Utils/Monitor/DeviceMacLearningUtil.ts +177 -0
  144. package/Utils/Monitor/MetricValueFormatter.ts +342 -0
  145. package/Utils/Monitor/MonitorTemplateCustomFieldUtil.ts +124 -0
  146. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +7 -2
  147. package/Utils/Monitor/NetworkTopologyUtil.ts +693 -0
  148. package/Utils/NetworkAutomation/AutoImportRunChain.ts +138 -0
  149. package/build/dist/Models/DatabaseModels/GlobalConfig.js +12 -12
  150. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  151. package/build/dist/Models/DatabaseModels/InventoryItem.js +34 -1
  152. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -1
  153. package/build/dist/Models/DatabaseModels/NetworkDevice.js +128 -0
  154. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  155. package/build/dist/Server/EnvironmentConfig.js +44 -12
  156. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  157. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js +14 -0
  158. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1791700000000-AddMacAddressToNetworkDevice.js.map +1 -0
  159. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  160. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  161. package/build/dist/Server/Infrastructure/Redis.js +8 -8
  162. package/build/dist/Server/Infrastructure/Redis.js.map +1 -1
  163. package/build/dist/Server/Infrastructure/Status.js +10 -4
  164. package/build/dist/Server/Infrastructure/Status.js.map +1 -1
  165. package/build/dist/Server/Services/DatabaseService.js +58 -13
  166. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  167. package/build/dist/Server/Services/InventoryItemService.js +28 -1
  168. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  169. package/build/dist/Server/Services/MonitorTemplateService.js +116 -17
  170. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  171. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +270 -37
  172. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -1
  173. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js +65 -0
  174. package/build/dist/Server/Services/NetworkDeviceDiscoveryScanService.js.map +1 -1
  175. package/build/dist/Server/Services/NetworkDeviceService.js +54 -0
  176. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  177. package/build/dist/Server/Services/UserNotificationRuleService.js +60 -25
  178. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  179. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +29 -4
  180. package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
  181. package/build/dist/Server/Types/Database/QueryUtil.js +36 -0
  182. package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
  183. package/build/dist/Server/Types/Markdown.js +75 -0
  184. package/build/dist/Server/Types/Markdown.js.map +1 -1
  185. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +53 -18
  186. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  187. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js +10 -1
  188. package/build/dist/Server/Utils/EmailRollup/EmailRollupFlushRunner.js.map +1 -1
  189. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js +2 -2
  190. package/build/dist/Server/Utils/EmailRollup/EmailRollupRenderer.js.map +1 -1
  191. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +58 -35
  192. package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
  193. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +29 -5
  194. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  195. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +166 -35
  196. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  197. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js +58 -9
  198. package/build/dist/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.js.map +1 -1
  199. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js +9 -3
  200. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageBuilder.js.map +1 -1
  201. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js +26 -12
  202. package/build/dist/Server/Utils/Monitor/MonitorCriteriaMessageFormatter.js.map +1 -1
  203. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js +74 -33
  204. package/build/dist/Server/Utils/Monitor/MonitorCriteriaObservationBuilder.js.map +1 -1
  205. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js +114 -0
  206. package/build/dist/Server/Utils/Monitor/NetworkDeviceMacLearningUtil.js.map +1 -0
  207. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +25 -0
  208. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  209. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +9 -1
  210. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  211. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +70 -13
  212. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  213. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js +54 -0
  214. package/build/dist/Types/BaseDatabase/IncludesAnyOfGroups.js.map +1 -0
  215. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  216. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  217. package/build/dist/Types/Dashboard/DashboardVariable.js +1 -0
  218. package/build/dist/Types/Dashboard/DashboardVariable.js.map +1 -1
  219. package/build/dist/Types/JSON.js +1 -0
  220. package/build/dist/Types/JSON.js.map +1 -1
  221. package/build/dist/Types/JSONFunctions.js +13 -0
  222. package/build/dist/Types/JSONFunctions.js.map +1 -1
  223. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +291 -6
  224. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
  225. package/build/dist/Types/SerializableObjectDictionary.js +4 -0
  226. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  227. package/build/dist/Types/Telemetry/InventoryLiveness.js +37 -0
  228. package/build/dist/Types/Telemetry/InventoryLiveness.js.map +1 -0
  229. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js +7 -0
  230. package/build/dist/UI/Components/CSVFileUpload/CSVFileUpload.js.map +1 -1
  231. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +9 -2
  232. package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
  233. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js +137 -0
  234. package/build/dist/UI/Components/Dashboard/DashboardVariableControl.js.map +1 -0
  235. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js +9 -1
  236. package/build/dist/Utils/Dashboard/Components/DashboardMonitorListComponent.js.map +1 -1
  237. package/build/dist/Utils/Dashboard/LabelVariable.js +96 -0
  238. package/build/dist/Utils/Dashboard/LabelVariable.js.map +1 -0
  239. package/build/dist/Utils/Dashboard/VariableUrlState.js +19 -4
  240. package/build/dist/Utils/Dashboard/VariableUrlState.js.map +1 -1
  241. package/build/dist/Utils/MetricUnitUtil.js +26 -0
  242. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  243. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js +112 -0
  244. package/build/dist/Utils/Monitor/DeviceMacLearningUtil.js.map +1 -0
  245. package/build/dist/Utils/Monitor/MetricValueFormatter.js +276 -0
  246. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -0
  247. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js +101 -0
  248. package/build/dist/Utils/Monitor/MonitorTemplateCustomFieldUtil.js.map +1 -0
  249. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +7 -2
  250. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  251. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +484 -0
  252. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  253. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js +59 -0
  254. package/build/dist/Utils/NetworkAutomation/AutoImportRunChain.js.map +1 -0
  255. package/jest.config.json +2 -0
  256. package/package.json +1 -1
  257. package/test-setup.sh +9 -9
  258. package/tsconfig.json +51 -11
  259. package/Tests/App/Dashboard/NotificationEmailPreferences.test.tsx +0 -399
@@ -0,0 +1,178 @@
1
+ import GlobalCache from "../../../Server/Infrastructure/GlobalCache";
2
+ import InventoryItem from "../../../Models/DatabaseModels/InventoryItem";
3
+ import InventoryItemService from "../../../Server/Services/InventoryItemService";
4
+ import Service from "../../../Models/DatabaseModels/Service";
5
+ import ServiceService from "../../../Server/Services/ServiceService";
6
+ import ObjectID from "../../../Types/ObjectID";
7
+ import PositiveNumber from "../../../Types/PositiveNumber";
8
+ import EntitySource from "../../../Types/Telemetry/EntitySource";
9
+ import EntityType from "../../../Types/Telemetry/EntityType";
10
+ import { ExtractedEntity } from "../../../Server/Utils/Telemetry/TelemetryEntity";
11
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
12
+
13
+ /*
14
+ * What may become a row in the Inventory registry.
15
+ *
16
+ * Two gates decide it, and they answer different questions. The TYPE gate asks
17
+ * whether this kind of thing belongs in the registry at all — containers,
18
+ * processes, service instances and SDKs are high-churn, so their keys travel on
19
+ * signals but never mint rows. The per-entity `membershipOnly` flag asks
20
+ * whether this particular observation is the resource's entity of that type or
21
+ * a duplicate the producer sent alongside it: one OTLP resource describes one
22
+ * pod, on one node, in one cluster, but nothing stops a producer emitting two
23
+ * `entity_refs` of the same type, and promoting both put two rows in Inventory
24
+ * from a single batch.
25
+ *
26
+ * `reconcileEntities` is public, so it repeats both gates rather than trusting
27
+ * its caller — the cost of a missed gate here is a row that should never have
28
+ * existed, and once created nothing removes it until its TTL expires.
29
+ *
30
+ * Everything external is mocked — no Postgres, no Redis.
31
+ */
32
+
33
+ const PROJECT_ID: ObjectID = ObjectID.generate();
34
+ const SERVICE_ROW_ID: ObjectID = ObjectID.generate();
35
+
36
+ let createdRows: Array<InventoryItem>;
37
+ let bumps: Array<ObjectID>;
38
+
39
+ function entity(
40
+ entityType: EntityType,
41
+ overrides: Partial<ExtractedEntity> = {},
42
+ ): ExtractedEntity {
43
+ return {
44
+ entityType,
45
+ entityKey: "0123456789abcdef",
46
+ identifyingAttributes: { "some.name": "thing" },
47
+ ...overrides,
48
+ };
49
+ }
50
+
51
+ function reconcile(entities: Array<ExtractedEntity>): Promise<void> {
52
+ return InventoryItemService.reconcileEntities({
53
+ projectId: PROJECT_ID,
54
+ entities,
55
+ });
56
+ }
57
+
58
+ beforeEach(() => {
59
+ createdRows = [];
60
+ bumps = [];
61
+
62
+ // Both fences open, so every reconcile actually does its work.
63
+ jest
64
+ .spyOn(GlobalCache, "setStringIfNotExists")
65
+ .mockImplementation(async () => {
66
+ return true;
67
+ });
68
+ jest.spyOn(GlobalCache, "deleteKey").mockImplementation(async () => {
69
+ return undefined as never;
70
+ });
71
+
72
+ const serviceRow: Service = new Service();
73
+ serviceRow.id = SERVICE_ROW_ID;
74
+ serviceRow._id = SERVICE_ROW_ID.toString();
75
+ jest
76
+ .spyOn(ServiceService, "findOneBy")
77
+ .mockImplementation(async (): Promise<Service | null> => {
78
+ return serviceRow;
79
+ });
80
+
81
+ jest
82
+ .spyOn(InventoryItemService, "findOneBy")
83
+ .mockImplementation(async (): Promise<InventoryItem | null> => {
84
+ return null;
85
+ });
86
+
87
+ jest
88
+ .spyOn(InventoryItemService, "countBy")
89
+ .mockImplementation(async (): Promise<PositiveNumber> => {
90
+ return new PositiveNumber(0);
91
+ });
92
+
93
+ jest
94
+ .spyOn(InventoryItemService, "create")
95
+ .mockImplementation(async (createBy: unknown): Promise<InventoryItem> => {
96
+ const data: InventoryItem = (createBy as { data: InventoryItem }).data;
97
+ createdRows.push(data);
98
+ return data;
99
+ });
100
+
101
+ jest
102
+ .spyOn(InventoryItemService, "updateColumnsByIdIfUnlockedWithoutHooks")
103
+ .mockImplementation(async (data: unknown): Promise<boolean> => {
104
+ bumps.push((data as { id: ObjectID }).id);
105
+ return true;
106
+ });
107
+ });
108
+
109
+ afterEach(() => {
110
+ jest.restoreAllMocks();
111
+ });
112
+
113
+ describe("membership-only entities never become rows", () => {
114
+ test.each([
115
+ EntityType.Container,
116
+ EntityType.Process,
117
+ EntityType.ServiceInstance,
118
+ EntityType.TelemetrySdk,
119
+ ])("%s is not promoted by type", async (entityType: EntityType) => {
120
+ await reconcile([entity(entityType)]);
121
+
122
+ expect(createdRows).toHaveLength(0);
123
+ expect(bumps).toHaveLength(0);
124
+ });
125
+
126
+ test.each([
127
+ EntityType.KubernetesPod,
128
+ EntityType.KubernetesNode,
129
+ EntityType.Service,
130
+ EntityType.Host,
131
+ ])(
132
+ "%s flagged membershipOnly is not promoted even though its type is",
133
+ async (entityType: EntityType) => {
134
+ await reconcile([entity(entityType, { membershipOnly: true })]);
135
+
136
+ expect(createdRows).toHaveLength(0);
137
+ expect(bumps).toHaveLength(0);
138
+ },
139
+ );
140
+
141
+ test("a demoted duplicate does not suppress the promoted entity beside it", async () => {
142
+ /*
143
+ * The shape `entitiesFromRefs` produces when a producer sends two refs of
144
+ * one type: the first is the entity, the rest are membership keys.
145
+ */
146
+ await reconcile([
147
+ entity(EntityType.KubernetesPod, { entityKey: "aaaaaaaaaaaaaaaa" }),
148
+ entity(EntityType.KubernetesPod, {
149
+ entityKey: "bbbbbbbbbbbbbbbb",
150
+ membershipOnly: true,
151
+ }),
152
+ ]);
153
+
154
+ expect(createdRows).toHaveLength(1);
155
+ expect(createdRows[0]!.entityKey).toBe("aaaaaaaaaaaaaaaa");
156
+ });
157
+ });
158
+
159
+ describe("promoted entities still become rows", () => {
160
+ test.each([
161
+ EntityType.Service,
162
+ EntityType.Host,
163
+ EntityType.KubernetesPod,
164
+ EntityType.KubernetesNode,
165
+ EntityType.KubernetesCluster,
166
+ EntityType.KubernetesNamespace,
167
+ EntityType.KubernetesDeployment,
168
+ EntityType.ProxmoxCluster,
169
+ EntityType.CephCluster,
170
+ EntityType.DockerSwarmCluster,
171
+ ])("%s is created as a discovered row", async (entityType: EntityType) => {
172
+ await reconcile([entity(entityType)]);
173
+
174
+ expect(createdRows).toHaveLength(1);
175
+ expect(createdRows[0]!.source).toBe(EntitySource.Discovered);
176
+ expect(createdRows[0]!.entityType).toBe(entityType);
177
+ });
178
+ });
@@ -0,0 +1,502 @@
1
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
2
+ import MonitorTemplate from "../../../Models/DatabaseModels/MonitorTemplate";
3
+ import MonitorService from "../../../Server/Services/MonitorService";
4
+ import MonitorTemplateService, {
5
+ SyncLinkedMonitorsResult,
6
+ } from "../../../Server/Services/MonitorTemplateService";
7
+ import FindBy from "../../../Server/Types/Database/FindBy";
8
+ import Query from "../../../Server/Types/Database/Query";
9
+ import Select from "../../../Server/Types/Database/Select";
10
+ import ModelPermission from "../../../Server/Types/Database/Permissions/Index";
11
+ import BadDataException from "../../../Types/Exception/BadDataException";
12
+ import { JSONObject } from "../../../Types/JSON";
13
+ import MonitorType from "../../../Types/Monitor/MonitorType";
14
+ import ObjectID from "../../../Types/ObjectID";
15
+ import { afterEach, describe, expect, it, jest } from "@jest/globals";
16
+ import type { SpyInstance } from "jest-mock";
17
+
18
+ /*
19
+ * Contract under test — pushing a monitor template's CUSTOM FIELD DEFAULTS
20
+ * onto the monitors already made from it (issue #3548).
21
+ *
22
+ * Provisioning has always copied a template's `customFields` onto the monitors
23
+ * it creates, so setting defaults on a template covers everything imported
24
+ * after the edit. The fleet that makes this a problem is on the other side of
25
+ * that line: a thousand devices an auto-import rule already turned into
26
+ * monitors with every custom field empty. Sync is what reaches them, and three
27
+ * things about it are easy to get wrong and impossible to undo:
28
+ *
29
+ * - it OVERLAYS. A template holds a key for every custom field the project
30
+ * has defined the moment its edit form is saved, so an assignment would
31
+ * blank every field the template happens not to default across the whole
32
+ * fleet;
33
+ * - it is per-monitor, because the value written depends on what that
34
+ * monitor already holds — which rules out the bulk `updateBy` every other
35
+ * syncable field rides;
36
+ * - it is opt-in by name. An unscoped sync pushes criteria, interval and
37
+ * labels and deliberately leaves operator-entered custom field values
38
+ * alone.
39
+ */
40
+
41
+ const PROJECT_ID: ObjectID = new ObjectID(
42
+ "11111111-1111-4111-8111-111111111111",
43
+ );
44
+ const TEMPLATE_ID: ObjectID = new ObjectID(
45
+ "22222222-2222-4222-8222-222222222222",
46
+ );
47
+
48
+ function buildTemplate(data?: {
49
+ customFields?: JSONObject | undefined;
50
+ monitorType?: MonitorType | undefined;
51
+ }): MonitorTemplate {
52
+ const template: MonitorTemplate = new MonitorTemplate();
53
+ template.id = TEMPLATE_ID;
54
+ template.projectId = PROJECT_ID;
55
+ /*
56
+ * Ping rather than Network Device, so nothing but custom fields can push a
57
+ * sync onto the per-monitor path.
58
+ */
59
+ template.monitorType = data?.monitorType || MonitorType.Ping;
60
+ template.monitoringInterval = "*/10 * * * *";
61
+ template.minimumProbeAgreement = 2;
62
+ const customFields: JSONObject | undefined =
63
+ data && "customFields" in data
64
+ ? data.customFields
65
+ : {
66
+ Vendor: "Cisco",
67
+ "Configuration Item": "",
68
+ };
69
+ if (customFields) {
70
+ template.customFields = customFields;
71
+ }
72
+ return template;
73
+ }
74
+
75
+ function buildLinkedMonitor(customFields?: JSONObject | undefined): Monitor {
76
+ const monitor: Monitor = new Monitor();
77
+ monitor.id = ObjectID.generate();
78
+ monitor.projectId = PROJECT_ID;
79
+ monitor.monitorType = MonitorType.Ping;
80
+ monitor.monitorTemplateId = TEMPLATE_ID;
81
+ if (customFields) {
82
+ monitor.customFields = customFields;
83
+ }
84
+ return monitor;
85
+ }
86
+
87
+ interface MockedSync {
88
+ updateOneSpy: SpyInstance<typeof MonitorService.updateOneById>;
89
+ bulkUpdateSpy: SpyInstance<typeof MonitorService.updateBy>;
90
+ findBySpy: SpyInstance<typeof MonitorService.findBy>;
91
+ permissionSpy: SpyInstance<
92
+ typeof ModelPermission.checkUpdateQueryPermissions
93
+ >;
94
+ }
95
+
96
+ function mockSync(data: {
97
+ template: MonitorTemplate;
98
+ monitors: Array<Monitor>;
99
+ }): MockedSync {
100
+ jest
101
+ .spyOn(MonitorTemplateService, "findOneById")
102
+ .mockResolvedValue(data.template);
103
+ jest
104
+ .spyOn(MonitorTemplateService, "countLinkedMonitors")
105
+ .mockResolvedValue(data.monitors.length);
106
+
107
+ return {
108
+ updateOneSpy: jest
109
+ .spyOn(MonitorService, "updateOneById")
110
+ .mockResolvedValue(1),
111
+ bulkUpdateSpy: jest.spyOn(MonitorService, "updateBy").mockResolvedValue(0),
112
+ findBySpy: jest
113
+ .spyOn(MonitorService, "findBy")
114
+ .mockResolvedValue(data.monitors),
115
+ permissionSpy: jest
116
+ .spyOn(ModelPermission, "checkUpdateQueryPermissions")
117
+ .mockImplementation(
118
+ async (
119
+ _modelType: any,
120
+ query: Query<Monitor>,
121
+ ): Promise<Query<Monitor>> => {
122
+ return query;
123
+ },
124
+ ) as SpyInstance<typeof ModelPermission.checkUpdateQueryPermissions>,
125
+ };
126
+ }
127
+
128
+ function writtenCustomFields(
129
+ updateOneSpy: SpyInstance<typeof MonitorService.updateOneById>,
130
+ callIndex: number,
131
+ ): JSONObject {
132
+ return updateOneSpy.mock.calls[callIndex]![0].data
133
+ .customFields as unknown as JSONObject;
134
+ }
135
+
136
+ afterEach(() => {
137
+ jest.restoreAllMocks();
138
+ });
139
+
140
+ describe("MonitorTemplateService custom field default synchronization", () => {
141
+ it("is not pushed by an unscoped sync", async () => {
142
+ const template: MonitorTemplate = buildTemplate();
143
+ const monitor: Monitor = buildLinkedMonitor({ Vendor: "Juniper" });
144
+ const mocks: MockedSync = mockSync({ template, monitors: [monitor] });
145
+
146
+ const result: SyncLinkedMonitorsResult =
147
+ await MonitorTemplateService.syncLinkedMonitors({
148
+ monitorTemplateId: TEMPLATE_ID,
149
+ props: { isRoot: true },
150
+ });
151
+
152
+ expect(result).toEqual({ totalLinkedMonitors: 1, syncedMonitors: 0 });
153
+
154
+ /*
155
+ * The bulk path, and the payload it carries has no customFields key at
156
+ * all — not an empty one, which would still clear the column. (`labels` is
157
+ * absent because this template has none, not because it is unsyncable.)
158
+ */
159
+ expect(mocks.updateOneSpy).not.toHaveBeenCalled();
160
+ expect(mocks.bulkUpdateSpy).toHaveBeenCalledTimes(1);
161
+
162
+ const bulkPayloadKeys: Array<string> = Object.keys(
163
+ mocks.bulkUpdateSpy.mock.calls[0]![0].data as unknown as JSONObject,
164
+ );
165
+ expect(bulkPayloadKeys).not.toContain("customFields");
166
+ expect(bulkPayloadKeys).toEqual([
167
+ "monitoringInterval",
168
+ "minimumProbeAgreement",
169
+ ]);
170
+ });
171
+
172
+ it("is accepted as a scoped field, and an unknown field still is not", async () => {
173
+ const template: MonitorTemplate = buildTemplate();
174
+ mockSync({ template, monitors: [buildLinkedMonitor()] });
175
+
176
+ await expect(
177
+ MonitorTemplateService.syncLinkedMonitors({
178
+ monitorTemplateId: TEMPLATE_ID,
179
+ fields: ["customFields"],
180
+ props: { isRoot: true },
181
+ }),
182
+ ).resolves.toEqual({ totalLinkedMonitors: 1, syncedMonitors: 1 });
183
+
184
+ await expect(
185
+ MonitorTemplateService.syncLinkedMonitors({
186
+ monitorTemplateId: TEMPLATE_ID,
187
+ fields: ["name"],
188
+ props: { isRoot: true },
189
+ }),
190
+ ).rejects.toThrow(BadDataException);
191
+ });
192
+
193
+ /*
194
+ * The behaviour the whole feature turns on: only the fields the template
195
+ * actually defaults are written, and every other value on the monitor —
196
+ * including one on a field the template holds an empty string for — survives.
197
+ */
198
+ it("overlays the template's defaults and leaves every other value on the monitor alone", async () => {
199
+ const template: MonitorTemplate = buildTemplate({
200
+ customFields: {
201
+ Vendor: "Cisco",
202
+ Duration: 30,
203
+ "Configuration Item": "",
204
+ Services: [],
205
+ },
206
+ });
207
+ const monitor: Monitor = buildLinkedMonitor({
208
+ Vendor: "Juniper",
209
+ "Configuration Item": "CI-4417",
210
+ Services: ["billing"],
211
+ Notes: "replaced 2026-01-04",
212
+ });
213
+ const mocks: MockedSync = mockSync({ template, monitors: [monitor] });
214
+
215
+ const result: SyncLinkedMonitorsResult =
216
+ await MonitorTemplateService.syncLinkedMonitors({
217
+ monitorTemplateId: TEMPLATE_ID,
218
+ fields: ["customFields"],
219
+ props: { isRoot: true },
220
+ });
221
+
222
+ expect(result).toEqual({ totalLinkedMonitors: 1, syncedMonitors: 1 });
223
+ expect(writtenCustomFields(mocks.updateOneSpy, 0)).toEqual({
224
+ Vendor: "Cisco",
225
+ Duration: 30,
226
+ "Configuration Item": "CI-4417",
227
+ Services: ["billing"],
228
+ Notes: "replaced 2026-01-04",
229
+ });
230
+
231
+ // Scoped to custom fields, so nothing else about the monitor is rewritten.
232
+ expect(
233
+ Object.keys(
234
+ mocks.updateOneSpy.mock.calls[0]![0].data as unknown as JSONObject,
235
+ ),
236
+ ).toEqual(["customFields"]);
237
+ });
238
+
239
+ /*
240
+ * Each monitor's payload is computed from that monitor's own bag, which is
241
+ * exactly why this cannot ride the shared bulk update the other syncable
242
+ * fields use.
243
+ */
244
+ it("writes a different payload per monitor, one row at a time", async () => {
245
+ const template: MonitorTemplate = buildTemplate({
246
+ customFields: { Vendor: "Cisco" },
247
+ });
248
+ const alreadyTagged: Monitor = buildLinkedMonitor({
249
+ Vendor: "Juniper",
250
+ "Configuration Item": "CI-1",
251
+ });
252
+ const untouched: Monitor = buildLinkedMonitor();
253
+ const mocks: MockedSync = mockSync({
254
+ template,
255
+ monitors: [alreadyTagged, untouched],
256
+ });
257
+
258
+ const result: SyncLinkedMonitorsResult =
259
+ await MonitorTemplateService.syncLinkedMonitors({
260
+ monitorTemplateId: TEMPLATE_ID,
261
+ fields: ["customFields"],
262
+ props: { isRoot: true },
263
+ });
264
+
265
+ expect(result).toEqual({ totalLinkedMonitors: 2, syncedMonitors: 2 });
266
+ expect(mocks.bulkUpdateSpy).not.toHaveBeenCalled();
267
+ expect(mocks.updateOneSpy).toHaveBeenCalledTimes(2);
268
+
269
+ expect(writtenCustomFields(mocks.updateOneSpy, 0)).toEqual({
270
+ Vendor: "Cisco",
271
+ "Configuration Item": "CI-1",
272
+ });
273
+ expect(writtenCustomFields(mocks.updateOneSpy, 1)).toEqual({
274
+ Vendor: "Cisco",
275
+ });
276
+ });
277
+
278
+ /*
279
+ * Whatever else it pushes, a per-monitor sync has to READ the column it is
280
+ * about to overlay — a select that omits it hands the merge an undefined bag
281
+ * and turns the overlay into an assignment, silently blanking the fields the
282
+ * template does not default.
283
+ */
284
+ it("reads each monitor's existing custom fields before overlaying them", async () => {
285
+ const template: MonitorTemplate = buildTemplate({
286
+ customFields: { Vendor: "Cisco" },
287
+ });
288
+ const mocks: MockedSync = mockSync({
289
+ template,
290
+ monitors: [buildLinkedMonitor()],
291
+ });
292
+
293
+ await MonitorTemplateService.syncLinkedMonitors({
294
+ monitorTemplateId: TEMPLATE_ID,
295
+ fields: ["customFields"],
296
+ props: { isRoot: true },
297
+ });
298
+
299
+ const select: Select<Monitor> = (
300
+ mocks.findBySpy.mock.calls[0]![0] as FindBy<Monitor>
301
+ ).select as Select<Monitor>;
302
+ expect((select as JSONObject)["customFields"]).toBe(true);
303
+ });
304
+
305
+ /*
306
+ * The read of the template itself, for the same reason. Selecting the column
307
+ * is what makes the difference between "this template defaults nothing" and
308
+ * "nobody asked for the column", and the two are indistinguishable
309
+ * afterwards.
310
+ */
311
+ it("selects customFields on the template it reads", async () => {
312
+ const template: MonitorTemplate = buildTemplate();
313
+ mockSync({ template, monitors: [buildLinkedMonitor()] });
314
+ const templateReadSpy: SpyInstance<
315
+ typeof MonitorTemplateService.findOneById
316
+ > = jest.spyOn(MonitorTemplateService, "findOneById");
317
+
318
+ await MonitorTemplateService.syncLinkedMonitors({
319
+ monitorTemplateId: TEMPLATE_ID,
320
+ fields: ["customFields"],
321
+ props: { isRoot: true },
322
+ });
323
+
324
+ expect(
325
+ (templateReadSpy.mock.calls[0]![0].select as JSONObject)["customFields"],
326
+ ).toBe(true);
327
+ });
328
+
329
+ /*
330
+ * A caller who may not update Monitor.customFields must be refused before
331
+ * the first write, not on whichever row the loop happens to reach when the
332
+ * column check finally runs. The per-monitor values are not known at that
333
+ * point and only the KEYS decide a column check, so the probe names the
334
+ * column with an empty bag.
335
+ */
336
+ it("names customFields in the up-front column permission probe", async () => {
337
+ const template: MonitorTemplate = buildTemplate({
338
+ customFields: { Vendor: "Cisco" },
339
+ });
340
+ const mocks: MockedSync = mockSync({
341
+ template,
342
+ monitors: [buildLinkedMonitor()],
343
+ });
344
+
345
+ await MonitorTemplateService.syncLinkedMonitors({
346
+ monitorTemplateId: TEMPLATE_ID,
347
+ fields: ["customFields"],
348
+ props: { isRoot: true },
349
+ });
350
+
351
+ expect(mocks.permissionSpy).toHaveBeenCalledTimes(1);
352
+ expect(
353
+ Object.keys(mocks.permissionSpy.mock.calls[0]![2] as JSONObject),
354
+ ).toContain("customFields");
355
+ });
356
+
357
+ /*
358
+ * "0 of 200 synced" is the summary the dashboard reads as a permissions
359
+ * problem and tells the operator to run the sync again as somebody else. A
360
+ * template with nothing to push must therefore write nothing and say so,
361
+ * rather than walk the fleet.
362
+ */
363
+ it.each([
364
+ ["an absent bag", undefined],
365
+ ["an empty bag", {}],
366
+ ["a bag holding only blanks", { Vendor: "", Services: [], Duration: null }],
367
+ ])(
368
+ "writes nothing when the template has %s",
369
+ async (
370
+ _label: string,
371
+ customFields: JSONObject | undefined,
372
+ ): Promise<void> => {
373
+ const template: MonitorTemplate = buildTemplate({ customFields });
374
+ const mocks: MockedSync = mockSync({
375
+ template,
376
+ monitors: [buildLinkedMonitor({ Vendor: "Juniper" })],
377
+ });
378
+
379
+ const result: SyncLinkedMonitorsResult =
380
+ await MonitorTemplateService.syncLinkedMonitors({
381
+ monitorTemplateId: TEMPLATE_ID,
382
+ fields: ["customFields"],
383
+ props: { isRoot: true },
384
+ });
385
+
386
+ expect(result).toEqual({ totalLinkedMonitors: 1, syncedMonitors: 0 });
387
+ expect(mocks.updateOneSpy).not.toHaveBeenCalled();
388
+ expect(mocks.bulkUpdateSpy).not.toHaveBeenCalled();
389
+ expect(mocks.findBySpy).not.toHaveBeenCalled();
390
+ },
391
+ );
392
+
393
+ /*
394
+ * Custom fields ride alongside the other per-monitor field rather than
395
+ * displacing it: a Network Device template pushing criteria AND defaults has
396
+ * to rebind the steps to each monitor's own device in the same write.
397
+ */
398
+ it("carries a Network Device criteria rebind and the defaults in one write", async () => {
399
+ const template: MonitorTemplate = buildTemplate({
400
+ monitorType: MonitorType.NetworkDevice,
401
+ customFields: { Vendor: "Cisco" },
402
+ });
403
+ const monitor: Monitor = buildLinkedMonitor({ Duration: 30 });
404
+ const mocks: MockedSync = mockSync({ template, monitors: [monitor] });
405
+
406
+ const result: SyncLinkedMonitorsResult =
407
+ await MonitorTemplateService.syncLinkedMonitors({
408
+ monitorTemplateId: TEMPLATE_ID,
409
+ fields: ["customFields", "monitoringInterval"],
410
+ props: { isRoot: true },
411
+ });
412
+
413
+ expect(result).toEqual({ totalLinkedMonitors: 1, syncedMonitors: 1 });
414
+ expect(mocks.updateOneSpy).toHaveBeenCalledTimes(1);
415
+ expect(
416
+ Object.keys(
417
+ mocks.updateOneSpy.mock.calls[0]![0].data as unknown as JSONObject,
418
+ ).sort(),
419
+ ).toEqual(["customFields", "monitoringInterval"]);
420
+ expect(writtenCustomFields(mocks.updateOneSpy, 0)).toEqual({
421
+ Vendor: "Cisco",
422
+ Duration: 30,
423
+ });
424
+ });
425
+
426
+ describe("syncToMonitor", () => {
427
+ it("overlays the defaults onto that one monitor's values", async () => {
428
+ const template: MonitorTemplate = buildTemplate({
429
+ customFields: { Vendor: "Cisco", "Configuration Item": "" },
430
+ });
431
+ const monitor: Monitor = buildLinkedMonitor({
432
+ Vendor: "Juniper",
433
+ "Configuration Item": "CI-4417",
434
+ });
435
+
436
+ jest
437
+ .spyOn(MonitorTemplateService, "findOneById")
438
+ .mockResolvedValue(template);
439
+ jest.spyOn(MonitorService, "findOneById").mockResolvedValue(monitor);
440
+ const updateOneSpy: SpyInstance<typeof MonitorService.updateOneById> =
441
+ jest.spyOn(MonitorService, "updateOneById").mockResolvedValue(1);
442
+
443
+ await MonitorTemplateService.syncToMonitor({
444
+ monitorTemplateId: TEMPLATE_ID,
445
+ monitorId: monitor.id!,
446
+ fields: ["customFields"],
447
+ props: { isRoot: true },
448
+ });
449
+
450
+ expect(writtenCustomFields(updateOneSpy, 0)).toEqual({
451
+ Vendor: "Cisco",
452
+ "Configuration Item": "CI-4417",
453
+ });
454
+ });
455
+
456
+ it("reads the monitor's existing custom fields before overlaying them", async () => {
457
+ const template: MonitorTemplate = buildTemplate({
458
+ customFields: { Vendor: "Cisco" },
459
+ });
460
+ const monitor: Monitor = buildLinkedMonitor();
461
+
462
+ jest
463
+ .spyOn(MonitorTemplateService, "findOneById")
464
+ .mockResolvedValue(template);
465
+ const monitorReadSpy: SpyInstance<typeof MonitorService.findOneById> =
466
+ jest.spyOn(MonitorService, "findOneById").mockResolvedValue(monitor);
467
+ jest.spyOn(MonitorService, "updateOneById").mockResolvedValue(1);
468
+
469
+ await MonitorTemplateService.syncToMonitor({
470
+ monitorTemplateId: TEMPLATE_ID,
471
+ monitorId: monitor.id!,
472
+ fields: ["customFields"],
473
+ props: { isRoot: true },
474
+ });
475
+
476
+ expect(
477
+ (monitorReadSpy.mock.calls[0]![0].select as JSONObject)["customFields"],
478
+ ).toBe(true);
479
+ });
480
+
481
+ it("writes nothing when the template defaults nothing", async () => {
482
+ const template: MonitorTemplate = buildTemplate({ customFields: {} });
483
+ const monitor: Monitor = buildLinkedMonitor({ Vendor: "Juniper" });
484
+
485
+ jest
486
+ .spyOn(MonitorTemplateService, "findOneById")
487
+ .mockResolvedValue(template);
488
+ jest.spyOn(MonitorService, "findOneById").mockResolvedValue(monitor);
489
+ const updateOneSpy: SpyInstance<typeof MonitorService.updateOneById> =
490
+ jest.spyOn(MonitorService, "updateOneById").mockResolvedValue(1);
491
+
492
+ await MonitorTemplateService.syncToMonitor({
493
+ monitorTemplateId: TEMPLATE_ID,
494
+ monitorId: monitor.id!,
495
+ fields: ["customFields"],
496
+ props: { isRoot: true },
497
+ });
498
+
499
+ expect(updateOneSpy).not.toHaveBeenCalled();
500
+ });
501
+ });
502
+ });