@oneuptime/common 12.0.27 → 12.0.29

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 (204) hide show
  1. package/Models/AnalyticsModels/RumSessionChunk.ts +52 -0
  2. package/Models/DatabaseModels/CloudResource.ts +17 -1
  3. package/Models/DatabaseModels/DashboardDomain.ts +43 -0
  4. package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
  5. package/Models/DatabaseModels/RumApplication.ts +35 -2
  6. package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
  7. package/Server/API/DashboardDomainAPI.ts +74 -0
  8. package/Server/API/MicrosoftTeamsAPI.ts +81 -180
  9. package/Server/API/StatusPageDomainAPI.ts +74 -0
  10. package/Server/API/TelemetryAPI.ts +104 -15
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  14. package/Server/Middleware/ProjectAuthorization.ts +6 -1
  15. package/Server/Services/DashboardDomainService.ts +131 -0
  16. package/Server/Services/LogPipelineProcessorService.ts +88 -0
  17. package/Server/Services/MonitorTemplateService.ts +70 -11
  18. package/Server/Services/StatusPageDomainService.ts +163 -3
  19. package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
  20. package/Server/Types/Domain.ts +17 -0
  21. package/Server/Utils/APIKey/AccessPermission.ts +24 -0
  22. package/Server/Utils/Greenlock/Greenlock.ts +156 -58
  23. package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
  24. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
  25. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
  26. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +12 -5
  27. package/Server/Utils/SessionReplay/SessionReplayIdentity.ts +102 -0
  28. package/Server/Utils/SessionReplay/SessionReplayReadService.ts +11 -2
  29. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
  30. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
  31. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
  32. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
  33. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
  34. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
  35. package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
  36. package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
  37. package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
  38. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +81 -0
  39. package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
  40. package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
  41. package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
  42. package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
  43. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
  44. package/Tests/Server/API/SessionReplayAPI.test.ts +232 -0
  45. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
  46. package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
  47. package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
  48. package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
  49. package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
  50. package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
  51. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
  52. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
  53. package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
  54. package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
  55. package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
  56. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
  57. package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
  58. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
  59. package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
  60. package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
  61. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
  62. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
  63. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
  64. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
  65. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
  66. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
  67. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
  68. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
  69. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
  70. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
  71. package/Tests/Types/API/HTTPResponse.test.ts +175 -0
  72. package/Tests/Types/API/Route.test.ts +36 -4
  73. package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
  74. package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
  75. package/Tests/Types/DateLocalTimeString.test.ts +259 -0
  76. package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
  77. package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
  78. package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
  79. package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
  80. package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
  81. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
  82. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
  83. package/Tests/UI/Utils/Navigation.test.ts +82 -0
  84. package/Tests/Utils/Array.test.ts +100 -0
  85. package/Tests/Utils/CertificateReissue.test.ts +225 -0
  86. package/Tests/Utils/Grok/Grok.test.ts +492 -0
  87. package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
  88. package/Types/API/Route.ts +20 -5
  89. package/Types/Date.ts +143 -13
  90. package/Types/Rum/SessionReplay.ts +76 -1
  91. package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
  92. package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
  93. package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
  94. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
  95. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
  96. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
  97. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
  98. package/UI/Utils/Navigation.ts +42 -0
  99. package/Utils/Array.ts +37 -0
  100. package/Utils/CertificateReissue.ts +128 -0
  101. package/Utils/Grok/Grok.ts +461 -0
  102. package/Utils/Grok/GrokPatterns.ts +118 -0
  103. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
  104. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +48 -0
  105. package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
  106. package/build/dist/Models/DatabaseModels/CloudResource.js +19 -2
  107. package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
  109. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  110. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
  111. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/RumApplication.js +37 -3
  113. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  114. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
  115. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  116. package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
  117. package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
  118. package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
  119. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  120. package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
  121. package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
  122. package/build/dist/Server/API/TelemetryAPI.js +67 -14
  123. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  129. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  130. package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
  131. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  132. package/build/dist/Server/Services/DashboardDomainService.js +104 -0
  133. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  134. package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
  135. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
  136. package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
  137. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  138. package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
  139. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  140. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
  141. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
  142. package/build/dist/Server/Types/Domain.js +16 -0
  143. package/build/dist/Server/Types/Domain.js.map +1 -1
  144. package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
  145. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  146. package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
  147. package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
  148. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
  149. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
  150. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
  151. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  152. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
  153. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  154. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  155. package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js +59 -0
  156. package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js.map +1 -0
  157. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +9 -2
  158. package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
  159. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
  160. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  161. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
  162. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  163. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
  164. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
  165. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
  166. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  167. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
  168. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  169. package/build/dist/Types/API/Route.js +16 -5
  170. package/build/dist/Types/API/Route.js.map +1 -1
  171. package/build/dist/Types/Date.js +115 -13
  172. package/build/dist/Types/Date.js.map +1 -1
  173. package/build/dist/Types/Rum/SessionReplay.js +38 -0
  174. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  175. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
  176. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
  177. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
  178. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  179. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
  180. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
  181. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
  182. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  183. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
  184. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  185. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
  186. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  187. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
  188. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
  189. package/build/dist/UI/Utils/Navigation.js +30 -0
  190. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  191. package/build/dist/Utils/Array.js +25 -0
  192. package/build/dist/Utils/Array.js.map +1 -1
  193. package/build/dist/Utils/CertificateReissue.js +97 -0
  194. package/build/dist/Utils/CertificateReissue.js.map +1 -0
  195. package/build/dist/Utils/Grok/Grok.js +324 -0
  196. package/build/dist/Utils/Grok/Grok.js.map +1 -0
  197. package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
  198. package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
  199. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
  200. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  201. package/package.json +1 -1
  202. package/UI/Utils/JsonWebToken.ts +0 -14
  203. package/build/dist/UI/Utils/JsonWebToken.js +0 -10
  204. package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
