@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.
- package/Models/AnalyticsModels/RumSessionChunk.ts +52 -0
- package/Models/DatabaseModels/CloudResource.ts +17 -1
- package/Models/DatabaseModels/DashboardDomain.ts +43 -0
- package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
- package/Models/DatabaseModels/RumApplication.ts +35 -2
- package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
- package/Server/API/DashboardDomainAPI.ts +74 -0
- package/Server/API/MicrosoftTeamsAPI.ts +81 -180
- package/Server/API/StatusPageDomainAPI.ts +74 -0
- package/Server/API/TelemetryAPI.ts +104 -15
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/ProjectAuthorization.ts +6 -1
- package/Server/Services/DashboardDomainService.ts +131 -0
- package/Server/Services/LogPipelineProcessorService.ts +88 -0
- package/Server/Services/MonitorTemplateService.ts +70 -11
- package/Server/Services/StatusPageDomainService.ts +163 -3
- package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
- package/Server/Types/Domain.ts +17 -0
- package/Server/Utils/APIKey/AccessPermission.ts +24 -0
- package/Server/Utils/Greenlock/Greenlock.ts +156 -58
- package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +12 -5
- package/Server/Utils/SessionReplay/SessionReplayIdentity.ts +102 -0
- package/Server/Utils/SessionReplay/SessionReplayReadService.ts +11 -2
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
- package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
- package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +81 -0
- package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
- package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
- package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
- package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
- package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
- package/Tests/Server/API/SessionReplayAPI.test.ts +232 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
- package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
- package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
- package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
- package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
- package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
- package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
- package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
- package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
- package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
- package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
- package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
- package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
- package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
- package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
- package/Tests/Types/API/HTTPResponse.test.ts +175 -0
- package/Tests/Types/API/Route.test.ts +36 -4
- package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
- package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
- package/Tests/Types/DateLocalTimeString.test.ts +259 -0
- package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
- package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
- package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
- package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
- package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
- package/Tests/UI/Utils/Navigation.test.ts +82 -0
- package/Tests/Utils/Array.test.ts +100 -0
- package/Tests/Utils/CertificateReissue.test.ts +225 -0
- package/Tests/Utils/Grok/Grok.test.ts +492 -0
- package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
- package/Types/API/Route.ts +20 -5
- package/Types/Date.ts +143 -13
- package/Types/Rum/SessionReplay.ts +76 -1
- package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
- package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
- package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
- package/UI/Utils/Navigation.ts +42 -0
- package/Utils/Array.ts +37 -0
- package/Utils/CertificateReissue.ts +128 -0
- package/Utils/Grok/Grok.ts +461 -0
- package/Utils/Grok/GrokPatterns.ts +118 -0
- package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +48 -0
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
- package/build/dist/Models/DatabaseModels/CloudResource.js +19 -2
- package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +37 -3
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
- package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
- package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
- package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +67 -14
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/DashboardDomainService.js +104 -0
- package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
- package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
- package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
- package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
- package/build/dist/Server/Types/Domain.js +16 -0
- package/build/dist/Server/Types/Domain.js.map +1 -1
- package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
- package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
- package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
- package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js +59 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +9 -2
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/API/Route.js +16 -5
- package/build/dist/Types/API/Route.js.map +1 -1
- package/build/dist/Types/Date.js +115 -13
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplay.js +38 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +30 -0
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/Utils/Array.js +25 -0
- package/build/dist/Utils/Array.js.map +1 -1
- package/build/dist/Utils/CertificateReissue.js +97 -0
- package/build/dist/Utils/CertificateReissue.js.map +1 -0
- package/build/dist/Utils/Grok/Grok.js +324 -0
- package/build/dist/Utils/Grok/Grok.js.map +1 -0
- package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
- package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
- package/package.json +1 -1
- package/UI/Utils/JsonWebToken.ts +0 -14
- package/build/dist/UI/Utils/JsonWebToken.js +0 -10
- package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
|
|
2
|
+
import {
|
|
3
|
+
ExpressRequest,
|
|
4
|
+
ExpressResponse,
|
|
5
|
+
ExpressRouter,
|
|
6
|
+
} from "../../../Server/Utils/Express";
|
|
7
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
8
|
+
import MicrosoftTeamsAPI from "../../../Server/API/MicrosoftTeamsAPI";
|
|
9
|
+
import MicrosoftTeamsUtil from "../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* The bug this suite exists for was not in the routing, it was in the answer.
|
|
13
|
+
*
|
|
14
|
+
* /api/microsoft-bot/messages accepts POST only, because Azure Bot Service is
|
|
15
|
+
* the only thing that ever calls it. Checking a messaging endpoint with a
|
|
16
|
+
* browser or curl is the first thing a self-hosted admin does, and that check
|
|
17
|
+
* is a GET — which fell through to the catch-all in StartServer and came back
|
|
18
|
+
* "Page not found - /api/microsoft-bot/messages". True of the method, false of
|
|
19
|
+
* the endpoint, and indistinguishable from a route that had been dropped from
|
|
20
|
+
* the build. Admins filed regressions against the route while the real fault
|
|
21
|
+
* (Azure could not reach the deployment at all) went untouched.
|
|
22
|
+
*
|
|
23
|
+
* So the assertions here are about the response being *diagnostic*: a 405 that
|
|
24
|
+
* separates "wrong method" from "no such route", and a body that redirects the
|
|
25
|
+
* next hour of debugging at the network path instead of OneUptime's routing
|
|
26
|
+
* table. Content assertions look brittle; they are the feature.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const MOCK_TEAMS_CLIENT_ID: string = "11111111-2222-3333-4444-555555555555";
|
|
30
|
+
const MOCK_TEAMS_CLIENT_SECRET: string =
|
|
31
|
+
"mock-teams-client-secret-that-must-never-be-echoed";
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
* Same lazy-getter shape as MicrosoftTeamsBotTestEndpoint.test.ts: the route
|
|
35
|
+
* reads these at request time (named imports compile to property reads on the
|
|
36
|
+
* module object), so getters let one loaded copy of the API module serve both
|
|
37
|
+
* the configured and unconfigured cases.
|
|
38
|
+
*/
|
|
39
|
+
jest.mock("../../../Server/EnvironmentConfig", () => {
|
|
40
|
+
const state: { clientId: string | null; clientSecret: string | null } = {
|
|
41
|
+
clientId: "11111111-2222-3333-4444-555555555555",
|
|
42
|
+
clientSecret: "mock-teams-client-secret-that-must-never-be-echoed",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const mocked: Record<string, unknown> = {
|
|
46
|
+
...(jest.requireActual("../../../Server/EnvironmentConfig") as Record<
|
|
47
|
+
string,
|
|
48
|
+
unknown
|
|
49
|
+
>),
|
|
50
|
+
setMockTeamsConfig: (
|
|
51
|
+
clientId: string | null,
|
|
52
|
+
clientSecret: string | null,
|
|
53
|
+
): void => {
|
|
54
|
+
state.clientId = clientId;
|
|
55
|
+
state.clientSecret = clientSecret;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
Object.defineProperty(mocked, "MicrosoftTeamsAppClientId", {
|
|
60
|
+
get: (): string | null => {
|
|
61
|
+
return state.clientId;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
Object.defineProperty(mocked, "MicrosoftTeamsAppClientSecret", {
|
|
66
|
+
get: (): string | null => {
|
|
67
|
+
return state.clientSecret;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return mocked;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
type SetMockTeamsConfigFunction = (
|
|
75
|
+
clientId: string | null,
|
|
76
|
+
clientSecret: string | null,
|
|
77
|
+
) => void;
|
|
78
|
+
|
|
79
|
+
const setTeamsConfig: SetMockTeamsConfigFunction = (
|
|
80
|
+
clientId: string | null,
|
|
81
|
+
clientSecret: string | null,
|
|
82
|
+
): void => {
|
|
83
|
+
(
|
|
84
|
+
jest.requireMock("../../../Server/EnvironmentConfig") as {
|
|
85
|
+
setMockTeamsConfig: SetMockTeamsConfigFunction;
|
|
86
|
+
}
|
|
87
|
+
).setMockTeamsConfig(clientId, clientSecret);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type ExpressRouteHandler = (
|
|
91
|
+
req: ExpressRequest,
|
|
92
|
+
res: ExpressResponse,
|
|
93
|
+
) => Promise<void> | void;
|
|
94
|
+
|
|
95
|
+
type ExpressRouterLayer = {
|
|
96
|
+
route?: {
|
|
97
|
+
path: string;
|
|
98
|
+
methods: Record<string, boolean>;
|
|
99
|
+
stack: Array<{ handle: ExpressRouteHandler }>;
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const MESSAGES_PATH: string = "/microsoft-bot/messages";
|
|
104
|
+
|
|
105
|
+
type FindRouteFunction = (
|
|
106
|
+
path: string,
|
|
107
|
+
method: string,
|
|
108
|
+
) => ExpressRouterLayer["route"] | undefined;
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
* Reads the router the API class actually builds, so the route wiring (path +
|
|
112
|
+
* method) is under test rather than a hand-copied handler.
|
|
113
|
+
*/
|
|
114
|
+
const findRoute: FindRouteFunction = (
|
|
115
|
+
path: string,
|
|
116
|
+
method: string,
|
|
117
|
+
): ExpressRouterLayer["route"] | undefined => {
|
|
118
|
+
const router: ExpressRouter = new MicrosoftTeamsAPI().getRouter();
|
|
119
|
+
const layers: Array<ExpressRouterLayer> = (
|
|
120
|
+
router as unknown as { stack: Array<ExpressRouterLayer> }
|
|
121
|
+
).stack;
|
|
122
|
+
|
|
123
|
+
return layers.find((candidate: ExpressRouterLayer) => {
|
|
124
|
+
return (
|
|
125
|
+
candidate.route?.path === path &&
|
|
126
|
+
candidate.route?.methods[method] === true
|
|
127
|
+
);
|
|
128
|
+
})?.route;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
type GetMessagesGetHandlerFunction = () => ExpressRouteHandler;
|
|
132
|
+
|
|
133
|
+
const getMessagesGetHandler: GetMessagesGetHandlerFunction =
|
|
134
|
+
(): ExpressRouteHandler => {
|
|
135
|
+
const route: ExpressRouterLayer["route"] | undefined = findRoute(
|
|
136
|
+
MESSAGES_PATH,
|
|
137
|
+
"get",
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
if (!route) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`GET ${MESSAGES_PATH} is not registered on the MicrosoftTeamsAPI router`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
expect(route.stack).toHaveLength(1);
|
|
147
|
+
|
|
148
|
+
return route.stack[0]!.handle;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
type InvokedResponse = {
|
|
152
|
+
statusCode: number;
|
|
153
|
+
body: JSONObject;
|
|
154
|
+
headers: Record<string, string>;
|
|
155
|
+
sentAsCsv: boolean;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
type CallMessagesGetFunction = (
|
|
159
|
+
query?: Record<string, string>,
|
|
160
|
+
) => Promise<InvokedResponse>;
|
|
161
|
+
|
|
162
|
+
const callMessagesGet: CallMessagesGetFunction = async (
|
|
163
|
+
query: Record<string, string> = {},
|
|
164
|
+
): Promise<InvokedResponse> => {
|
|
165
|
+
const handler: ExpressRouteHandler = getMessagesGetHandler();
|
|
166
|
+
|
|
167
|
+
const invoked: InvokedResponse = {
|
|
168
|
+
statusCode: 0,
|
|
169
|
+
body: {},
|
|
170
|
+
headers: {},
|
|
171
|
+
sentAsCsv: false,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const res: Partial<ExpressResponse> = {
|
|
175
|
+
setHeader(name: string, value: string): ExpressResponse {
|
|
176
|
+
invoked.headers[name.toLowerCase()] = value;
|
|
177
|
+
return res as ExpressResponse;
|
|
178
|
+
},
|
|
179
|
+
status(statusCode: number): ExpressResponse {
|
|
180
|
+
invoked.statusCode = statusCode;
|
|
181
|
+
return res as ExpressResponse;
|
|
182
|
+
},
|
|
183
|
+
json(body: JSONObject): ExpressResponse {
|
|
184
|
+
invoked.body = body;
|
|
185
|
+
return res as ExpressResponse;
|
|
186
|
+
},
|
|
187
|
+
/*
|
|
188
|
+
* Present but never expected to fire. Response.sendJsonObjectResponse
|
|
189
|
+
* lands here for its ?output-type=csv branch, so a call to send() would
|
|
190
|
+
* mean the handler went back through the helper that can rewrite a 405
|
|
191
|
+
* into a 200.
|
|
192
|
+
*/
|
|
193
|
+
send(body: unknown): ExpressResponse {
|
|
194
|
+
invoked.sentAsCsv = true;
|
|
195
|
+
invoked.body = body as JSONObject;
|
|
196
|
+
return res as ExpressResponse;
|
|
197
|
+
},
|
|
198
|
+
} as unknown as Partial<ExpressResponse>;
|
|
199
|
+
|
|
200
|
+
const req: Partial<ExpressRequest> = {
|
|
201
|
+
query: query,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
await handler(req as ExpressRequest, res as ExpressResponse);
|
|
205
|
+
|
|
206
|
+
return invoked;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
type JoinGuidanceFunction = (values: unknown) => string;
|
|
210
|
+
|
|
211
|
+
const joinGuidance: JoinGuidanceFunction = (values: unknown): string => {
|
|
212
|
+
return (values as Array<string>).join(" | ").toLowerCase();
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
beforeEach(() => {
|
|
216
|
+
setTeamsConfig(MOCK_TEAMS_CLIENT_ID, MOCK_TEAMS_CLIENT_SECRET);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
afterEach(() => {
|
|
220
|
+
jest.restoreAllMocks();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("bot messaging endpoint route registration", () => {
|
|
224
|
+
test("POST /microsoft-bot/messages is registered", () => {
|
|
225
|
+
/*
|
|
226
|
+
* The claim in the bug report was that this route had gone missing from
|
|
227
|
+
* the build. It had not, and this is the assertion that keeps it that way.
|
|
228
|
+
*/
|
|
229
|
+
expect(findRoute(MESSAGES_PATH, "post")).toBeDefined();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("GET /microsoft-bot/messages is registered", () => {
|
|
233
|
+
expect(findRoute(MESSAGES_PATH, "get")).toBeDefined();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("the GET and POST handlers are different functions", () => {
|
|
237
|
+
const getRoute: ExpressRouterLayer["route"] | undefined = findRoute(
|
|
238
|
+
MESSAGES_PATH,
|
|
239
|
+
"get",
|
|
240
|
+
);
|
|
241
|
+
const postRoute: ExpressRouterLayer["route"] | undefined = findRoute(
|
|
242
|
+
MESSAGES_PATH,
|
|
243
|
+
"post",
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
expect(getRoute?.stack[0]?.handle).not.toBe(postRoute?.stack[0]?.handle);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("the diagnostic GET is not registered for POST", () => {
|
|
250
|
+
const getRoute: ExpressRouterLayer["route"] | undefined = findRoute(
|
|
251
|
+
MESSAGES_PATH,
|
|
252
|
+
"get",
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
expect(getRoute?.methods["post"]).toBeFalsy();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("GET /microsoft-bot/test is still registered alongside it", () => {
|
|
259
|
+
expect(findRoute("/microsoft-bot/test", "get")).toBeDefined();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
/*
|
|
264
|
+
* The hazard this change introduces is exactly one: a GET handler sitting on
|
|
265
|
+
* the bot's own path. If it ever caught POSTs — someone "tidying" the pair into
|
|
266
|
+
* a router.all, or registering the GET above the POST with a next() — every
|
|
267
|
+
* Teams button tap and every chat installation event would be answered with a
|
|
268
|
+
* 405 explaining that the endpoint wants POST, which is the funniest possible
|
|
269
|
+
* way to break the integration and would look identical from inside Teams to
|
|
270
|
+
* the outage this whole change is about.
|
|
271
|
+
*
|
|
272
|
+
* Inspecting route.methods cannot see that: findRoute already selects the layer
|
|
273
|
+
* by method, and router.all() registers as `_all` rather than as each verb, so
|
|
274
|
+
* the tidied-up version would not even be found. Only dispatching through the
|
|
275
|
+
* real router settles it, so these drive router(req, res, next) directly and
|
|
276
|
+
* watch which handler runs.
|
|
277
|
+
*/
|
|
278
|
+
describe("dispatching through the real router", () => {
|
|
279
|
+
type DispatchResult = {
|
|
280
|
+
processBotActivityCalls: number;
|
|
281
|
+
statusCode: number;
|
|
282
|
+
fellThrough: boolean;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
type DispatchFunction = (method: string) => Promise<DispatchResult>;
|
|
286
|
+
|
|
287
|
+
const dispatch: DispatchFunction = async (
|
|
288
|
+
method: string,
|
|
289
|
+
): Promise<DispatchResult> => {
|
|
290
|
+
const result: DispatchResult = {
|
|
291
|
+
processBotActivityCalls: 0,
|
|
292
|
+
statusCode: 0,
|
|
293
|
+
fellThrough: false,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
/*
|
|
297
|
+
* Counted by hand rather than read off the spy: jest.spyOn's return type
|
|
298
|
+
* varies across jest versions, and naming it would make this suite's
|
|
299
|
+
* compilation depend on which one is installed. afterEach's
|
|
300
|
+
* restoreAllMocks puts the real implementation back.
|
|
301
|
+
*/
|
|
302
|
+
jest
|
|
303
|
+
.spyOn(MicrosoftTeamsUtil, "processBotActivity")
|
|
304
|
+
.mockImplementation(async (): Promise<void> => {
|
|
305
|
+
result.processBotActivityCalls++;
|
|
306
|
+
return undefined;
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const router: ExpressRouter = new MicrosoftTeamsAPI().getRouter();
|
|
310
|
+
|
|
311
|
+
const res: Partial<ExpressResponse> = {
|
|
312
|
+
setHeader(): ExpressResponse {
|
|
313
|
+
return res as ExpressResponse;
|
|
314
|
+
},
|
|
315
|
+
status(statusCode: number): ExpressResponse {
|
|
316
|
+
result.statusCode = statusCode;
|
|
317
|
+
return res as ExpressResponse;
|
|
318
|
+
},
|
|
319
|
+
json(): ExpressResponse {
|
|
320
|
+
return res as ExpressResponse;
|
|
321
|
+
},
|
|
322
|
+
send(): ExpressResponse {
|
|
323
|
+
return res as ExpressResponse;
|
|
324
|
+
},
|
|
325
|
+
end(): ExpressResponse {
|
|
326
|
+
return res as ExpressResponse;
|
|
327
|
+
},
|
|
328
|
+
} as unknown as Partial<ExpressResponse>;
|
|
329
|
+
|
|
330
|
+
const req: Record<string, unknown> = {
|
|
331
|
+
method: method,
|
|
332
|
+
url: MESSAGES_PATH,
|
|
333
|
+
originalUrl: MESSAGES_PATH,
|
|
334
|
+
baseUrl: "",
|
|
335
|
+
query: {},
|
|
336
|
+
headers: {},
|
|
337
|
+
body: {
|
|
338
|
+
type: "message",
|
|
339
|
+
channelData: { tenant: { id: "tenant-id" } },
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
await new Promise<void>((resolve: () => void) => {
|
|
344
|
+
(router as unknown as (...args: Array<unknown>) => void)(
|
|
345
|
+
req,
|
|
346
|
+
res,
|
|
347
|
+
(): void => {
|
|
348
|
+
// next() means no route in this router matched the request.
|
|
349
|
+
result.fellThrough = true;
|
|
350
|
+
resolve();
|
|
351
|
+
},
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
/*
|
|
355
|
+
* Both handlers answer synchronously up to their first await, so a
|
|
356
|
+
* macrotask hop is enough to let either finish before we assert.
|
|
357
|
+
*/
|
|
358
|
+
setTimeout(resolve, 0);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
jest.restoreAllMocks();
|
|
362
|
+
|
|
363
|
+
return result;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
test("a POST bot activity still reaches processBotActivity", async () => {
|
|
367
|
+
const result: DispatchResult = await dispatch("POST");
|
|
368
|
+
|
|
369
|
+
expect(result.processBotActivityCalls).toBe(1);
|
|
370
|
+
expect(result.fellThrough).toBe(false);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("a POST bot activity is never answered 405 by the diagnostic handler", async () => {
|
|
374
|
+
const result: DispatchResult = await dispatch("POST");
|
|
375
|
+
|
|
376
|
+
expect(result.statusCode).not.toBe(405);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test("a GET is answered 405 and never invokes processBotActivity", async () => {
|
|
380
|
+
const result: DispatchResult = await dispatch("GET");
|
|
381
|
+
|
|
382
|
+
expect(result.statusCode).toBe(405);
|
|
383
|
+
expect(result.processBotActivityCalls).toBe(0);
|
|
384
|
+
expect(result.fellThrough).toBe(false);
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe("GET /microsoft-bot/messages", () => {
|
|
389
|
+
test("responds 405, not 404", async () => {
|
|
390
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
391
|
+
|
|
392
|
+
expect(response.statusCode).toBe(405);
|
|
393
|
+
expect(response.statusCode).not.toBe(404);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("sets Allow: POST", async () => {
|
|
397
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
398
|
+
|
|
399
|
+
expect(response.headers["allow"]).toBe("POST");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("advertises POST in the body as well as the header", async () => {
|
|
403
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
404
|
+
const allow: Array<string> = response.body[
|
|
405
|
+
"allow"
|
|
406
|
+
] as unknown as Array<string>;
|
|
407
|
+
|
|
408
|
+
expect(Array.isArray(allow)).toBe(true);
|
|
409
|
+
expect(allow).toEqual(["POST"]);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("never presents itself as a not-found", async () => {
|
|
413
|
+
/*
|
|
414
|
+
* "Page not found" is what the old catch-all said, and saying it here
|
|
415
|
+
* would put the admin straight back into believing the route is gone.
|
|
416
|
+
*
|
|
417
|
+
* It is banned from the fields that describe THIS response, not from the
|
|
418
|
+
* whole body: ifYouGetA404InsteadOfThis quotes the catch-all's exact body
|
|
419
|
+
* on purpose, because that quoted string is how an admin tells OneUptime's
|
|
420
|
+
* own 404 (request arrived) from a proxy's 404 (request did not). Banning
|
|
421
|
+
* the substring outright would forbid the one place it belongs.
|
|
422
|
+
*/
|
|
423
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
424
|
+
|
|
425
|
+
const selfDescribingFields: Array<string> = [
|
|
426
|
+
"status",
|
|
427
|
+
"whatThisProves",
|
|
428
|
+
"whatThisDoesNotProve",
|
|
429
|
+
"nextStep",
|
|
430
|
+
];
|
|
431
|
+
|
|
432
|
+
selfDescribingFields.forEach((field: string) => {
|
|
433
|
+
const value: string = (response.body[field] as string).toLowerCase();
|
|
434
|
+
expect(value).not.toContain("page not found");
|
|
435
|
+
expect(value).not.toContain("not found -");
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("quotes the catch-all's body only inside the 404 guidance", async () => {
|
|
440
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
441
|
+
const on404: string = response.body["ifYouGetA404InsteadOfThis"] as string;
|
|
442
|
+
|
|
443
|
+
expect(on404).toContain(
|
|
444
|
+
'{"message":"Page not found - /api/microsoft-bot/messages"}',
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("states the endpoint exists and takes POST only", async () => {
|
|
449
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
450
|
+
const status: string = (response.body["status"] as string).toLowerCase();
|
|
451
|
+
|
|
452
|
+
expect(status).toContain("exists");
|
|
453
|
+
expect(status).toContain("post");
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("reports the messaging endpoint this deployment expects", async () => {
|
|
457
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
458
|
+
const messagingEndpoint: string = response.body[
|
|
459
|
+
"messagingEndpoint"
|
|
460
|
+
] as string;
|
|
461
|
+
|
|
462
|
+
expect(typeof messagingEndpoint).toBe("string");
|
|
463
|
+
expect(messagingEndpoint.endsWith("/microsoft-bot/messages")).toBe(true);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("points at /microsoft-bot/test as the next step", async () => {
|
|
467
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
468
|
+
const nextStep: string = response.body["nextStep"] as string;
|
|
469
|
+
|
|
470
|
+
expect(nextStep).toContain("/microsoft-bot/test");
|
|
471
|
+
expect(nextStep.toLowerCase()).toContain("bot id");
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
describe("the response separates what a 405 proves from what it does not", () => {
|
|
476
|
+
test("whatThisProves says the endpoint is registered and the request arrived", async () => {
|
|
477
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
478
|
+
const proves: string = (
|
|
479
|
+
response.body["whatThisProves"] as string
|
|
480
|
+
).toLowerCase();
|
|
481
|
+
|
|
482
|
+
expect(proves).toContain("registered");
|
|
483
|
+
expect(proves).toContain("not missing");
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("whatThisProves does not claim a 404 would have meant the request never arrived", async () => {
|
|
487
|
+
/*
|
|
488
|
+
* It said exactly that in an earlier draft, and it was false in the one
|
|
489
|
+
* case that matters. OneUptime's own catch-all answers an unmatched GET
|
|
490
|
+
* with 404 and the body {"message":"Page not found - <path>"} — 58 bytes
|
|
491
|
+
* for this path, which is precisely the `404 58` the reporter of issue
|
|
492
|
+
* #3488 pasted from their access log. Their request DID arrive. Shipping
|
|
493
|
+
* "a 404 means it never arrived" inside the endpoint built to end that
|
|
494
|
+
* misreading would have taught admins to distrust the one log line
|
|
495
|
+
* proving their host was reachable.
|
|
496
|
+
*/
|
|
497
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
498
|
+
const proves: string = (
|
|
499
|
+
response.body["whatThisProves"] as string
|
|
500
|
+
).toLowerCase();
|
|
501
|
+
|
|
502
|
+
expect(proves).not.toContain("never arrived");
|
|
503
|
+
expect(proves).not.toContain("would have meant");
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("a 404 is explained by its body, not by its status code alone", async () => {
|
|
507
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
508
|
+
const on404: string = (
|
|
509
|
+
response.body["ifYouGetA404InsteadOfThis"] as string
|
|
510
|
+
).toLowerCase();
|
|
511
|
+
|
|
512
|
+
expect(on404).toContain("body");
|
|
513
|
+
expect(on404).toContain("page not found");
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("the 404 guidance says OneUptime's own JSON body means the request DID arrive", async () => {
|
|
517
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
518
|
+
const on404: string = (
|
|
519
|
+
response.body["ifYouGetA404InsteadOfThis"] as string
|
|
520
|
+
).toLowerCase();
|
|
521
|
+
|
|
522
|
+
expect(on404).toContain("did arrive");
|
|
523
|
+
expect(on404).toMatch(/post-only|post only/);
|
|
524
|
+
expect(on404).toContain("/api");
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("the 404 guidance names the HTML error page as the opposite reading", async () => {
|
|
528
|
+
/*
|
|
529
|
+
* Both readings have to be present or the field just relocates the
|
|
530
|
+
* original error: a proxy's own 404 really does mean the request never
|
|
531
|
+
* reached OneUptime, and that is the case the admin must be able to tell
|
|
532
|
+
* apart from theirs.
|
|
533
|
+
*/
|
|
534
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
535
|
+
const on404: string = (
|
|
536
|
+
response.body["ifYouGetA404InsteadOfThis"] as string
|
|
537
|
+
).toLowerCase();
|
|
538
|
+
|
|
539
|
+
expect(on404).toContain("html");
|
|
540
|
+
expect(on404).toMatch(/proxy|nginx|ingress|load balancer/);
|
|
541
|
+
expect(on404).toContain("never reached oneuptime");
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("whatThisDoesNotProve names Azure Bot Service reachability", async () => {
|
|
545
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
546
|
+
const doesNotProve: string = (
|
|
547
|
+
response.body["whatThisDoesNotProve"] as string
|
|
548
|
+
).toLowerCase();
|
|
549
|
+
|
|
550
|
+
expect(doesNotProve).toContain("azure bot service");
|
|
551
|
+
expect(doesNotProve).toContain("reach");
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("the two claims are distinct fields, not one merged sentence", async () => {
|
|
555
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
556
|
+
|
|
557
|
+
expect(typeof response.body["whatThisProves"]).toBe("string");
|
|
558
|
+
expect(typeof response.body["whatThisDoesNotProve"]).toBe("string");
|
|
559
|
+
expect(response.body["whatThisProves"]).not.toBe(
|
|
560
|
+
response.body["whatThisDoesNotProve"],
|
|
561
|
+
);
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
describe("unable-to-reach-app guidance", () => {
|
|
566
|
+
test("is a non-empty ordered list", async () => {
|
|
567
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
568
|
+
const guidance: Array<string> = response.body[
|
|
569
|
+
"ifTeamsSaysUnableToReachApp"
|
|
570
|
+
] as unknown as Array<string>;
|
|
571
|
+
|
|
572
|
+
expect(Array.isArray(guidance)).toBe(true);
|
|
573
|
+
expect(guidance.length).toBeGreaterThan(0);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("leads with outbound alerts proving nothing about the bot endpoint", async () => {
|
|
577
|
+
/*
|
|
578
|
+
* This is the misreading that makes the integration look half-broken
|
|
579
|
+
* rather than network-broken: alert cards post fine because OneUptime
|
|
580
|
+
* calls Microsoft, and every inbound path is dead. It has to be first.
|
|
581
|
+
*/
|
|
582
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
583
|
+
const guidance: Array<string> = response.body[
|
|
584
|
+
"ifTeamsSaysUnableToReachApp"
|
|
585
|
+
] as unknown as Array<string>;
|
|
586
|
+
|
|
587
|
+
const first: string = guidance[0]!.toLowerCase();
|
|
588
|
+
|
|
589
|
+
expect(first).toContain("outbound");
|
|
590
|
+
expect(first).toMatch(/prove(s)? nothing|no inbound access/);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
test.each([
|
|
594
|
+
["the TLS chain requirement", ["publicly trusted certificate"]],
|
|
595
|
+
["public reachability of the host", ["private dns"]],
|
|
596
|
+
[
|
|
597
|
+
"grepping for the POST rather than the 404",
|
|
598
|
+
["post /api/microsoft-bot/messages"],
|
|
599
|
+
],
|
|
600
|
+
["the Azure Bot messaging endpoint setting", ["azure bot resource"]],
|
|
601
|
+
])("covers %s", async (_label: string, requiredFragments: Array<string>) => {
|
|
602
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
603
|
+
const joined: string = joinGuidance(
|
|
604
|
+
response.body["ifTeamsSaysUnableToReachApp"],
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
requiredFragments.forEach((fragment: string) => {
|
|
608
|
+
expect(joined).toContain(fragment);
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
test("explains why a failed TLS handshake leaves no trace in the access log", async () => {
|
|
613
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
614
|
+
const joined: string = joinGuidance(
|
|
615
|
+
response.body["ifTeamsSaysUnableToReachApp"],
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
expect(joined).toContain("access log");
|
|
619
|
+
expect(joined).toMatch(/before oneuptime ever sees|never got through/);
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test("names the three interactive paths that depend on inbound traffic", async () => {
|
|
623
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
624
|
+
const joined: string = joinGuidance(
|
|
625
|
+
response.body["ifTeamsSaysUnableToReachApp"],
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
expect(joined).toContain("card buttons");
|
|
629
|
+
expect(joined).toContain("bot commands");
|
|
630
|
+
expect(joined).toContain("chat registration");
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
describe("the status code cannot be talked out of being 405", () => {
|
|
635
|
+
test("?output-type=csv still returns 405 JSON", async () => {
|
|
636
|
+
/*
|
|
637
|
+
* Response.sendJsonObjectResponse answers this query parameter with a 200
|
|
638
|
+
* and a CSV body regardless of the status code it was given. On an
|
|
639
|
+
* endpoint whose entire value is its status code that would undo the fix,
|
|
640
|
+
* so the handler writes the response itself.
|
|
641
|
+
*/
|
|
642
|
+
const response: InvokedResponse = await callMessagesGet({
|
|
643
|
+
"output-type": "csv",
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
expect(response.statusCode).toBe(405);
|
|
647
|
+
expect(response.sentAsCsv).toBe(false);
|
|
648
|
+
expect(response.body["status"]).toBeDefined();
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
test("an unrelated query parameter does not change the response", async () => {
|
|
652
|
+
const withQuery: InvokedResponse = await callMessagesGet({
|
|
653
|
+
foo: "bar",
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
expect(withQuery.statusCode).toBe(405);
|
|
657
|
+
expect(withQuery.body["status"]).toBeDefined();
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
describe("the diagnostic answer does not depend on Teams being configured", () => {
|
|
662
|
+
test.each([
|
|
663
|
+
["client id missing", null, MOCK_TEAMS_CLIENT_SECRET],
|
|
664
|
+
["client secret missing", MOCK_TEAMS_CLIENT_ID, null],
|
|
665
|
+
["both missing", null, null],
|
|
666
|
+
["both empty strings", "", ""],
|
|
667
|
+
])(
|
|
668
|
+
"still answers 405 with guidance when %s",
|
|
669
|
+
async (
|
|
670
|
+
_label: string,
|
|
671
|
+
clientId: string | null,
|
|
672
|
+
clientSecret: string | null,
|
|
673
|
+
) => {
|
|
674
|
+
/*
|
|
675
|
+
* An admin whose credentials are wrong still needs to be told the route
|
|
676
|
+
* exists. Gating this response on configuration would put them back in
|
|
677
|
+
* front of a 404-shaped answer at exactly the wrong moment.
|
|
678
|
+
*/
|
|
679
|
+
setTeamsConfig(clientId, clientSecret);
|
|
680
|
+
|
|
681
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
682
|
+
|
|
683
|
+
expect(response.statusCode).toBe(405);
|
|
684
|
+
expect(response.headers["allow"]).toBe("POST");
|
|
685
|
+
expect(response.body["status"]).toBeDefined();
|
|
686
|
+
expect(
|
|
687
|
+
(
|
|
688
|
+
response.body[
|
|
689
|
+
"ifTeamsSaysUnableToReachApp"
|
|
690
|
+
] as unknown as Array<string>
|
|
691
|
+
).length,
|
|
692
|
+
).toBeGreaterThan(0);
|
|
693
|
+
},
|
|
694
|
+
);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
describe("secret handling", () => {
|
|
698
|
+
test.each([
|
|
699
|
+
["fully configured", MOCK_TEAMS_CLIENT_ID, MOCK_TEAMS_CLIENT_SECRET],
|
|
700
|
+
["client id missing", null, MOCK_TEAMS_CLIENT_SECRET],
|
|
701
|
+
])(
|
|
702
|
+
"never echoes the client secret (%s)",
|
|
703
|
+
async (
|
|
704
|
+
_label: string,
|
|
705
|
+
clientId: string | null,
|
|
706
|
+
clientSecret: string | null,
|
|
707
|
+
) => {
|
|
708
|
+
setTeamsConfig(clientId, clientSecret);
|
|
709
|
+
|
|
710
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
711
|
+
const serialized: string = JSON.stringify(response.body);
|
|
712
|
+
|
|
713
|
+
expect(serialized).not.toContain(MOCK_TEAMS_CLIENT_SECRET);
|
|
714
|
+
expect(serialized.toLowerCase()).not.toContain("clientsecret");
|
|
715
|
+
},
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
test("does not echo the client id either — this route identifies the endpoint, not the bot", async () => {
|
|
719
|
+
/*
|
|
720
|
+
* /microsoft-bot/test is the place that returns botId, behind the same
|
|
721
|
+
* deployment but as a deliberate answer to "which package should be
|
|
722
|
+
* installed". This route is reachable by anyone who can reach the host,
|
|
723
|
+
* so it stays about the endpoint.
|
|
724
|
+
*/
|
|
725
|
+
const response: InvokedResponse = await callMessagesGet();
|
|
726
|
+
|
|
727
|
+
expect(JSON.stringify(response.body)).not.toContain(MOCK_TEAMS_CLIENT_ID);
|
|
728
|
+
});
|
|
729
|
+
});
|