@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,169 @@
1
+ import Monitor from "../../../../Models/DatabaseModels/Monitor";
2
+ import QueryUtil from "../../../../Server/Types/Database/QueryUtil";
3
+ import IncludesAnyOfGroups from "../../../../Types/BaseDatabase/IncludesAnyOfGroups";
4
+ import Includes from "../../../../Types/BaseDatabase/Includes";
5
+ import BadDataException from "../../../../Types/Exception/BadDataException";
6
+ import ObjectID from "../../../../Types/ObjectID";
7
+ import { FindOperator, In } from "typeorm";
8
+
9
+ const relation: {
10
+ joinTableName: string;
11
+ ownerColumnName: string;
12
+ relationColumnName: string;
13
+ } = {
14
+ joinTableName: "MonitorLabel",
15
+ ownerColumnName: "monitorId",
16
+ relationColumnName: "labelId",
17
+ };
18
+
19
+ describe("QueryUtil — grouped relation membership", () => {
20
+ beforeEach(() => {
21
+ jest
22
+ .spyOn(QueryUtil, "getManyToManyRelationMetadata")
23
+ .mockReturnValue(relation);
24
+ });
25
+
26
+ afterEach(() => {
27
+ jest.restoreAllMocks();
28
+ });
29
+
30
+ test("ANDs independent ANY groups without joining the relation into the row set", () => {
31
+ const result: Record<string, any> = QueryUtil.serializeQuery(Monitor, {
32
+ labels: new IncludesAnyOfGroups([
33
+ ["network", "router"],
34
+ ["0660", "0661"],
35
+ ]),
36
+ });
37
+ expect(result["labels"]).toBeUndefined();
38
+ const filter: FindOperator<FindOperator<unknown>[]> = result["_id"];
39
+ expect(filter.type).toBe("and");
40
+ expect(filter.value).toHaveLength(2);
41
+ expect(
42
+ filter.value.map((child: FindOperator<unknown>) => {
43
+ return Object.values(child.objectLiteralParameters || {});
44
+ }),
45
+ ).toEqual([[["network", "router"]], [["0660", "0661"]]]);
46
+ for (const child of filter.value) {
47
+ expect(child.getSql?.("Monitor._id")).toContain(
48
+ 'Monitor._id IN (SELECT "MonitorLabel"."monitorId"',
49
+ );
50
+ }
51
+ const parameterNames: Array<string> = filter.value.flatMap(
52
+ (child: FindOperator<unknown>) => {
53
+ return Object.keys(child.objectLiteralParameters || {});
54
+ },
55
+ );
56
+ expect(new Set(parameterNames).size).toBe(2);
57
+ });
58
+
59
+ test("an empty group contributes a false condition", () => {
60
+ const result: Record<string, any> = QueryUtil.serializeQuery(Monitor, {
61
+ labels: new IncludesAnyOfGroups([["network"], []]),
62
+ });
63
+ expect(result["_id"].value[1].getSql("Monitor._id")).toBe("TRUE = FALSE");
64
+ });
65
+
66
+ test.each(["before", "after"])(
67
+ "preserves an ObjectID filter placed %s the relation key",
68
+ (position: string) => {
69
+ const id: ObjectID = ObjectID.generate();
70
+ const labels: IncludesAnyOfGroups = new IncludesAnyOfGroups([
71
+ ["network"],
72
+ ["0660"],
73
+ ]);
74
+ const query: Record<string, unknown> =
75
+ position === "before" ? { _id: id, labels } : { labels, _id: id };
76
+ const result: Record<string, any> = QueryUtil.serializeQuery(
77
+ Monitor,
78
+ query as any,
79
+ );
80
+ expect(result["_id"].type).toBe("and");
81
+ expect(result["_id"].value).toHaveLength(3);
82
+ expect(
83
+ Object.values(result["_id"].value[0].objectLiteralParameters),
84
+ ).toEqual([id.toString()]);
85
+ },
86
+ );
87
+
88
+ test.each(["before", "after"])(
89
+ "preserves an Includes id filter placed %s the relation key",
90
+ (position: string) => {
91
+ const ids: Array<string> = [
92
+ ObjectID.generate().toString(),
93
+ ObjectID.generate().toString(),
94
+ ];
95
+ const labels: IncludesAnyOfGroups = new IncludesAnyOfGroups([
96
+ ["network"],
97
+ ["0660"],
98
+ ]);
99
+ const query: Record<string, unknown> =
100
+ position === "before"
101
+ ? { _id: new Includes(ids), labels }
102
+ : { labels, _id: new Includes(ids) };
103
+ const result: Record<string, any> = QueryUtil.serializeQuery(
104
+ Monitor,
105
+ query as any,
106
+ );
107
+ expect(result["_id"].value[0].type).toBe("raw");
108
+ expect(
109
+ Object.values(result["_id"].value[0].objectLiteralParameters),
110
+ ).toEqual([ids]);
111
+ },
112
+ );
113
+
114
+ test("preserves existing string and TypeORM id conditions", () => {
115
+ const id: string = ObjectID.generate().toString();
116
+ const stringResult: Record<string, any> = QueryUtil.serializeQuery(
117
+ Monitor,
118
+ {
119
+ _id: id,
120
+ labels: new IncludesAnyOfGroups([["network"], ["0660"]]),
121
+ },
122
+ );
123
+ expect(
124
+ Object.values(stringResult["_id"].value[0].objectLiteralParameters),
125
+ ).toEqual([id]);
126
+ const idFilter: FindOperator<string> = In([id]);
127
+ const operatorResult: Record<string, any> = QueryUtil.serializeQuery(
128
+ Monitor,
129
+ {
130
+ _id: idFilter as any,
131
+ labels: new IncludesAnyOfGroups([["network"], ["0660"]]),
132
+ },
133
+ );
134
+ expect(operatorResult["_id"].value[0]).toBe(idFilter);
135
+ });
136
+
137
+ test("retains the project and other widget conditions", () => {
138
+ const projectId: ObjectID = ObjectID.generate();
139
+ const result: Record<string, any> = QueryUtil.serializeQuery(Monitor, {
140
+ projectId,
141
+ disableActiveMonitoring: false,
142
+ labels: new IncludesAnyOfGroups([["network"], ["0660"]]),
143
+ });
144
+ expect(Object.values(result["projectId"].objectLiteralParameters)).toEqual([
145
+ projectId.toString(),
146
+ ]);
147
+ expect(result["disableActiveMonitoring"]).toBe(false);
148
+ });
149
+
150
+ test.each(["name", "customFields", "project", "missingColumn"])(
151
+ "rejects the operator on the unsupported %s column",
152
+ (column: string) => {
153
+ expect(() => {
154
+ return QueryUtil.serializeQuery(Monitor, {
155
+ [column]: new IncludesAnyOfGroups([["network"]]),
156
+ });
157
+ }).toThrow(BadDataException);
158
+ },
159
+ );
160
+
161
+ test("fails clearly when the many-to-many relation cannot be resolved", () => {
162
+ jest.mocked(QueryUtil.getManyToManyRelationMetadata).mockReturnValue(null);
163
+ expect(() => {
164
+ return QueryUtil.serializeQuery(Monitor, {
165
+ labels: new IncludesAnyOfGroups([["network"], ["0660"]]),
166
+ });
167
+ }).toThrow("available many-to-many relation metadata");
168
+ });
169
+ });
@@ -0,0 +1,297 @@
1
+ import Entities from "../../../../Models/DatabaseModels/Index";
2
+ import Monitor from "../../../../Models/DatabaseModels/Monitor";
3
+ import PostgresAppInstance from "../../../../Server/Infrastructure/PostgresDatabase";
4
+ import Query from "../../../../Server/Types/Database/Query";
5
+ import QueryUtil from "../../../../Server/Types/Database/QueryUtil";
6
+ import Includes from "../../../../Types/BaseDatabase/Includes";
7
+ import IncludesAnyOfGroups from "../../../../Types/BaseDatabase/IncludesAnyOfGroups";
8
+ import { JSONObject } from "../../../../Types/JSON";
9
+ import JSONFunctions from "../../../../Types/JSONFunctions";
10
+ import ObjectID from "../../../../Types/ObjectID";
11
+ import { DataSource, FindOptionsWhere } from "typeorm";
12
+
13
+ /*
14
+ * Run with RUN_POSTGRES_DASHBOARD_LABEL_TESTS=true and config.env loaded.
15
+ * All rows and cloned production tables live in a unique temporary schema.
16
+ * Uses the local development database on port 5400 unless overridden below.
17
+ */
18
+ const describePostgres: typeof describe =
19
+ process.env["RUN_POSTGRES_DASHBOARD_LABEL_TESTS"] === "true"
20
+ ? describe
21
+ : describe.skip;
22
+
23
+ describePostgres("dashboard label groups against Postgres", () => {
24
+ const schema: string = `dashboard_labels_${ObjectID.generate().toString().replace(/-/g, "")}`;
25
+ const projectId: string = ObjectID.generate().toString();
26
+ const otherProjectId: string = ObjectID.generate().toString();
27
+ const statusId: string = ObjectID.generate().toString();
28
+ const network: string = ObjectID.generate().toString();
29
+ const router: string = ObjectID.generate().toString();
30
+ const unit0660: string = ObjectID.generate().toString();
31
+ const unit0661: string = ObjectID.generate().toString();
32
+ const monitorIds: Record<string, string> = {};
33
+ let database: DataSource;
34
+
35
+ beforeAll(async () => {
36
+ database = new DataSource({
37
+ type: "postgres",
38
+ host: process.env["DASHBOARD_LABEL_TEST_DATABASE_HOST"] || "localhost",
39
+ port: Number(process.env["DASHBOARD_LABEL_TEST_DATABASE_PORT"] || "5400"),
40
+ username: process.env["DATABASE_USERNAME"] || "postgres",
41
+ password: process.env["DATABASE_PASSWORD"] || "password",
42
+ database: process.env["DATABASE_NAME"] || "oneuptimedb",
43
+ entities: Entities,
44
+ schema,
45
+ synchronize: false,
46
+ extra: { options: `-c search_path=${schema},public` },
47
+ });
48
+ await database.initialize();
49
+ await database.query(`CREATE SCHEMA "${schema}"`);
50
+ for (const table of ["Monitor", "Label", "MonitorLabel"]) {
51
+ await database.query(
52
+ `CREATE TABLE "${schema}"."${table}" (LIKE public."${table}" INCLUDING ALL)`,
53
+ );
54
+ }
55
+ /*
56
+ * LIKE does not copy foreign keys. Retain the production relation's
57
+ * cascade behavior so deleting a label exercises the real join lifecycle.
58
+ */
59
+ await database.query(
60
+ `ALTER TABLE "${schema}"."MonitorLabel" ADD FOREIGN KEY ("labelId") REFERENCES "${schema}"."Label" ("_id") ON UPDATE CASCADE ON DELETE CASCADE`,
61
+ );
62
+ await database.query(
63
+ `ALTER TABLE "${schema}"."MonitorLabel" ADD FOREIGN KEY ("monitorId") REFERENCES "${schema}"."Monitor" ("_id") ON UPDATE CASCADE ON DELETE CASCADE`,
64
+ );
65
+ expect(
66
+ (await database.query("SELECT current_schema()"))[0].current_schema,
67
+ ).toBe(schema);
68
+ jest.spyOn(PostgresAppInstance, "isConnected").mockReturnValue(true);
69
+ jest.spyOn(PostgresAppInstance, "getDataSource").mockReturnValue(database);
70
+
71
+ for (const [id, name] of [
72
+ [network, "network"],
73
+ [router, "router"],
74
+ [unit0660, "0660"],
75
+ [unit0661, "0661"],
76
+ ]) {
77
+ await database.query(
78
+ `INSERT INTO "Label" ("_id", "version", "projectId", "name", "slug", "color") VALUES ($1, 1, $2, $3, $3, '#000000')`,
79
+ [id, projectId, name],
80
+ );
81
+ }
82
+ await seedMonitor("network-0660", [network, unit0660]);
83
+ await seedMonitor("network-0661", [network, unit0661]);
84
+ await seedMonitor("router-0661", [router, unit0661]);
85
+ await seedMonitor("both-units", [network, router, unit0660, unit0661]);
86
+ await seedMonitor("network-no-unit", [network]);
87
+ await seedMonitor("unit-only", [unit0660]);
88
+ await seedMonitor("no-labels", []);
89
+ await seedMonitor("other-project", [network, unit0660], otherProjectId);
90
+ });
91
+
92
+ afterAll(async () => {
93
+ jest.restoreAllMocks();
94
+ if (database?.isInitialized) {
95
+ await database.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
96
+ await database.destroy();
97
+ }
98
+ });
99
+
100
+ async function seedMonitor(
101
+ name: string,
102
+ labels: Array<string>,
103
+ monitorProjectId: string = projectId,
104
+ ): Promise<void> {
105
+ const id: string = ObjectID.generate().toString();
106
+ monitorIds[name] = id;
107
+ await database.query(
108
+ `INSERT INTO "Monitor" ("_id", "version", "projectId", "name", "slug", "monitorType", "currentMonitorStatusId") VALUES ($1, 1, $2, $3, $3, 'Manual', $4)`,
109
+ [id, monitorProjectId, name, statusId],
110
+ );
111
+ for (const labelId of labels) {
112
+ await database.query(
113
+ `INSERT INTO "MonitorLabel" ("monitorId", "labelId") VALUES ($1, $2)`,
114
+ [id, labelId],
115
+ );
116
+ }
117
+ }
118
+
119
+ async function findNames(query: Query<Monitor>): Promise<Array<string>> {
120
+ const serialized: Query<Monitor> = QueryUtil.serializeQuery(Monitor, {
121
+ projectId: new ObjectID(projectId),
122
+ ...query,
123
+ });
124
+ const monitors: Array<Monitor> = await database
125
+ .getRepository(Monitor)
126
+ .find({
127
+ select: { _id: true, name: true },
128
+ where: serialized as unknown as FindOptionsWhere<Monitor>,
129
+ order: { name: "ASC" },
130
+ });
131
+ return monitors.map((monitor: Monitor) => {
132
+ return monitor.name!;
133
+ });
134
+ }
135
+
136
+ test("requires a category label and the selected unit on the same monitor", async () => {
137
+ expect(
138
+ await findNames({
139
+ labels: new IncludesAnyOfGroups([[network, router], [unit0660]]),
140
+ }),
141
+ ).toEqual(["both-units", "network-0660"]);
142
+ });
143
+
144
+ test("switching the selected unit changes the actual matching rows", async () => {
145
+ expect(
146
+ await findNames({
147
+ labels: new IncludesAnyOfGroups([[network, router], [unit0661]]),
148
+ }),
149
+ ).toEqual(["both-units", "network-0661", "router-0661"]);
150
+ });
151
+
152
+ test("matches any selected unit without duplicating monitors carrying both", async () => {
153
+ expect(
154
+ await findNames({
155
+ labels: new IncludesAnyOfGroups([[network], [unit0660, unit0661]]),
156
+ }),
157
+ ).toEqual(["both-units", "network-0660", "network-0661"]);
158
+ });
159
+
160
+ test("three independent conditions remain conjunctive", async () => {
161
+ expect(
162
+ await findNames({
163
+ labels: new IncludesAnyOfGroups([
164
+ [network],
165
+ [router],
166
+ [unit0660, unit0661],
167
+ ]),
168
+ }),
169
+ ).toEqual(["both-units"]);
170
+ });
171
+
172
+ test("All can omit just the unit condition while retaining the fixed labels", async () => {
173
+ expect(
174
+ await findNames({ labels: new Includes([network, router]) }),
175
+ ).toEqual([
176
+ "both-units",
177
+ "network-0660",
178
+ "network-0661",
179
+ "network-no-unit",
180
+ "router-0661",
181
+ ]);
182
+ });
183
+
184
+ test("empty and missing-label groups return no monitors", async () => {
185
+ expect(
186
+ await findNames({ labels: new IncludesAnyOfGroups([[network], []]) }),
187
+ ).toEqual([]);
188
+ expect(
189
+ await findNames({
190
+ labels: new IncludesAnyOfGroups([
191
+ [network],
192
+ [ObjectID.generate().toString()],
193
+ ]),
194
+ }),
195
+ ).toEqual([]);
196
+ });
197
+
198
+ test.each(["before", "after"])(
199
+ "retains the explicit monitor id %s labels",
200
+ async (position: string) => {
201
+ const id: ObjectID = new ObjectID(monitorIds["network-0660"]!);
202
+ const labels: IncludesAnyOfGroups = new IncludesAnyOfGroups([
203
+ [network],
204
+ [unit0660],
205
+ ]);
206
+ expect(
207
+ await findNames(
208
+ position === "before" ? { _id: id, labels } : { labels, _id: id },
209
+ ),
210
+ ).toEqual(["network-0660"]);
211
+ expect(
212
+ await findNames({
213
+ labels: new IncludesAnyOfGroups([[network], [unit0661]]),
214
+ _id: id,
215
+ }),
216
+ ).toEqual([]);
217
+ },
218
+ );
219
+
220
+ test("retains other widget filters", async () => {
221
+ expect(
222
+ await findNames({
223
+ currentMonitorStatusId: new ObjectID(ObjectID.generate().toString()),
224
+ labels: new IncludesAnyOfGroups([[network], [unit0660]]),
225
+ }),
226
+ ).toEqual([]);
227
+ });
228
+
229
+ test("API serialization keeps the same database behavior", async () => {
230
+ const payload: JSONObject = JSON.parse(
231
+ JSON.stringify(
232
+ JSONFunctions.serialize({
233
+ labels: new IncludesAnyOfGroups([[network, router], [unit0661]]),
234
+ }),
235
+ ),
236
+ );
237
+ expect(
238
+ await findNames(JSONFunctions.deserialize(payload) as Query<Monitor>),
239
+ ).toEqual(["both-units", "network-0661", "router-0661"]);
240
+ });
241
+
242
+ test("count and pagination operate on unique monitors", async () => {
243
+ const where: FindOptionsWhere<Monitor> = QueryUtil.serializeQuery(Monitor, {
244
+ projectId: new ObjectID(projectId),
245
+ labels: new IncludesAnyOfGroups([
246
+ [network, router],
247
+ [unit0660, unit0661],
248
+ ]),
249
+ }) as unknown as FindOptionsWhere<Monitor>;
250
+ const [rows, count]: [Array<Monitor>, number] = await database
251
+ .getRepository(Monitor)
252
+ .findAndCount({
253
+ where,
254
+ select: { _id: true, name: true },
255
+ order: { name: "ASC" },
256
+ skip: 1,
257
+ take: 2,
258
+ });
259
+ expect(count).toBe(4);
260
+ expect(
261
+ rows.map((monitor: Monitor) => {
262
+ return monitor.name;
263
+ }),
264
+ ).toEqual(["network-0660", "network-0661"]);
265
+ });
266
+
267
+ test("a deleted selected label produces no results while retaining the fixed condition", async () => {
268
+ const deletedLabel: string = ObjectID.generate().toString();
269
+ await database.query(
270
+ `INSERT INTO "Label" ("_id", "version", "projectId", "name", "slug", "color") VALUES ($1, 1, $2, 'removed-unit', 'removed-unit', '#000000')`,
271
+ [deletedLabel, projectId],
272
+ );
273
+ await seedMonitor("deleted-label-monitor", [network, deletedLabel]);
274
+ try {
275
+ expect(
276
+ await findNames({
277
+ labels: new IncludesAnyOfGroups([[network], [deletedLabel]]),
278
+ }),
279
+ ).toEqual(["deleted-label-monitor"]);
280
+ await database.query(`DELETE FROM "Label" WHERE "_id" = $1`, [
281
+ deletedLabel,
282
+ ]);
283
+ expect(
284
+ await findNames({
285
+ labels: new IncludesAnyOfGroups([[network], [deletedLabel]]),
286
+ }),
287
+ ).toEqual([]);
288
+ } finally {
289
+ await database.query(`DELETE FROM "Monitor" WHERE "_id" = $1`, [
290
+ monitorIds["deleted-label-monitor"],
291
+ ]);
292
+ await database.query(`DELETE FROM "Label" WHERE "_id" = $1`, [
293
+ deletedLabel,
294
+ ]);
295
+ }
296
+ });
297
+ });
@@ -1,4 +1,4 @@
1
- import Markdown from "../../../Server/Types/Markdown";
1
+ import Markdown, { MarkdownContentType } from "../../../Server/Types/Markdown";
2
2
 