@@ -0,0 +1,520 @@
1
+ import "@testing-library/jest-dom";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { describe, expect, test } from "@jest/globals";
4
+ import * as React from "react";
5
+ import fs from "fs";
6
+ import path from "path";
7
+
8
+ import MicrosoftTeamsIntegrationDocumentation from "../../../../App/FeatureSet/Dashboard/src/Components/MicrosoftTeams/MicrosoftTeamsIntegrationDocumentation";
9
+
10
+ /*
11
+ * A self-hosted deployment that Azure Bot Service cannot reach does not look
12
+ * broken. Alert cards keep arriving, because OneUptime posts those by calling
13
+ * Microsoft — no inbound access needed. Everything that travels the other way
14
+ * is dead: card buttons answer "Unable to reach app", bot commands go
15
+ * unanswered, and chats never register because chat registration *is* an
16
+ * inbound bot activity.
17
+ *
18
+ * Read from inside Teams that reads as a half-finished integration, and the
19
+ * cheapest check an admin runs next — opening the messaging endpoint in a
20
+ * browser — used to return "Page not found". So the docs are what has to
21
+ * carry the diagnosis: the two directions are independent, a working alert
22
+ * tests only one of them, and an empty Chats list is the confirmation rather
23
+ * than a second unrelated bug.
24
+ *
25
+ * These assertions are on prose because the failure was prose.
26
+ */
27
+
28
+ const REPO_ROOT: string = path.resolve(__dirname, "../../../..");
29
+
30
+ const DOCS_MARKDOWN_PATH: string = path.join(
31
+ REPO_ROOT,
32
+ "App/FeatureSet/Docs/Content/en/self-hosted/microsoft-teams-integration.md",
33
+ );
34
+
35
+ const IN_PRODUCT_GUIDE_PATH: string = path.join(
36
+ REPO_ROOT,
37
+ "App/FeatureSet/Dashboard/src/Components/MicrosoftTeams/MicrosoftTeamsIntegrationDocumentation.tsx",
38
+ );
39
+
40
+ const TEAMS_API_PATH: string = path.join(
41
+ REPO_ROOT,
42
+ "Common/Server/API/MicrosoftTeamsAPI.ts",
43
+ );
44
+
45
+ const MESSAGING_ENDPOINT_PATH: string = "/api/microsoft-bot/messages";
46
+
47
+ type ReadFileFunction = (filePath: string) => string;
48
+
49
+ const readSource: ReadFileFunction = (filePath: string): string => {
50
+ return fs.readFileSync(filePath, "utf8");
51
+ };
52
+
53
+ type RenderDocumentationFunction = () => Promise<string>;
54
+
55
+ /*
56
+ * react-markdown is mocked globally to a div holding its children, so the
57
+ * resolved markdown (window.location.origin already interpolated) is what
58
+ * lands in the DOM.
59
+ *
60
+ * The explicit timeout is not padding. MarkdownViewer is a React.lazy import,
61
+ * so the very first render has to pull the viewer's whole module graph through
62
+ * ts-jest before anything reaches the DOM — comfortably past findBy's 1s
63
+ * default on a cold cache, while every later render resolves the already
64
+ * loaded chunk in single-digit milliseconds. Left at the default, the first
65
+ * test to render would fail and every other one would pass, which reads as a
66
+ * content bug rather than the warm-up cost it is.
67
+ */
68
+ const renderDocumentationText: RenderDocumentationFunction =
69
+ async (): Promise<string> => {
70
+ render(<MicrosoftTeamsIntegrationDocumentation />);
71
+
72
+ const markdown: HTMLElement = await screen.findByTestId(
73
+ "react-markdown",
74
+ {},
75
+ { timeout: 30000 },
76
+ );
77
+
78
+ return markdown.textContent || "";
79
+ };
80
+
81
+ /*
82
+ * Raised for the same reason: the lazy chunk has to load once, inside whichever
83
+ * test renders first, and 5s is not enough headroom for that on a cold cache
84
+ * under CI contention.
85
+ */
86
+ jest.setTimeout(60000);
87
+
88
+ const docsMarkdown: string = readSource(DOCS_MARKDOWN_PATH);
89
+
90
+ describe("Self-hosted docs: verifying the messaging endpoint during setup", () => {
91
+ const stepFour: string = docsMarkdown
92
+ .split("### Step 4: Create a Bot Service")[1]!
93
+ .split("### Step 5:")[0]!;
94
+
95
+ test("tells the admin to verify the endpoint before moving on", () => {
96
+ expect(stepFour).toContain("Verify the endpoint before you move on");
97
+ });
98
+
99
+ test("says to test from outside the network, not from inside the cluster", () => {
100
+ const lowered: string = stepFour.toLowerCase();
101
+
102
+ expect(lowered).toContain("outside your network");
103
+ expect(lowered).toContain("inside the cluster");
104
+ expect(lowered).toContain("vpn");
105
+ });
106
+
107
+ test("gives a curl that shows the response body, not only the status code", () => {
108
+ /*
109
+ * An earlier draft used `-o /dev/null -w '%{http_code}'`, which throws
110
+ * away the one thing that makes a 404 diagnosable. Keep -i.
111
+ */
112
+ expect(stepFour).toContain("curl -sS -i");
113
+ expect(stepFour).not.toContain("-o /dev/null");
114
+ expect(stepFour).toContain(MESSAGING_ENDPOINT_PATH);
115
+ });
116
+
117
+ test("names 405 as the passing result", () => {
118
+ /*
119
+ * The status code is the whole signal, so it has to be stated as success
120
+ * outright — an admin who reads 405 as an error stops here.
121
+ */
122
+ expect(stepFour).toContain("**405**");
123
+ /*
124
+ * [\s\S] rather than the dotAll flag: this package targets es2017, where
125
+ * /s is a compile error (TS1501) that takes the whole suite down with it.
126
+ * stepFour spans several lines, so `.` alone would not reach the verdict.
127
+ */
128
+ expect(stepFour.toLowerCase()).toMatch(/405[\s\S]*(correct|done)/);
129
+ });
130
+
131
+ test("splits 404 by response body rather than calling every 404 unreachable", () => {
132
+ /*
133
+ * The regression this guards is the one the reviewers caught: OneUptime's
134
+ * own not-found handler serves a 404 for this path, so "404 means the
135
+ * request did not reach OneUptime" is false exactly when an admin is most
136
+ * likely to hit it (any build older than the 405, or an ingress that
137
+ * strips the /api prefix). Both readings must be present and separated by
138
+ * the body.
139
+ */
140
+ const lowered: string = stepFour.toLowerCase();
141
+
142
+ expect(stepFour).toContain(
143
+ '**404 with a JSON body of `{"message":"Page not found - /api/microsoft-bot/messages"}`**',
144
+ );
145
+ expect(lowered).toContain(
146
+ "came from oneuptime, so the request *did* arrive",
147
+ );
148
+
149
+ expect(stepFour).toContain("**404 with an HTML error page**");
150
+ expect(lowered).toContain("never reached oneuptime");
151
+
152
+ expect(lowered).toMatch(/proxy|ingress|load balancer/);
153
+ expect(lowered).toContain("tls error");
154
+ });
155
+
156
+ test("explains why the body matters before listing the outcomes", () => {
157
+ expect(stepFour.toLowerCase()).toContain(
158
+ "the only thing that tells you who produced it",
159
+ );
160
+ });
161
+ });
162
+
163
+ describe("Self-hosted docs: the unreachable-endpoint troubleshooting section", () => {
164
+ const section: string = docsMarkdown
165
+ .split(
166
+ '### Card buttons say "Unable to reach app", and chats never appear',
167
+ )[1]!
168
+ .split("### Checking this deployment's bot configuration")[0]!;
169
+
170
+ test("the section exists and is reachable from the Step 4 anchor", () => {
171
+ expect(section.length).toBeGreaterThan(0);
172
+ expect(docsMarkdown).toContain(
173
+ "#card-buttons-say-unable-to-reach-app-and-chats-never-appear",
174
+ );
175
+ });
176
+
177
+ test("states the single root cause up front", () => {
178
+ expect(section).toContain(
179
+ "**Azure Bot Service cannot reach your messaging endpoint.**",
180
+ );
181
+ expect(section).toContain("These are one failure, not two");
182
+ });
183
+
184
+ test("explains that a working alert card tests the opposite direction", () => {
185
+ const lowered: string = section.toLowerCase();
186
+
187
+ expect(lowered).toContain("oneuptime calls microsoft");
188
+ expect(lowered).toMatch(
189
+ /tells you nothing at all about the bot endpoint|working alert/,
190
+ );
191
+ });
192
+
193
+ test("tabulates which paths need inbound access and which do not", () => {
194
+ expect(section).toContain("| Direction |");
195
+ expect(section).toContain("Needs Azure to reach you?");
196
+
197
+ const rows: Array<string> = section.split("\n").filter((line: string) => {
198
+ return line.trim().startsWith("|") && line.includes("**");
199
+ });
200
+
201
+ /*
202
+ * One "No" row (the outbound alert) and three "Yes" rows (button tap, bot
203
+ * command, chat registration) — the asymmetry is the point of the table,
204
+ * so a table that lost the outbound row would still read as consistent
205
+ * and would no longer explain anything.
206
+ */
207
+ expect(
208
+ rows.filter((row: string) => {
209
+ return row.includes("**No**");
210
+ }),
211
+ ).toHaveLength(1);
212
+ expect(
213
+ rows.filter((row: string) => {
214
+ return row.includes("**Yes**");
215
+ }).length,
216
+ ).toBeGreaterThanOrEqual(3);
217
+ });
218
+
219
+ test("explains why the empty Chats list confirms the diagnosis", () => {
220
+ const lowered: string = section.toLowerCase();
221
+
222
+ expect(lowered).toContain("refresh chats");
223
+ expect(lowered).toContain("application permissions");
224
+ });
225
+
226
+ test("names all three activities that register a chat, not just the install event", () => {
227
+ /*
228
+ * captureChatFromBotActivity is called from three inbound paths in
229
+ * MicrosoftTeams.ts: installationUpdate add/add-upgrade, conversationUpdate
230
+ * with the bot in membersAdded, and any message in a personal/groupChat
231
+ * conversation. That third one is deliberate — it is the documented
232
+ * recovery path for chats installed before chat capture shipped, and the
233
+ * Chats card's own empty state tells admins to use it. Saying a chat can
234
+ * only register from an installation event tells an admin their setup is
235
+ * broken when messaging the bot would have fixed it.
236
+ */
237
+ const lowered: string = section.toLowerCase();
238
+
239
+ expect(lowered).toContain("installed");
240
+ expect(lowered).toContain("added to the conversation");
241
+ expect(lowered).toContain("any message sent to the bot");
242
+ });
243
+
244
+ test("the inbound table describes chat registration as an activity, not an install event", () => {
245
+ const chatRow: string =
246
+ section.split("\n").find((line: string) => {
247
+ return line.includes("A chat registers under");
248
+ }) || "";
249
+
250
+ expect(chatRow).not.toBe("");
251
+ expect(chatRow).toContain("bot activity");
252
+ expect(chatRow).not.toContain("installation event");
253
+ });
254
+
255
+ test("tells the admin to grep for the POST rather than the 404", () => {
256
+ expect(section).toContain("Look for the POST, not for 404s");
257
+ expect(section).toContain("grep 'POST /api/microsoft-bot/messages'");
258
+ });
259
+
260
+ test("covers the certificate chain, with a command that checks it", () => {
261
+ expect(section).toContain("publicly trusted certificate");
262
+ expect(section).toContain("full chain");
263
+ expect(section).toContain("openssl s_client");
264
+ expect(section).toContain("-verify_return_error");
265
+ });
266
+
267
+ test("explains why a TLS failure leaves the access log empty", () => {
268
+ expect(section.toLowerCase()).toContain(
269
+ "before oneuptime sees the request",
270
+ );
271
+ });
272
+
273
+ test("covers private DNS and split-horizon records", () => {
274
+ const lowered: string = section.toLowerCase();
275
+
276
+ expect(lowered).toContain("private dns name");
277
+ expect(lowered).toContain("split-horizon");
278
+ });
279
+
280
+ test("says outright that a 404 on GET is not the bug", () => {
281
+ /*
282
+ * This is the sentence the issue was filed for. Without it the docs
283
+ * describe the right fix and still leave the misleading evidence
284
+ * unexplained.
285
+ */
286
+ expect(section).toContain(
287
+ "**A 404 on `GET /api/microsoft-bot/messages` is not the bug, and it is not evidence Azure could not reach you.**",
288
+ );
289
+ expect(section).toContain("has always accepted POST only");
290
+ expect(section).toContain("405 Method Not Allowed");
291
+ });
292
+
293
+ test("states that OneUptime generated that 404, so the request reached the app", () => {
294
+ /*
295
+ * The point that closes issue #3488: the reporter's `404 58` is 58 bytes
296
+ * of OneUptime's own JSON, so their request arrived. Reading that 404 as
297
+ * "Azure could not reach us" is the wrong turn the whole section exists
298
+ * to prevent, and the byte count is what makes it checkable against a
299
+ * real access log.
300
+ */
301
+ expect(section).toContain(
302
+ '{"message":"Page not found - /api/microsoft-bot/messages"}',
303
+ );
304
+ expect(section.toLowerCase()).toContain("the request reached the app");
305
+ expect(section).toContain("58 bytes");
306
+ expect(section).toContain(
307
+ '"GET /api/microsoft-bot/messages HTTP/1.1" 404 58',
308
+ );
309
+ });
310
+
311
+ test("tells admins on older builds to judge a 404 by its body", () => {
312
+ const lowered: string = section.toLowerCase();
313
+
314
+ expect(lowered).toContain("judge a 404 by its body");
315
+ expect(lowered).toContain("html error page");
316
+ });
317
+ });
318
+
319
+ describe("Self-hosted docs: the bot configuration check", () => {
320
+ const section: string = docsMarkdown
321
+ .split("### Checking this deployment's bot configuration")[1]!
322
+ .split('### "The OneUptime app is not installed')[0]!;
323
+
324
+ test("documents the /api/microsoft-bot/test endpoint", () => {
325
+ expect(section).toContain("/api/microsoft-bot/test");
326
+ expect(section).toContain("curl");
327
+ });
328
+
329
+ test("says what it cannot tell you, so it is not mistaken for a green light", () => {
330
+ const lowered: string = section.toLowerCase();
331
+
332
+ expect(lowered).toContain("does not call azure");
333
+ expect(lowered).toContain("cannot tell you");
334
+ expect(lowered).toContain("reach you");
335
+ });
336
+
337
+ test("names the bot id comparison as the decisive check", () => {
338
+ expect(section).toContain("botId");
339
+ expect(section.toLowerCase()).toContain("bot id of the oneuptime app");
340
+ });
341
+ });
342
+
343
+ describe("In-product setup guide", () => {
344
+ test("Step 5 tells the admin to verify the endpoint and names 405 as correct", async () => {
345
+ const text: string = await renderDocumentationText();
346
+
347
+ const stepFive: string = text
348
+ .split("##### Step 5: Create a Bot Service")[1]!
349
+ .split("##### Step 6")[0]!;
350
+
351
+ expect(stepFive).toContain("Verify the endpoint before moving on");
352
+ expect(stepFive).toContain("**405**");
353
+ expect(stepFive).toContain("curl -sS -i");
354
+ expect(stepFive).not.toContain("-o /dev/null");
355
+ expect(stepFive.toLowerCase()).toContain("outside your network");
356
+ });
357
+
358
+ test("Step 5 splits 404 by response body, matching the self-hosted docs", async () => {
359
+ const text: string = await renderDocumentationText();
360
+
361
+ const stepFive: string = text
362
+ .split("##### Step 5: Create a Bot Service")[1]!
363
+ .split("##### Step 6")[0]!;
364
+
365
+ expect(stepFive).toContain("**404 with a JSON body of");
366
+ expect(stepFive).toContain("**404 with an HTML error page**");
367
+ expect(stepFive.toLowerCase()).toContain("the request *did* arrive");
368
+ expect(stepFive.toLowerCase()).toContain("never reached oneuptime");
369
+ });
370
+
371
+ test("Step 5 interpolates this deployment's own origin into the check", async () => {
372
+ const text: string = await renderDocumentationText();
373
+
374
+ const stepFive: string = text
375
+ .split("##### Step 5: Create a Bot Service")[1]!
376
+ .split("##### Step 6")[0]!;
377
+
378
+ expect(stepFive).toContain(
379
+ `${window.location.origin}${MESSAGING_ENDPOINT_PATH}`,
380
+ );
381
+ });
382
+
383
+ test("Troubleshooting covers the unreachable endpoint as one failure, not two", async () => {
384
+ const text: string = await renderDocumentationText();
385
+ const troubleshooting: string = text.split("##### Troubleshooting")[1]!;
386
+
387
+ expect(troubleshooting).toContain(
388
+ 'Card buttons say "Unable to reach app", and chats never appear',
389
+ );
390
+ expect(troubleshooting).toContain("One failure, not two");
391
+ expect(troubleshooting).toContain(
392
+ "**Azure Bot Service cannot reach your messaging endpoint.**",
393
+ );
394
+ });
395
+
396
+ test("Troubleshooting tells the admin to grep for the POST", async () => {
397
+ const text: string = await renderDocumentationText();
398
+ const troubleshooting: string = text.split("##### Troubleshooting")[1]!;
399
+
400
+ expect(troubleshooting).toContain("Look for the POST, not for 404s");
401
+ expect(troubleshooting).toContain("POST /api/microsoft-bot/messages");
402
+ });
403
+
404
+ test("Troubleshooting covers the TLS chain and public DNS resolution", async () => {
405
+ const text: string = await renderDocumentationText();
406
+ const troubleshooting: string = text.split("##### Troubleshooting")[1]!;
407
+
408
+ expect(troubleshooting).toContain("publicly trusted certificate");
409
+ expect(troubleshooting.toLowerCase()).toContain("split-horizon");
410
+ });
411
+
412
+ test("Troubleshooting says a 404 on GET is not the bug, and that OneUptime served it", async () => {
413
+ const text: string = await renderDocumentationText();
414
+ const troubleshooting: string = text.split("##### Troubleshooting")[1]!;
415
+
416
+ expect(troubleshooting).toContain(
417
+ "A 404 on `GET /api/microsoft-bot/messages` is not the bug, and it is not evidence Azure could not reach you.",
418
+ );
419
+ expect(troubleshooting).toContain(
420
+ '{"message":"Page not found - /api/microsoft-bot/messages"}',
421
+ );
422
+ expect(troubleshooting.toLowerCase()).toContain("reached the app");
423
+ expect(troubleshooting).toContain("58 bytes");
424
+ });
425
+
426
+ test("the empty-chats entry names every activity that registers a chat", async () => {
427
+ /*
428
+ * An earlier draft said chats register "only when the bot receives an
429
+ * activity" here while the markdown said "only ... the installation
430
+ * event" — two documents in one change disagreeing, with the markdown
431
+ * being the wrong one. Both now name all three paths.
432
+ */
433
+ const text: string = await renderDocumentationText();
434
+ const troubleshooting: string = text.split("##### Troubleshooting")[1]!;
435
+
436
+ const chatsSection: string = troubleshooting.split(
437
+ "**No chats appear under Microsoft Teams Chats**",
438
+ )[1]!;
439
+
440
+ const lowered: string = chatsSection.toLowerCase();
441
+
442
+ expect(lowered).toContain("the app being installed");
443
+ expect(lowered).toContain("added to the conversation");
444
+ expect(lowered).toContain("any message sent to the bot");
445
+ });
446
+
447
+ test("the empty-chats entry now lists the unreachable endpoint first", async () => {
448
+ /*
449
+ * It used to name exactly one cause — a wrong app package — which is the
450
+ * rarer of the two and sends the admin to re-upload a manifest that was
451
+ * already correct. The giveaway for the common cause (alerts still post)
452
+ * has to be on this screen.
453
+ */
454
+ const text: string = await renderDocumentationText();
455
+ const troubleshooting: string = text.split("##### Troubleshooting")[1]!;
456
+
457
+ const chatsSection: string = troubleshooting.split(
458
+ "**No chats appear under Microsoft Teams Chats**",
459
+ )[1]!;
460
+
461
+ const unreachableIndex: number = chatsSection.indexOf(
462
+ "The messaging endpoint is unreachable",
463
+ );
464
+ const wrongPackageIndex: number = chatsSection.indexOf(
465
+ "The installed package points at a different deployment",
466
+ );
467
+
468
+ expect(unreachableIndex).toBeGreaterThan(-1);
469
+ expect(wrongPackageIndex).toBeGreaterThan(-1);
470
+ expect(unreachableIndex).toBeLessThan(wrongPackageIndex);
471
+ expect(chatsSection).toContain("alerts still post to channels");
472
+ });
473
+
474
+ test("documents /api/microsoft-bot/test and its limits", async () => {
475
+ const text: string = await renderDocumentationText();
476
+ const troubleshooting: string = text.split("##### Troubleshooting")[1]!;
477
+
478
+ expect(troubleshooting).toContain("/api/microsoft-bot/test");
479
+ expect(troubleshooting).toContain("does not call Azure");
480
+ });
481
+ });
482
+
483
+ describe("The messaging endpoint path is stated identically everywhere", () => {
484
+ /*
485
+ * Three files tell an admin what to paste into the Azure Bot's Configuration
486
+ * blade, and the API is the only one of them that decides what the server
487
+ * actually serves. A drift between them is unfalsifiable from inside Teams:
488
+ * the bot simply never responds, which is the same symptom as every other
489
+ * cause in this section.
490
+ */
491
+ const SOURCES: Array<string> = [
492
+ DOCS_MARKDOWN_PATH,
493
+ IN_PRODUCT_GUIDE_PATH,
494
+ TEAMS_API_PATH,
495
+ ];
496
+
497
+ test.each(SOURCES)("%s names /microsoft-bot/messages", (filePath: string) => {
498
+ expect(readSource(filePath)).toContain("/microsoft-bot/messages");
499
+ });
500
+
501
+ test("no source has drifted to a different bot path", () => {
502
+ const BOT_PATH_PATTERN: RegExp = /\/microsoft-bot\/[a-z-]+/g;
503
+ const KNOWN_PATHS: Array<string> = [
504
+ "/microsoft-bot/messages",
505
+ "/microsoft-bot/test",
506
+ ];
507
+
508
+ SOURCES.forEach((filePath: string) => {
509
+ const found: Array<string> = readSource(filePath).match(
510
+ BOT_PATH_PATTERN,
511
+ ) as Array<string>;
512
+
513
+ expect(found).not.toBeNull();
514
+
515
+ found.forEach((botPath: string) => {
516
+ expect(KNOWN_PATHS).toContain(botPath);
517
+ });
518
+ });
519
+ });
520
+ });
@@ -1,3 +1,5 @@
1
+ import Route from "../../../Types/API/Route";
2
+ import BadDataException from "../../../Types/Exception/BadDataException";
1
3
  import Navigation from "../../../UI/Utils/Navigation";
2
4
  import {
3
5
  afterEach,
@@ -181,3 +183,83 @@ describe("Navigation URL query string helpers", () => {
181
183
  });
182
184
  });
183
185
  });
186
+
187
+ describe("Navigation internal route safety", () => {
188
+ beforeEach(() => {
189
+ setUrl("/dashboard/monitors");
190
+ });
191
+
192
+ afterEach(() => {
193
+ jest.restoreAllMocks();
194
+ });
195
+
196
+ test.each([
197
+ "/",
198
+ "/incidents/incident-id",
199
+ "/scheduled-maintenance/event-id?tab=timeline#note",
200
+ "/status-page/status-page-id/",
201
+ "/callback?next=https://example.com/complete",
202
+ "/literal/javascript:alert(1)",
203
+ ])("accepts same-origin path %s", (routeValue: string) => {
204
+ expect(Navigation.isSafeInternalRoute(routeValue)).toBe(true);
205
+ });
206
+
207
+ test.each([
208
+ "",
209
+ "incidents/incident-id",
210
+ "?tab=timeline",
211
+ "#note",
212
+ "javascript:alert(document.domain)",
213
+ "JaVaScRiPt:alert(1)",
214
+ "data:text/plain,hello",
215
+ "http://evil.example/path",
216
+ "https://evil.example/path",
217
+ "//evil.example/path",
218
+ "///evil.example/path",
219
+ "\\evil.example/path",
220
+ "/\\evil.example/path",
221
+ '/"',
222
+ "/☃",
223
+ "/\u0000control",
224
+ ])("rejects non-internal target %s", (routeValue: string) => {
225
+ expect(Navigation.isSafeInternalRoute(routeValue)).toBe(false);
226
+ });
227
+
228
+ test("rejects an absolute URL even when it names the current origin", () => {
229
+ expect(
230
+ Navigation.isSafeInternalRoute(
231
+ `${window.location.origin}/dashboard/monitors`,
232
+ ),
233
+ ).toBe(false);
234
+ });
235
+
236
+ test.each([
237
+ "javascript:alert(document.domain)",
238
+ "https://evil.example/path",
239
+ "//evil.example/path",
240
+ "incidents/incident-id",
241
+ "/\\evil.example/path",
242
+ ])(
243
+ "forced navigation independently rejects spoofed Route target %s",
244
+ (routeValue: string) => {
245
+ const currentUrl: string = window.location.href;
246
+ const route: Route = new Route("/safe");
247
+
248
+ jest.spyOn(route, "toString").mockReturnValue(routeValue);
249
+
250
+ expect(() => {
251
+ Navigation.navigate(route, { forceNavigate: true });
252
+ }).toThrowError(BadDataException);
253
+ expect(window.location.href).toBe(currentUrl);
254
+ },
255
+ );
256
+
257
+ test("forced navigation still accepts an internal path", () => {
258
+ Navigation.navigate(new Route("/dashboard/monitors#forced"), {
259
+ forceNavigate: true,
260
+ });
261
+
262
+ expect(window.location.pathname).toBe("/dashboard/monitors");
263
+ expect(window.location.hash).toBe("#forced");
264
+ });
265
+ });