3
3
  /*
4
4
  * slugify turns a heading into the `id` the docs renderer emits, and therefore
@@ -104,3 +104,196 @@ describe("Markdown.slugify", () => {
104
104
  });
105
105
  });
106
106
  });
107
+
108
+ /*
109
+ * THE EMAIL RENDERER.
110
+ *
111
+ * Until this suite existed there was no coverage of convertToHTML at all,
112
+ * for any content type — and the email renderer was a bare `new Renderer()`,
113
+ * so marked's stock output went straight into the message. That mattered
114
+ * most for the alert and incident root cause, which carries a
115
+ * GitHub-flavoured table of breaching samples: it arrived as a naked
116
+ * <table> with no borders and no padding, every row running into the next.
117
+ *
118
+ * Two properties are load-bearing and easy to break:
119
+ *
120
+ * - the styling must be INLINE. MailService compiles the Handlebars
121
+ * template and hands the string to nodemailer with no CSS-inlining step
122
+ * anywhere in the pipeline, and Gmail strips <style> from the head, so a
123
+ * class name reaches the reader as nothing at all.
124
+ * - raw HTML a user typed must still be escaped. convertToHTML overrides
125
+ * `renderer.html` for exactly that, on a renderer every email shares.
126
+ */
127
+ describe("Markdown.convertToHTML - email renderer", () => {
128
+ const TABLE_MARKDOWN: string = [
129
+ "| Timestamp | Value |",
130
+ "| --- | --- |",
131
+ "| `2026-08-14T10:30:00.000Z` | 1.07 GB |",
132
+ ].join("\n");
133
+
134
+ test("renders a GFM table with inline styles, not class names", async () => {
135
+ const html: string = await Markdown.convertToHTML(
136
+ TABLE_MARKDOWN,
137
+ MarkdownContentType.Email,
138
+ );
139
+
140
+ expect(html).toContain("<table");
141
+ expect(html).toContain("<th");
142
+ expect(html).toContain("<td");
143
+ expect(html).toContain("border-collapse:collapse");
144
+ expect(html).toContain("padding:8px 10px");
145
+
146
+ /*
147
+ * A class would be dead weight: nothing inlines CSS before send and
148
+ * Gmail drops the <style> block Style.hbs emits.
149
+ */
150
+ expect(html).not.toContain('class="');
151
+ });
152
+
153
+ test("the table carries the legacy attributes Outlook honours over CSS", async () => {
154
+ const html: string = await Markdown.convertToHTML(
155
+ TABLE_MARKDOWN,
156
+ MarkdownContentType.Email,
157
+ );
158
+
159
+ expect(html).toContain('cellpadding="0"');
160
+ expect(html).toContain('cellspacing="0"');
161
+ expect(html).toContain('border="0"');
162
+ expect(html).toContain('width="100%"');
163
+ });
164
+
165
+ /*
166
+ * The DetailBox card leaves roughly 416px of usable width and a
167
+ * root-cause table routinely runs to four or more columns, so a long
168
+ * metric name has to wrap inside its column rather than push the table
169
+ * past the body width. Email clients ignore overflow, so there is no
170
+ * scrollbar to fall back on.
171
+ */
172
+ test("cells wrap long words rather than overflowing the card", async () => {
173
+ const html: string = await Markdown.convertToHTML(
174
+ TABLE_MARKDOWN,
175
+ MarkdownContentType.Email,
176
+ );
177
+
178
+ expect(html).toContain("word-break:break-word");
179
+ expect(html).not.toContain("overflow-x");
180
+ });
181
+
182
+ test("header cells are distinguishable from body cells", async () => {
183
+ const html: string = await Markdown.convertToHTML(
184
+ TABLE_MARKDOWN,
185
+ MarkdownContentType.Email,
186
+ );
187
+
188
+ const header: string = html.slice(
189
+ html.indexOf("<thead>"),
190
+ html.indexOf("</thead>"),
191
+ );
192
+ const body: string = html.slice(html.indexOf("<tbody>"));
193
+
194
+ expect(header).toContain("font-weight:600");
195
+ expect(header).toContain("background-color:#f8fafc");
196
+ expect(body).not.toContain("background-color:#f8fafc");
197
+ });
198
+
199
+ test("column alignment from the markdown survives", async () => {
200
+ const html: string = await Markdown.convertToHTML(
201
+ ["| A | B |", "| ---: | :--- |", "| 1 | 2 |"].join("\n"),
202
+ MarkdownContentType.Email,
203
+ );
204
+
205
+ expect(html).toContain("text-align:right");
206
+ expect(html).toContain("text-align:left");
207
+ });
208
+
209
+ // The root cause backticks every metric name, alias and timestamp.
210
+ test("inline code is styled and its content preserved", async () => {
211
+ const html: string = await Markdown.convertToHTML(
212
+ "Metric `k8s.pod.memory.usage` breached",
213
+ MarkdownContentType.Email,
214
+ );
215
+
216
+ expect(html).toContain("<code");
217
+ expect(html).toContain("background-color:#f1f5f9");
218
+ expect(html).toContain("k8s.pod.memory.usage");
219
+ });
220
+
221
+ test("code content is escaped rather than left live", async () => {
222
+ const html: string = await Markdown.convertToHTML(
223
+ "Metric `<img src=x onerror=alert(1)>` breached",
224
+ MarkdownContentType.Email,
225
+ );
226
+
227
+ expect(html).not.toContain("<img src=x");
228
+ expect(html).toContain("&lt;img");
229
+ });
230
+
231
+ /*
232
+ * The reason the DetailBoxField contract permits this output on a raw
233
+ * slot at all: convertToHTML escapes the raw-HTML tokens a user typed
234
+ * while letting the renderer's own tags through.
235
+ */
236
+ test("raw HTML typed into the markdown source is escaped", async () => {
237
+ const html: string = await Markdown.convertToHTML(
238
+ 'Hello <img src=x onerror="alert(1)"> world',
239
+ MarkdownContentType.Email,
240
+ );
241
+
242
+ expect(html).not.toContain('onerror="alert(1)"');
243
+ expect(html).toContain("&lt;img");
244
+ });
245
+
246
+ test("bold and links still render, so the rest of the email is unchanged", async () => {
247
+ const html: string = await Markdown.convertToHTML(
248
+ "**Filter Conditions Met**: see [the dashboard](https://example.com)",
249
+ MarkdownContentType.Email,
250
+ );
251
+
252
+ expect(html).toContain("<strong>Filter Conditions Met</strong>");
253
+ expect(html).toContain('href="https://example.com"');
254
+ });
255
+
256
+ /*
257
+ * The renderer is memoised in a static field and convertToHTML reassigns
258
+ * `renderer.html` on it for every call, so any per-call state would leak
259
+ * between emails. Rendering twice must be identical.
260
+ */
261
+ test("repeated renders are identical", async () => {
262
+ const first: string = await Markdown.convertToHTML(
263
+ TABLE_MARKDOWN,
264
+ MarkdownContentType.Email,
265
+ );
266
+ const second: string = await Markdown.convertToHTML(
267
+ TABLE_MARKDOWN,
268
+ MarkdownContentType.Email,
269
+ );
270
+
271
+ expect(second).toBe(first);
272
+ });
273
+
274
+ test("an empty root cause does not throw", async () => {
275
+ await expect(
276
+ Markdown.convertToHTML("", MarkdownContentType.Email),
277
+ ).resolves.toBe("");
278
+ });
279
+ });
280
+
281
+ describe("Markdown.escapeHtml", () => {
282
+ /*
283
+ * Public so that code hand-building an email fragment by string
284
+ * interpolation escapes the same way marked's own path does.
285
+ */
286
+ test("escapes the five characters that break out of HTML", () => {
287
+ expect(Markdown.escapeHtml(`<a href="x" title='y'>&</a>`)).toBe(
288
+ "&lt;a href=&quot;x&quot; title=&#39;y&#39;&gt;&amp;&lt;/a&gt;",
289
+ );
290
+ });
291
+
292
+ test("escapes the ampersand first, so entities are not double-encoded wrongly", () => {
293
+ expect(Markdown.escapeHtml("&lt;")).toBe("&amp;lt;");
294
+ });
295
+
296
+ test("leaves ordinary text alone", () => {
297
+ expect(Markdown.escapeHtml("web-7d9f / prod")).toBe("web-7d9f / prod");
298
+ });
299
+ });