@oneuptime/common 12.0.11 → 12.0.13
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/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +32 -5
- package/Models/DatabaseModels/NetworkDevice.ts +64 -0
- package/Models/DatabaseModels/StatusPage.ts +59 -0
- package/Models/DatabaseModels/StatusPageOidc.ts +2 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +19 -2
- package/Server/API/AIChatAPI.ts +55 -56
- package/Server/API/CommonAPI.ts +89 -0
- package/Server/API/MicrosoftTeamsAPI.ts +20 -9
- package/Server/API/SlackAPI.ts +13 -10
- package/Server/API/StatusPageAPI.ts +2197 -2128
- package/Server/API/TelemetryAPI.ts +72 -3
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.ts +30 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/UserAuthorization.ts +11 -2
- package/Server/Services/ApiKeyPermissionService.ts +116 -2
- package/Server/Services/DatabaseService.ts +17 -6
- package/Server/Services/LogAggregationService.ts +34 -0
- package/Server/Services/LogService.ts +21 -0
- package/Server/Services/NetworkSiteService.ts +12 -0
- package/Server/Services/SpanService.ts +21 -0
- package/Server/Services/TraceAggregationService.ts +15 -0
- package/Server/Services/WorkspaceNotificationRuleService.ts +172 -220
- package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
- package/Server/Types/Database/Permissions/AccessControlPermission.ts +47 -16
- package/Server/Types/Workflow/Components/Conditions/IfElse.ts +9 -0
- package/Server/Types/Workflow/Components/JavaScript.ts +9 -0
- package/Server/Utils/APIKey/AccessPermission.ts +16 -40
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +144 -0
- package/Server/Utils/LogRedaction.ts +576 -0
- package/Server/Utils/Logger.ts +123 -46
- package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +94 -23
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +12 -0
- package/Server/Utils/Monitor/MonitorResource.ts +29 -9
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +28 -7
- package/Server/Utils/SessionReplay/SessionReplayGateCacheStore.ts +56 -10
- package/Server/Utils/StartServer.ts +30 -9
- package/Server/Utils/StatusPageSearchEngineIndexing.ts +33 -0
- package/Server/Utils/Telemetry/ResourceEntityFilter.ts +441 -0
- package/Server/Utils/VM/VMRunner.ts +693 -874
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +488 -46
- package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +300 -0
- package/Tests/App/StatusPage/StatusPageIndexPageRobotsMeta.test.ts +130 -0
- package/Tests/Models/DatabaseModels/DatabaseBaseModelToJSONObjectCache.test.ts +180 -0
- package/Tests/Models/StatusPageEnableSearchEngineIndexing.test.ts +157 -0
- package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +23 -1
- package/Tests/Server/API/AIChatRouteAuthorization.test.ts +785 -0
- package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +21 -8
- package/Tests/Server/API/CommonAPIAuthGuard.test.ts +290 -0
- package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +7 -1
- package/Tests/Server/API/StatusPageOverviewCache.test.ts +474 -0
- package/Tests/Server/API/StatusPageSeoSearchEngineIndexing.test.ts +189 -0
- package/Tests/Server/Infrastructure/InMemoryTTLCache.test.ts +158 -0
- package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +9 -60
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +24 -15
- package/Tests/Server/Middleware/UserAuthorization.test.ts +79 -0
- package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +332 -0
- package/Tests/Server/Services/ApiKeyPermissionService.test.ts +347 -0
- package/Tests/Server/Services/DatabaseServiceUpdateDebugLogging.test.ts +282 -0
- package/Tests/Server/Services/NetworkSiteService.test.ts +56 -2
- package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +267 -0
- package/Tests/Server/Services/WorkspaceNotificationRuleTestRuleChannels.test.ts +702 -0
- package/Tests/Server/Types/Database/Permissions/AccessControlPermission.test.ts +165 -0
- package/Tests/Server/Types/Workflow/Components/CustomCodeScriptError.test.ts +121 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +48 -36
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +213 -0
- package/Tests/Server/Utils/LogRedaction.test.ts +415 -0
- package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +245 -0
- package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +374 -4
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorJavascriptExpression.test.ts +92 -0
- package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +455 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceReachabilityRoundTrip.test.ts +369 -0
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +86 -8
- package/Tests/Server/Utils/PrivacyFilterUtil.test.ts +64 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCacheStore.test.ts +207 -0
- package/Tests/Server/Utils/StartServerBodyVerify.test.ts +335 -0
- package/Tests/Server/Utils/StatusPageSearchEngineIndexing.test.ts +60 -0
- package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +435 -0
- package/Tests/Server/Utils/VM/VMRunnerIsolation.test.ts +385 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +3 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +607 -62
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsInstalledTeamIdSpace.test.ts +778 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +44 -10
- package/Tests/Types/API/HostnameFromAuthority.test.ts +164 -0
- package/Tests/Types/Database/AccessControl/ColumnAccessControlCache.test.ts +344 -0
- package/Tests/Types/Database/DatabasePropertyFindOperator.test.ts +518 -0
- package/Tests/Types/Database/TableColumnMetadataCache.test.ts +286 -0
- package/Tests/Types/Monitor/MonitorStep.test.ts +100 -0
- package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +360 -0
- package/Tests/Types/StatusPage/SearchEngineIndexing.test.ts +86 -0
- package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +254 -0
- package/Tests/UI/Components/CardSelect.test.tsx +1021 -0
- package/Tests/UI/Components/MasterPage.test.tsx +36 -1
- package/Tests/Utils/Monitor/MonitorMetricType.test.ts +66 -1
- package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +115 -6
- package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +610 -0
- package/Tests/Utils/NetworkSite/SiteStatusRollupUtil.test.ts +114 -45
- package/Types/API/Hostname.ts +79 -0
- package/Types/Database/AccessControl/ColumnAccessControl.ts +33 -11
- package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +33 -11
- package/Types/Database/DatabaseProperty.ts +38 -0
- package/Types/Database/TableColumn.ts +34 -7
- package/Types/Events/Recurring.ts +16 -1
- package/Types/Monitor/CustomCodeMonitor/CustomCodeMonitorResponse.ts +14 -2
- package/Types/Monitor/MonitorStep.ts +13 -1
- package/Types/Monitor/MonitorType.ts +345 -40
- package/Types/Monitor/SSLMonitor/SslMonitorResponse.ts +43 -0
- package/Types/StatusPage/SearchEngineIndexing.ts +61 -0
- package/Types/Telemetry/ResourceEntityFacet.ts +211 -0
- package/UI/Components/CardSelect/CardSelect.tsx +714 -105
- package/UI/Components/EmptyState/EmptyState.tsx +8 -1
- package/UI/Components/Footer/Footer.tsx +32 -6
- package/UI/Components/Forms/Fields/FormField.tsx +3 -0
- package/UI/Components/Forms/Types/Field.ts +8 -0
- package/UI/Components/Loader/PageLoader.tsx +29 -8
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +26 -4
- package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +17 -1
- package/UI/Components/MasterPage/MasterPage.tsx +24 -11
- package/Utils/Monitor/MonitorMetricType.ts +16 -1
- package/Utils/Monitor/NetworkTopologyUtil.ts +60 -8
- package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +262 -0
- package/Utils/NetworkSite/SiteStatusRollupUtil.ts +41 -24
- package/Utils/Telemetry/CrossSignalScope.ts +75 -4
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +20 -4
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +66 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +60 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageOidc.js +2 -0
- package/build/dist/Models/DatabaseModels/StatusPageOidc.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +48 -35
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/CommonAPI.js +59 -0
- package/build/dist/Server/API/CommonAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +20 -6
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +13 -10
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +745 -673
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +34 -3
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.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/UserAuthorization.js +6 -2
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Services/ApiKeyPermissionService.js +90 -1
- package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +15 -6
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +2 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/LogService.js +16 -0
- package/build/dist/Server/Services/LogService.js.map +1 -1
- package/build/dist/Server/Services/NetworkSiteService.js +12 -0
- package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
- package/build/dist/Server/Services/SpanService.js +16 -0
- package/build/dist/Server/Services/SpanService.js.map +1 -1
- package/build/dist/Server/Services/TraceAggregationService.js +2 -0
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +137 -164
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js +21 -13
- package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +8 -0
- package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/JavaScript.js +8 -0
- package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
- package/build/dist/Server/Utils/APIKey/AccessPermission.js +9 -32
- package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +87 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/LogRedaction.js +449 -0
- package/build/dist/Server/Utils/LogRedaction.js.map +1 -0
- package/build/dist/Server/Utils/Logger.js +98 -45
- package/build/dist/Server/Utils/Logger.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +65 -13
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +12 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +20 -3
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +27 -7
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js +29 -10
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +10 -7
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js +24 -0
- package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +311 -0
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -0
- package/build/dist/Server/Utils/VM/VMRunner.js +402 -525
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +366 -37
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/API/Hostname.js +63 -0
- package/build/dist/Types/API/Hostname.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +20 -6
- package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +20 -6
- package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
- package/build/dist/Types/Database/DatabaseProperty.js +38 -0
- package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
- package/build/dist/Types/Database/TableColumn.js +24 -6
- package/build/dist/Types/Database/TableColumn.js.map +1 -1
- package/build/dist/Types/Events/Recurring.js +13 -1
- package/build/dist/Types/Events/Recurring.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStep.js +9 -1
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +330 -31
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/StatusPage/SearchEngineIndexing.js +55 -0
- package/build/dist/Types/StatusPage/SearchEngineIndexing.js.map +1 -0
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js +156 -0
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -0
- package/build/dist/UI/Components/CardSelect/CardSelect.js +360 -40
- package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
- package/build/dist/UI/Components/EmptyState/EmptyState.js +1 -1
- package/build/dist/UI/Components/EmptyState/EmptyState.js.map +1 -1
- package/build/dist/UI/Components/Footer/Footer.js +7 -4
- package/build/dist/UI/Components/Footer/Footer.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Loader/PageLoader.js +15 -5
- package/build/dist/UI/Components/Loader/PageLoader.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +16 -3
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js +13 -1
- package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/MasterPage/MasterPage.js +10 -2
- package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js +13 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +34 -7
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +177 -0
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -0
- package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js +23 -27
- package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +44 -4
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/package.json +1 -1
|
@@ -63,7 +63,9 @@ jest.mock("botbuilder", () => {
|
|
|
63
63
|
};
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
import MicrosoftTeamsUtil
|
|
66
|
+
import MicrosoftTeamsUtil, {
|
|
67
|
+
MicrosoftTeamsAppInstallState,
|
|
68
|
+
} from "../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams";
|
|
67
69
|
import WorkspaceProjectAuthTokenService from "../../../../Server/Services/WorkspaceProjectAuthTokenService";
|
|
68
70
|
import WorkspaceProjectAuthToken, {
|
|
69
71
|
MicrosoftTeamsInstalledTeam,
|
|
@@ -73,6 +75,9 @@ import WorkspaceType from "../../../../Types/Workspace/WorkspaceType";
|
|
|
73
75
|
import ObjectID from "../../../../Types/ObjectID";
|
|
74
76
|
import BadDataException from "../../../../Types/Exception/BadDataException";
|
|
75
77
|
import { JSONObject } from "../../../../Types/JSON";
|
|
78
|
+
import API from "../../../../Utils/API";
|
|
79
|
+
import HTTPErrorResponse from "../../../../Types/API/HTTPErrorResponse";
|
|
80
|
+
import HTTPResponse from "../../../../Types/API/HTTPResponse";
|
|
76
81
|
import WorkspaceMessagePayload, {
|
|
77
82
|
WorkspacePayloadMarkdown,
|
|
78
83
|
} from "../../../../Types/Workspace/WorkspaceMessagePayload";
|
|
@@ -135,13 +140,24 @@ function baseMiscData(
|
|
|
135
140
|
} as MicrosoftTeamsMiscData;
|
|
136
141
|
}
|
|
137
142
|
|
|
143
|
+
/*
|
|
144
|
+
* A captured install, as captureTeamFromBotActivity writes it: keyed by the
|
|
145
|
+
* GRAPH team id, with the Bot Framework thread id kept alongside. `id` is the
|
|
146
|
+
* Graph id here because that is the only form the send path can match — see
|
|
147
|
+
* MicrosoftTeamsInstalledTeamIdSpace.test.ts, which derives these fixtures from
|
|
148
|
+
* the real capture path instead of hand-writing them.
|
|
149
|
+
*/
|
|
138
150
|
function buildInstalledTeam(data: {
|
|
139
151
|
id: string;
|
|
140
152
|
name?: string | undefined;
|
|
141
153
|
serviceUrl?: string | undefined;
|
|
154
|
+
graphTeamId?: string | undefined;
|
|
155
|
+
teamsThreadId?: string | undefined;
|
|
142
156
|
}): MicrosoftTeamsInstalledTeam {
|
|
143
157
|
const installedTeam: MicrosoftTeamsInstalledTeam = {
|
|
144
158
|
id: data.id,
|
|
159
|
+
graphTeamId: "graphTeamId" in data ? data.graphTeamId : data.id,
|
|
160
|
+
teamsThreadId: data.teamsThreadId || `19:${data.id}@thread.tacv2`,
|
|
145
161
|
name: data.name || "Engineering",
|
|
146
162
|
addedAt: "2026-07-27T00:00:00.000Z",
|
|
147
163
|
};
|
|
@@ -151,6 +167,33 @@ function buildInstalledTeam(data: {
|
|
|
151
167
|
return installedTeam;
|
|
152
168
|
}
|
|
153
169
|
|
|
170
|
+
/*
|
|
171
|
+
* Pin the Graph installed-apps check. The send path consults it whenever it has
|
|
172
|
+
* no local install record, so leaving it live would put every such test on the
|
|
173
|
+
* network. Unknown is the honest default for a tenant that has not granted the
|
|
174
|
+
* permission, and it is also the pre-existing behaviour (fall through to
|
|
175
|
+
* Microsoft), which keeps these suites about one thing at a time.
|
|
176
|
+
*/
|
|
177
|
+
let installStateSpy: jest.SpyInstance | undefined = undefined;
|
|
178
|
+
|
|
179
|
+
function mockInstallState(
|
|
180
|
+
state: MicrosoftTeamsAppInstallState = MicrosoftTeamsAppInstallState.Unknown,
|
|
181
|
+
): jest.SpyInstance {
|
|
182
|
+
installStateSpy = jest
|
|
183
|
+
.spyOn(MicrosoftTeamsUtil, "isAppInstalledInTeam")
|
|
184
|
+
.mockResolvedValue(state);
|
|
185
|
+
return installStateSpy;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/*
|
|
189
|
+
* The suite that tests isAppInstalledInTeam itself has to undo the file-wide
|
|
190
|
+
* stub, or it would only ever assert against the stub.
|
|
191
|
+
*/
|
|
192
|
+
function useRealInstallStateCheck(): void {
|
|
193
|
+
installStateSpy?.mockRestore();
|
|
194
|
+
installStateSpy = undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
154
197
|
function buildChannel(data?: {
|
|
155
198
|
id?: string | undefined;
|
|
156
199
|
name?: string | undefined;
|
|
@@ -276,6 +319,7 @@ afterEach(() => {
|
|
|
276
319
|
* mockResolvedValueOnce queues leak between tests unless reset here.
|
|
277
320
|
*/
|
|
278
321
|
beforeEach(() => {
|
|
322
|
+
mockInstallState();
|
|
279
323
|
(MessageFactory.text as jest.Mock).mockReset();
|
|
280
324
|
(MessageFactory.attachment as jest.Mock).mockReset();
|
|
281
325
|
(MessageFactory.attachment as jest.Mock).mockImplementation(
|
|
@@ -541,19 +585,46 @@ describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - shared channel refusal"
|
|
|
541
585
|
});
|
|
542
586
|
});
|
|
543
587
|
|
|
588
|
+
/*
|
|
589
|
+
* The preflight decides whether to refuse locally or hand the send to Microsoft.
|
|
590
|
+
*
|
|
591
|
+
* It used to refuse purely on the absence of a local install record, which is
|
|
592
|
+
* not evidence: installedTeams is only written from bot activities, so a team
|
|
593
|
+
* the app was added to while OneUptime was unreachable — or before install
|
|
594
|
+
* capture shipped — looks identical to a team it was never added to. Admins who
|
|
595
|
+
* had followed every documented step were told to follow them again.
|
|
596
|
+
*
|
|
597
|
+
* A refusal now requires a POSITIVE answer from Graph that the app is absent.
|
|
598
|
+
* Installed and Unknown both proceed and let Microsoft, the only real authority
|
|
599
|
+
* on roster membership, decide.
|
|
600
|
+
*/
|
|
544
601
|
describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - install preflight", () => {
|
|
545
|
-
test("
|
|
546
|
-
mockProjectAuth({
|
|
602
|
+
test("a captured install for the target team proceeds without asking Graph", async () => {
|
|
603
|
+
mockProjectAuth({
|
|
604
|
+
installedTeams: {
|
|
605
|
+
[TEAM_ID]: buildInstalledTeam({ id: TEAM_ID }),
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
const installSpy: jest.SpyInstance = mockInstallState(
|
|
609
|
+
MicrosoftTeamsAppInstallState.NotInstalled,
|
|
610
|
+
);
|
|
547
611
|
const adapter: FakeBotAdapter = installFakeBotAdapter();
|
|
548
612
|
|
|
549
|
-
|
|
613
|
+
await sendCardToChannel();
|
|
550
614
|
|
|
551
615
|
expect(adapter.continueConversationAsync).toHaveBeenCalledTimes(1);
|
|
552
|
-
|
|
616
|
+
// The local record is the fast path; no Graph round trip per notification.
|
|
617
|
+
expect(installSpy).not.toHaveBeenCalled();
|
|
553
618
|
});
|
|
554
619
|
|
|
555
|
-
test("
|
|
556
|
-
mockProjectAuth({
|
|
620
|
+
test("several installed teams, one of which is the target, proceeds", async () => {
|
|
621
|
+
mockProjectAuth({
|
|
622
|
+
installedTeams: {
|
|
623
|
+
"team-0": buildInstalledTeam({ id: "team-0" }),
|
|
624
|
+
[TEAM_ID]: buildInstalledTeam({ id: TEAM_ID }),
|
|
625
|
+
"team-2": buildInstalledTeam({ id: "team-2" }),
|
|
626
|
+
},
|
|
627
|
+
});
|
|
557
628
|
const adapter: FakeBotAdapter = installFakeBotAdapter();
|
|
558
629
|
|
|
559
630
|
await sendCardToChannel();
|
|
@@ -561,12 +632,13 @@ describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - install preflight", ()
|
|
|
561
632
|
expect(adapter.continueConversationAsync).toHaveBeenCalledTimes(1);
|
|
562
633
|
});
|
|
563
634
|
|
|
564
|
-
test("
|
|
635
|
+
test("no local record + Graph says NOT installed refuses before the Bot Framework call", async () => {
|
|
565
636
|
mockProjectAuth({
|
|
566
637
|
installedTeams: {
|
|
567
638
|
"team-2": buildInstalledTeam({ id: "team-2", name: "Marketing" }),
|
|
568
639
|
},
|
|
569
640
|
});
|
|
641
|
+
mockInstallState(MicrosoftTeamsAppInstallState.NotInstalled);
|
|
570
642
|
const adapter: FakeBotAdapter = installFakeBotAdapter();
|
|
571
643
|
|
|
572
644
|
await expect(
|
|
@@ -584,12 +656,25 @@ describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - install preflight", ()
|
|
|
584
656
|
expect(adapter.continueConversationAsync).not.toHaveBeenCalled();
|
|
585
657
|
});
|
|
586
658
|
|
|
587
|
-
test("
|
|
588
|
-
mockProjectAuth({
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
659
|
+
test("the verified refusal is checked against the team the caller asked for", async () => {
|
|
660
|
+
mockProjectAuth({ installedTeams: {} });
|
|
661
|
+
const installSpy: jest.SpyInstance = mockInstallState(
|
|
662
|
+
MicrosoftTeamsAppInstallState.NotInstalled,
|
|
663
|
+
);
|
|
664
|
+
installFakeBotAdapter();
|
|
665
|
+
|
|
666
|
+
await expect(sendCardToChannel({ teamId: "team-99" })).rejects.toThrow(
|
|
667
|
+
/is not installed in the Microsoft Teams team/,
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
expect(installSpy).toHaveBeenCalledWith(
|
|
671
|
+
expect.objectContaining({ teamId: "team-99" }),
|
|
672
|
+
);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
test("no local record + Graph says installed proceeds to the send", async () => {
|
|
676
|
+
mockProjectAuth({ installedTeams: {} });
|
|
677
|
+
mockInstallState(MicrosoftTeamsAppInstallState.Installed);
|
|
593
678
|
const adapter: FakeBotAdapter = installFakeBotAdapter();
|
|
594
679
|
|
|
595
680
|
await sendCardToChannel();
|
|
@@ -597,14 +682,31 @@ describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - install preflight", ()
|
|
|
597
682
|
expect(adapter.continueConversationAsync).toHaveBeenCalledTimes(1);
|
|
598
683
|
});
|
|
599
684
|
|
|
600
|
-
test("
|
|
685
|
+
test("no local record + Graph cannot tell us proceeds rather than accusing", async () => {
|
|
601
686
|
mockProjectAuth({
|
|
602
687
|
installedTeams: {
|
|
603
|
-
"team-0": buildInstalledTeam({ id: "team-0" }),
|
|
604
|
-
[TEAM_ID]: buildInstalledTeam({ id: TEAM_ID }),
|
|
605
688
|
"team-2": buildInstalledTeam({ id: "team-2" }),
|
|
606
689
|
},
|
|
607
690
|
});
|
|
691
|
+
mockInstallState(MicrosoftTeamsAppInstallState.Unknown);
|
|
692
|
+
const adapter: FakeBotAdapter = installFakeBotAdapter();
|
|
693
|
+
|
|
694
|
+
await sendCardToChannel({ teamId: TEAM_ID });
|
|
695
|
+
|
|
696
|
+
expect(adapter.continueConversationAsync).toHaveBeenCalledTimes(1);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
test("installedTeams undefined (legacy workspace) with an unknown install state does NOT block the send", async () => {
|
|
700
|
+
mockProjectAuth({ miscData: baseMiscData() });
|
|
701
|
+
const adapter: FakeBotAdapter = installFakeBotAdapter();
|
|
702
|
+
|
|
703
|
+
await sendCardToChannel();
|
|
704
|
+
|
|
705
|
+
expect(adapter.continueConversationAsync).toHaveBeenCalledTimes(1);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test("an EMPTY installedTeams map with an unknown install state does NOT block the send", async () => {
|
|
709
|
+
mockProjectAuth({ installedTeams: {} });
|
|
608
710
|
const adapter: FakeBotAdapter = installFakeBotAdapter();
|
|
609
711
|
|
|
610
712
|
await sendCardToChannel();
|
|
@@ -612,13 +714,23 @@ describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - install preflight", ()
|
|
|
612
714
|
expect(adapter.continueConversationAsync).toHaveBeenCalledTimes(1);
|
|
613
715
|
});
|
|
614
716
|
|
|
615
|
-
test("
|
|
717
|
+
test("a record written before graph ids were captured cannot vouch for a team, so Graph decides", async () => {
|
|
718
|
+
/*
|
|
719
|
+
* The legacy on-disk shape: keyed by the Bot Framework thread id, with no
|
|
720
|
+
* graphTeamId. It must not be mistaken for an install of the Graph team id
|
|
721
|
+
* that happens to be the rule's value, and it must not be treated as proof
|
|
722
|
+
* of anything either — Graph is asked, and here it says no.
|
|
723
|
+
*/
|
|
616
724
|
mockProjectAuth({
|
|
617
725
|
installedTeams: {
|
|
618
|
-
"team-
|
|
619
|
-
|
|
726
|
+
"19:team-aaa@thread.tacv2": {
|
|
727
|
+
id: "19:team-aaa@thread.tacv2",
|
|
728
|
+
name: "Engineering",
|
|
729
|
+
addedAt: "2026-07-27T00:00:00.000Z",
|
|
730
|
+
} as MicrosoftTeamsInstalledTeam,
|
|
620
731
|
},
|
|
621
732
|
});
|
|
733
|
+
mockInstallState(MicrosoftTeamsAppInstallState.NotInstalled);
|
|
622
734
|
const adapter: FakeBotAdapter = installFakeBotAdapter();
|
|
623
735
|
|
|
624
736
|
await expect(sendCardToChannel({ teamId: TEAM_ID })).rejects.toThrow(
|
|
@@ -627,12 +739,29 @@ describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - install preflight", ()
|
|
|
627
739
|
expect(adapter.continueConversationAsync).not.toHaveBeenCalled();
|
|
628
740
|
});
|
|
629
741
|
|
|
630
|
-
test("
|
|
742
|
+
test("a legacy record does not block a send Graph is willing to allow", async () => {
|
|
743
|
+
mockProjectAuth({
|
|
744
|
+
installedTeams: {
|
|
745
|
+
"19:team-aaa@thread.tacv2": {
|
|
746
|
+
id: "19:team-aaa@thread.tacv2",
|
|
747
|
+
} as MicrosoftTeamsInstalledTeam,
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
mockInstallState(MicrosoftTeamsAppInstallState.Installed);
|
|
751
|
+
const adapter: FakeBotAdapter = installFakeBotAdapter();
|
|
752
|
+
|
|
753
|
+
await sendCardToChannel({ teamId: TEAM_ID });
|
|
754
|
+
|
|
755
|
+
expect(adapter.continueConversationAsync).toHaveBeenCalledTimes(1);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
test("the graph team id match is exact — a differently-cased record is not this team", async () => {
|
|
631
759
|
mockProjectAuth({
|
|
632
760
|
installedTeams: {
|
|
633
761
|
"TEAM-1": buildInstalledTeam({ id: "TEAM-1" }),
|
|
634
762
|
},
|
|
635
763
|
});
|
|
764
|
+
mockInstallState(MicrosoftTeamsAppInstallState.NotInstalled);
|
|
636
765
|
const adapter: FakeBotAdapter = installFakeBotAdapter();
|
|
637
766
|
|
|
638
767
|
await expect(sendCardToChannel({ teamId: "team-1" })).rejects.toThrow(
|
|
@@ -641,12 +770,9 @@ describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - install preflight", ()
|
|
|
641
770
|
expect(adapter.continueConversationAsync).not.toHaveBeenCalled();
|
|
642
771
|
});
|
|
643
772
|
|
|
644
|
-
test("the
|
|
645
|
-
mockProjectAuth({
|
|
646
|
-
|
|
647
|
-
"team-2": buildInstalledTeam({ id: "team-2" }),
|
|
648
|
-
},
|
|
649
|
-
});
|
|
773
|
+
test("the verified refusal for a PRIVATE channel gives the channel-install instructions", async () => {
|
|
774
|
+
mockProjectAuth({ installedTeams: {} });
|
|
775
|
+
mockInstallState(MicrosoftTeamsAppInstallState.NotInstalled);
|
|
650
776
|
installFakeBotAdapter();
|
|
651
777
|
|
|
652
778
|
await expect(
|
|
@@ -666,29 +792,25 @@ describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - install preflight", ()
|
|
|
666
792
|
);
|
|
667
793
|
});
|
|
668
794
|
|
|
669
|
-
test("the
|
|
670
|
-
mockProjectAuth({
|
|
671
|
-
|
|
672
|
-
"team-2": buildInstalledTeam({ id: "team-2" }),
|
|
673
|
-
},
|
|
674
|
-
});
|
|
795
|
+
test("the verified refusal is a BadDataException", async () => {
|
|
796
|
+
mockProjectAuth({ installedTeams: {} });
|
|
797
|
+
mockInstallState(MicrosoftTeamsAppInstallState.NotInstalled);
|
|
675
798
|
installFakeBotAdapter();
|
|
676
799
|
|
|
677
800
|
await expect(sendCardToChannel()).rejects.toBeInstanceOf(BadDataException);
|
|
678
801
|
});
|
|
679
802
|
|
|
680
803
|
test("the preflight never runs when the integration is missing entirely", async () => {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
.
|
|
684
|
-
|
|
804
|
+
mockProjectAuth({ miscData: null });
|
|
805
|
+
const installSpy: jest.SpyInstance = mockInstallState(
|
|
806
|
+
MicrosoftTeamsAppInstallState.NotInstalled,
|
|
807
|
+
);
|
|
808
|
+
installFakeBotAdapter();
|
|
685
809
|
|
|
686
810
|
await expect(sendCardToChannel()).rejects.toThrow(
|
|
687
|
-
|
|
688
|
-
"Microsoft Teams integration not found for this project",
|
|
689
|
-
),
|
|
811
|
+
/Microsoft Teams integration not found/,
|
|
690
812
|
);
|
|
691
|
-
expect(
|
|
813
|
+
expect(installSpy).not.toHaveBeenCalled();
|
|
692
814
|
});
|
|
693
815
|
});
|
|
694
816
|
|
|
@@ -822,9 +944,26 @@ describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - conversation reference"
|
|
|
822
944
|
});
|
|
823
945
|
});
|
|
824
946
|
|
|
947
|
+
/*
|
|
948
|
+
* How Microsoft's roster rejection is worded back to the admin.
|
|
949
|
+
*
|
|
950
|
+
* The rewrite used to assert "the app is not installed in the team" for every
|
|
951
|
+
* roster rejection. That is one cause among several, and for the customer who
|
|
952
|
+
* reported this it was the wrong one — the app was in the team's app list and
|
|
953
|
+
* the bot answered @mentions in the very channel that failed. The rewrite is
|
|
954
|
+
* now conditioned on what the preflight actually established: only a verified
|
|
955
|
+
* absence gets the install instructions, and anything else gets the causes that
|
|
956
|
+
* apply to an app which IS present, with Microsoft's own words attached.
|
|
957
|
+
*/
|
|
825
958
|
describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - roster error translation", () => {
|
|
826
|
-
test("
|
|
827
|
-
|
|
959
|
+
test("a VERIFIED absent install still gets the parent-team install instructions", async () => {
|
|
960
|
+
/*
|
|
961
|
+
* Graph is asked before the send here and says the app is absent, but the
|
|
962
|
+
* refusal happens up front, so this asserts the message an admin who really
|
|
963
|
+
* has not added the app sees.
|
|
964
|
+
*/
|
|
965
|
+
mockProjectAuth({ installedTeams: {} });
|
|
966
|
+
mockInstallState(MicrosoftTeamsAppInstallState.NotInstalled);
|
|
828
967
|
installFakeBotAdapter({ adapterError: new Error(ROSTER_ERROR_TEXT) });
|
|
829
968
|
|
|
830
969
|
await expect(
|
|
@@ -844,41 +983,156 @@ describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - roster error translatio
|
|
|
844
983
|
);
|
|
845
984
|
});
|
|
846
985
|
|
|
847
|
-
test("
|
|
986
|
+
test("an UNVERIFIED roster rejection does not claim the app is missing", async () => {
|
|
848
987
|
mockProjectAuth();
|
|
988
|
+
mockInstallState(MicrosoftTeamsAppInstallState.Unknown);
|
|
849
989
|
installFakeBotAdapter({ adapterError: new Error(ROSTER_ERROR_TEXT) });
|
|
850
990
|
|
|
851
991
|
let thrown: unknown = undefined;
|
|
852
992
|
try {
|
|
853
|
-
await sendCardToChannel(
|
|
993
|
+
await sendCardToChannel({
|
|
994
|
+
workspaceChannel: buildChannel({ name: "General" }),
|
|
995
|
+
});
|
|
854
996
|
} catch (err) {
|
|
855
997
|
thrown = err;
|
|
856
998
|
}
|
|
857
999
|
|
|
858
1000
|
expect(thrown).toBeInstanceOf(BadDataException);
|
|
859
|
-
expect((thrown as Error).message).not.toContain(
|
|
860
|
-
|
|
1001
|
+
expect((thrown as Error).message).not.toContain(
|
|
1002
|
+
"The OneUptime app is not installed in the Microsoft Teams team",
|
|
1003
|
+
);
|
|
1004
|
+
expect((thrown as Error).message).toContain("Likely causes");
|
|
861
1005
|
});
|
|
862
1006
|
|
|
863
|
-
test("
|
|
1007
|
+
test("an unverified rejection names the channel and quotes Microsoft", async () => {
|
|
864
1008
|
mockProjectAuth();
|
|
1009
|
+
mockInstallState(MicrosoftTeamsAppInstallState.Unknown);
|
|
1010
|
+
installFakeBotAdapter({ adapterError: new Error(ROSTER_ERROR_TEXT) });
|
|
1011
|
+
|
|
1012
|
+
let thrown: unknown = undefined;
|
|
1013
|
+
try {
|
|
1014
|
+
await sendCardToChannel({
|
|
1015
|
+
workspaceChannel: buildChannel({ name: "test public channel" }),
|
|
1016
|
+
});
|
|
1017
|
+
} catch (err) {
|
|
1018
|
+
thrown = err;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
expect((thrown as Error).message).toContain('"test public channel"');
|
|
1022
|
+
// The raw error is the one thing support needs and the rewrite used to eat.
|
|
1023
|
+
expect((thrown as Error).message).toContain(ROSTER_ERROR_TEXT);
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
test("when the app IS known installed, the rejection points at the bot id instead", async () => {
|
|
1027
|
+
/*
|
|
1028
|
+
* The reporter's exact situation: a real install, and a roster rejection
|
|
1029
|
+
* anyway. The actionable cause is that the installed package's bot is not
|
|
1030
|
+
* the one this deployment authenticates as.
|
|
1031
|
+
*/
|
|
1032
|
+
mockProjectAuth({
|
|
1033
|
+
installedTeams: {
|
|
1034
|
+
[TEAM_ID]: buildInstalledTeam({ id: TEAM_ID }),
|
|
1035
|
+
},
|
|
1036
|
+
});
|
|
1037
|
+
mockInstallState(MicrosoftTeamsAppInstallState.Installed);
|
|
1038
|
+
installFakeBotAdapter({ adapterError: new Error(ROSTER_ERROR_TEXT) });
|
|
1039
|
+
|
|
1040
|
+
let thrown: unknown = undefined;
|
|
1041
|
+
try {
|
|
1042
|
+
await sendCardToChannel({ teamId: TEAM_ID });
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
thrown = err;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
expect((thrown as Error).message).toContain(
|
|
1048
|
+
"MICROSOFT_TEAMS_APP_CLIENT_ID",
|
|
1049
|
+
);
|
|
1050
|
+
expect((thrown as Error).message).not.toContain(
|
|
1051
|
+
"The OneUptime app is not installed in the Microsoft Teams team",
|
|
1052
|
+
);
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
test("a STALE local install record is re-checked, and a confirmed removal gets the install instructions", async () => {
|
|
1056
|
+
/*
|
|
1057
|
+
* The local record let this send skip the preflight, but the app has since
|
|
1058
|
+
* been removed from the team and no uninstall event reached us. Wording the
|
|
1059
|
+
* error off the stale record would blame the bot id for a genuinely missing
|
|
1060
|
+
* install, so Graph is asked again on the failure path.
|
|
1061
|
+
*/
|
|
1062
|
+
mockProjectAuth({
|
|
1063
|
+
installedTeams: {
|
|
1064
|
+
[TEAM_ID]: buildInstalledTeam({ id: TEAM_ID }),
|
|
1065
|
+
},
|
|
1066
|
+
});
|
|
1067
|
+
const installSpy: jest.SpyInstance = mockInstallState(
|
|
1068
|
+
MicrosoftTeamsAppInstallState.NotInstalled,
|
|
1069
|
+
);
|
|
865
1070
|
installFakeBotAdapter({ adapterError: new Error(ROSTER_ERROR_TEXT) });
|
|
866
1071
|
|
|
867
1072
|
await expect(
|
|
868
1073
|
sendCardToChannel({
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
membershipType: "private",
|
|
872
|
-
}),
|
|
1074
|
+
teamId: TEAM_ID,
|
|
1075
|
+
workspaceChannel: buildChannel({ name: "General" }),
|
|
873
1076
|
}),
|
|
874
1077
|
).rejects.toThrow(
|
|
875
1078
|
new BadDataException(
|
|
876
|
-
MicrosoftTeamsUtil.getBotNotInTeamMessage({
|
|
877
|
-
channelName: "Ops War Room",
|
|
878
|
-
membershipType: "private",
|
|
879
|
-
}),
|
|
1079
|
+
MicrosoftTeamsUtil.getBotNotInTeamMessage({ channelName: "General" }),
|
|
880
1080
|
),
|
|
881
1081
|
);
|
|
1082
|
+
|
|
1083
|
+
// Not consulted on the happy path; consulted once the send actually failed.
|
|
1084
|
+
expect(installSpy).toHaveBeenCalledTimes(1);
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
test("the Graph check is not repeated when the preflight already made it", async () => {
|
|
1088
|
+
mockProjectAuth({ installedTeams: {} });
|
|
1089
|
+
const installSpy: jest.SpyInstance = mockInstallState(
|
|
1090
|
+
MicrosoftTeamsAppInstallState.Unknown,
|
|
1091
|
+
);
|
|
1092
|
+
installFakeBotAdapter({ adapterError: new Error(ROSTER_ERROR_TEXT) });
|
|
1093
|
+
|
|
1094
|
+
await expect(sendCardToChannel()).rejects.toThrow(/Likely causes/);
|
|
1095
|
+
|
|
1096
|
+
expect(installSpy).toHaveBeenCalledTimes(1);
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
test("an unverified rejection on a private channel still leads with the channel install", async () => {
|
|
1100
|
+
mockProjectAuth();
|
|
1101
|
+
mockInstallState(MicrosoftTeamsAppInstallState.Unknown);
|
|
1102
|
+
installFakeBotAdapter({ adapterError: new Error(ROSTER_ERROR_TEXT) });
|
|
1103
|
+
|
|
1104
|
+
let thrown: unknown = undefined;
|
|
1105
|
+
try {
|
|
1106
|
+
await sendCardToChannel({
|
|
1107
|
+
workspaceChannel: buildChannel({
|
|
1108
|
+
name: "Ops War Room",
|
|
1109
|
+
membershipType: "private",
|
|
1110
|
+
}),
|
|
1111
|
+
});
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
thrown = err;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
expect((thrown as Error).message).toContain("Manage channel > Apps");
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
test("every roster rejection mentions the Azure Bot Teams channel", async () => {
|
|
1120
|
+
/*
|
|
1121
|
+
* A bot whose Azure resource has no Microsoft Teams channel enabled is
|
|
1122
|
+
* rejected exactly like an uninstalled app, and nothing in OneUptime can
|
|
1123
|
+
* detect it — so it has to be in the message.
|
|
1124
|
+
*/
|
|
1125
|
+
mockProjectAuth();
|
|
1126
|
+
installFakeBotAdapter({ adapterError: new Error(ROSTER_ERROR_TEXT) });
|
|
1127
|
+
|
|
1128
|
+
let thrown: unknown = undefined;
|
|
1129
|
+
try {
|
|
1130
|
+
await sendCardToChannel();
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
thrown = err;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
expect((thrown as Error).message).toContain("Microsoft Teams channel");
|
|
882
1136
|
});
|
|
883
1137
|
|
|
884
1138
|
test("a roster rejection raised inside the proactive callback is translated too", async () => {
|
|
@@ -887,9 +1141,7 @@ describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - roster error translatio
|
|
|
887
1141
|
sendActivityError: new Error(ROSTER_ERROR_TEXT),
|
|
888
1142
|
});
|
|
889
1143
|
|
|
890
|
-
await expect(sendCardToChannel()).rejects.toThrow(
|
|
891
|
-
/is not installed in the Microsoft Teams team/,
|
|
892
|
-
);
|
|
1144
|
+
await expect(sendCardToChannel()).rejects.toThrow(/Likely causes/);
|
|
893
1145
|
});
|
|
894
1146
|
|
|
895
1147
|
test("a roster rejection thrown as a plain string is translated too", async () => {
|
|
@@ -898,9 +1150,7 @@ describe("MicrosoftTeamsUtil.sendAdaptiveCardToChannel - roster error translatio
|
|
|
898
1150
|
adapterError: "The bot is not part of the conversation roster.",
|
|
899
1151
|
});
|
|
900
1152
|
|
|
901
|
-
await expect(sendCardToChannel()).rejects.toThrow(
|
|
902
|
-
/is not installed in the Microsoft Teams team/,
|
|
903
|
-
);
|
|
1153
|
+
await expect(sendCardToChannel()).rejects.toThrow(/Likely causes/);
|
|
904
1154
|
});
|
|
905
1155
|
|
|
906
1156
|
test("unrelated adapter errors propagate untouched", async () => {
|
|
@@ -1149,6 +1399,7 @@ describe("MicrosoftTeamsUtil.sendMessage - unresolvable channel destinations are
|
|
|
1149
1399
|
"team-2": buildInstalledTeam({ id: "team-2" }),
|
|
1150
1400
|
},
|
|
1151
1401
|
});
|
|
1402
|
+
mockInstallState(MicrosoftTeamsAppInstallState.NotInstalled);
|
|
1152
1403
|
const adapter: FakeBotAdapter = installFakeBotAdapter();
|
|
1153
1404
|
|
|
1154
1405
|
const response: WorkspaceSendMessageResponse = await callSendMessage(
|
|
@@ -1167,3 +1418,297 @@ describe("MicrosoftTeamsUtil.sendMessage - unresolvable channel destinations are
|
|
|
1167
1418
|
);
|
|
1168
1419
|
});
|
|
1169
1420
|
});
|
|
1421
|
+
|
|
1422
|
+
describe("MicrosoftTeamsUtil.getRosterRejectionMessage", () => {
|
|
1423
|
+
test("names the channel that was refused", () => {
|
|
1424
|
+
const message: string = MicrosoftTeamsUtil.getRosterRejectionMessage({
|
|
1425
|
+
channelName: "test public channel",
|
|
1426
|
+
installState: MicrosoftTeamsAppInstallState.Unknown,
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
expect(message).toContain('"test public channel"');
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
test("never tells an admin the app is not installed", () => {
|
|
1433
|
+
/*
|
|
1434
|
+
* The whole point of this message: it is used when we do NOT know, and the
|
|
1435
|
+
* old wording turned "we do not know" into a false statement of fact.
|
|
1436
|
+
*/
|
|
1437
|
+
for (const installState of [
|
|
1438
|
+
MicrosoftTeamsAppInstallState.Unknown,
|
|
1439
|
+
MicrosoftTeamsAppInstallState.Installed,
|
|
1440
|
+
]) {
|
|
1441
|
+
const message: string = MicrosoftTeamsUtil.getRosterRejectionMessage({
|
|
1442
|
+
channelName: "General",
|
|
1443
|
+
installState: installState,
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
expect(message).not.toContain(
|
|
1447
|
+
"The OneUptime app is not installed in the Microsoft Teams team",
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
test("a known install points at a bot id mismatch, not a missing install", () => {
|
|
1453
|
+
const message: string = MicrosoftTeamsUtil.getRosterRejectionMessage({
|
|
1454
|
+
channelName: "General",
|
|
1455
|
+
installState: MicrosoftTeamsAppInstallState.Installed,
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
expect(message).toContain("MICROSOFT_TEAMS_APP_CLIENT_ID");
|
|
1459
|
+
expect(message).toContain("the app is installed in this team");
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
test("an unknown install state still suggests adding the app", () => {
|
|
1463
|
+
const message: string = MicrosoftTeamsUtil.getRosterRejectionMessage({
|
|
1464
|
+
channelName: "General",
|
|
1465
|
+
installState: MicrosoftTeamsAppInstallState.Unknown,
|
|
1466
|
+
});
|
|
1467
|
+
|
|
1468
|
+
expect(message).toContain("Manage team > Apps");
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
test("a private channel leads with the channel-level install", () => {
|
|
1472
|
+
const message: string = MicrosoftTeamsUtil.getRosterRejectionMessage({
|
|
1473
|
+
channelName: "Ops War Room",
|
|
1474
|
+
membershipType: "private",
|
|
1475
|
+
installState: MicrosoftTeamsAppInstallState.Unknown,
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
expect(message).toContain("Manage channel > Apps");
|
|
1479
|
+
expect(message).not.toContain("Manage team > Apps");
|
|
1480
|
+
});
|
|
1481
|
+
|
|
1482
|
+
test("Microsoft's own wording is quoted when we have it", () => {
|
|
1483
|
+
const message: string = MicrosoftTeamsUtil.getRosterRejectionMessage({
|
|
1484
|
+
channelName: "General",
|
|
1485
|
+
installState: MicrosoftTeamsAppInstallState.Unknown,
|
|
1486
|
+
microsoftError: new Error(ROSTER_ERROR_TEXT),
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
expect(message).toContain(ROSTER_ERROR_TEXT);
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
test("a non-Error microsoftError is stringified rather than dropped", () => {
|
|
1493
|
+
const message: string = MicrosoftTeamsUtil.getRosterRejectionMessage({
|
|
1494
|
+
channelName: "General",
|
|
1495
|
+
installState: MicrosoftTeamsAppInstallState.Unknown,
|
|
1496
|
+
microsoftError: "BotNotInConversationRoster",
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
expect(message).toContain("BotNotInConversationRoster");
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
test("no trailing quote section when there is no Microsoft error to quote", () => {
|
|
1503
|
+
const message: string = MicrosoftTeamsUtil.getRosterRejectionMessage({
|
|
1504
|
+
channelName: "General",
|
|
1505
|
+
installState: MicrosoftTeamsAppInstallState.Unknown,
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
expect(message).not.toContain("Microsoft's response was");
|
|
1509
|
+
expect(message.endsWith(".")).toBe(true);
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
test("the Azure Bot Teams channel is always listed as a cause", () => {
|
|
1513
|
+
for (const installState of [
|
|
1514
|
+
MicrosoftTeamsAppInstallState.Unknown,
|
|
1515
|
+
MicrosoftTeamsAppInstallState.Installed,
|
|
1516
|
+
MicrosoftTeamsAppInstallState.NotInstalled,
|
|
1517
|
+
]) {
|
|
1518
|
+
const message: string = MicrosoftTeamsUtil.getRosterRejectionMessage({
|
|
1519
|
+
channelName: "General",
|
|
1520
|
+
installState: installState,
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1523
|
+
expect(message).toContain(
|
|
1524
|
+
"does not have the Microsoft Teams channel enabled",
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
/*
|
|
1531
|
+
* The Graph installed-apps check.
|
|
1532
|
+
*
|
|
1533
|
+
* Its contract is asymmetric on purpose: it may only answer NotInstalled when
|
|
1534
|
+
* Graph actually enumerated the team's apps and this deployment's app was not
|
|
1535
|
+
* among them. Any other outcome — a permission the tenant never granted, a
|
|
1536
|
+
* transport failure, an unconfigured client id — is Unknown, because reporting
|
|
1537
|
+
* those as NotInstalled is what sent the reporter of this bug in circles.
|
|
1538
|
+
*/
|
|
1539
|
+
describe("MicrosoftTeamsUtil.isAppInstalledInTeam", () => {
|
|
1540
|
+
function mockGraph(pages: Array<JSONObject | HTTPErrorResponse>): jest.Mock {
|
|
1541
|
+
const get: jest.Mock = jest.fn();
|
|
1542
|
+
|
|
1543
|
+
for (const page of pages) {
|
|
1544
|
+
if (page instanceof HTTPErrorResponse) {
|
|
1545
|
+
get.mockResolvedValueOnce(page);
|
|
1546
|
+
continue;
|
|
1547
|
+
}
|
|
1548
|
+
get.mockResolvedValueOnce({ data: page } as HTTPResponse<JSONObject>);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
jest.spyOn(API, "get").mockImplementation(get as any);
|
|
1552
|
+
return get;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
function callIsAppInstalled(
|
|
1556
|
+
teamId: string = TEAM_ID,
|
|
1557
|
+
): Promise<MicrosoftTeamsAppInstallState> {
|
|
1558
|
+
return MicrosoftTeamsUtil.isAppInstalledInTeam({
|
|
1559
|
+
authToken: "auth-token",
|
|
1560
|
+
projectId: ObjectID.generate(),
|
|
1561
|
+
teamId: teamId,
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
beforeEach(() => {
|
|
1566
|
+
useRealInstallStateCheck();
|
|
1567
|
+
jest
|
|
1568
|
+
.spyOn(MicrosoftTeamsUtil, "getValidAccessToken")
|
|
1569
|
+
.mockResolvedValue("app-access-token");
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
test("an installed app matching this deployment's client id is Installed", async () => {
|
|
1573
|
+
mockGraph([
|
|
1574
|
+
{
|
|
1575
|
+
value: [
|
|
1576
|
+
{ id: "install-1", teamsApp: { externalId: MOCK_APP_CLIENT_ID } },
|
|
1577
|
+
],
|
|
1578
|
+
},
|
|
1579
|
+
]);
|
|
1580
|
+
|
|
1581
|
+
await expect(callIsAppInstalled()).resolves.toBe(
|
|
1582
|
+
MicrosoftTeamsAppInstallState.Installed,
|
|
1583
|
+
);
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
test("matching is on externalId, so another vendor's OneUptime app is not ours", async () => {
|
|
1587
|
+
/*
|
|
1588
|
+
* This is the case the reporter's screenshots could not rule out: a tile
|
|
1589
|
+
* called "OneUptime · HackerBay Inc" in the team's app list which is the
|
|
1590
|
+
* public store package, whose bot is not this deployment's bot.
|
|
1591
|
+
*/
|
|
1592
|
+
mockGraph([
|
|
1593
|
+
{
|
|
1594
|
+
value: [
|
|
1595
|
+
{
|
|
1596
|
+
id: "install-1",
|
|
1597
|
+
teamsApp: {
|
|
1598
|
+
externalId: "99999999-9999-9999-9999-999999999999",
|
|
1599
|
+
displayName: "OneUptime",
|
|
1600
|
+
},
|
|
1601
|
+
},
|
|
1602
|
+
],
|
|
1603
|
+
},
|
|
1604
|
+
]);
|
|
1605
|
+
|
|
1606
|
+
await expect(callIsAppInstalled()).resolves.toBe(
|
|
1607
|
+
MicrosoftTeamsAppInstallState.NotInstalled,
|
|
1608
|
+
);
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
test("an empty app list is NotInstalled", async () => {
|
|
1612
|
+
mockGraph([{ value: [] }]);
|
|
1613
|
+
|
|
1614
|
+
await expect(callIsAppInstalled()).resolves.toBe(
|
|
1615
|
+
MicrosoftTeamsAppInstallState.NotInstalled,
|
|
1616
|
+
);
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
test("a response with no value array at all is NotInstalled", async () => {
|
|
1620
|
+
mockGraph([{}]);
|
|
1621
|
+
|
|
1622
|
+
await expect(callIsAppInstalled()).resolves.toBe(
|
|
1623
|
+
MicrosoftTeamsAppInstallState.NotInstalled,
|
|
1624
|
+
);
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
test("the app is found on a later page", async () => {
|
|
1628
|
+
const get: jest.Mock = mockGraph([
|
|
1629
|
+
{
|
|
1630
|
+
value: [{ id: "install-1", teamsApp: { externalId: "other-app" } }],
|
|
1631
|
+
"@odata.nextLink": "https://graph.microsoft.com/v1.0/page-2",
|
|
1632
|
+
},
|
|
1633
|
+
{
|
|
1634
|
+
value: [
|
|
1635
|
+
{ id: "install-2", teamsApp: { externalId: MOCK_APP_CLIENT_ID } },
|
|
1636
|
+
],
|
|
1637
|
+
},
|
|
1638
|
+
]);
|
|
1639
|
+
|
|
1640
|
+
await expect(callIsAppInstalled()).resolves.toBe(
|
|
1641
|
+
MicrosoftTeamsAppInstallState.Installed,
|
|
1642
|
+
);
|
|
1643
|
+
expect(get).toHaveBeenCalledTimes(2);
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
test("NotInstalled is only returned after every page was read", async () => {
|
|
1647
|
+
const get: jest.Mock = mockGraph([
|
|
1648
|
+
{
|
|
1649
|
+
value: [{ id: "install-1", teamsApp: { externalId: "other-app" } }],
|
|
1650
|
+
"@odata.nextLink": "https://graph.microsoft.com/v1.0/page-2",
|
|
1651
|
+
},
|
|
1652
|
+
{ value: [{ id: "install-2", teamsApp: { externalId: "another-app" } }] },
|
|
1653
|
+
]);
|
|
1654
|
+
|
|
1655
|
+
await expect(callIsAppInstalled()).resolves.toBe(
|
|
1656
|
+
MicrosoftTeamsAppInstallState.NotInstalled,
|
|
1657
|
+
);
|
|
1658
|
+
expect(get).toHaveBeenCalledTimes(2);
|
|
1659
|
+
});
|
|
1660
|
+
|
|
1661
|
+
test("a Graph error is Unknown, never NotInstalled", async () => {
|
|
1662
|
+
/*
|
|
1663
|
+
* The realistic case: a workspace connected before
|
|
1664
|
+
* TeamsAppInstallation.ReadForTeam.All was documented gets a 403 here. It
|
|
1665
|
+
* must not be told its app is missing.
|
|
1666
|
+
*/
|
|
1667
|
+
mockGraph([new HTTPErrorResponse(403, { error: "Forbidden" }, {})]);
|
|
1668
|
+
|
|
1669
|
+
await expect(callIsAppInstalled()).resolves.toBe(
|
|
1670
|
+
MicrosoftTeamsAppInstallState.Unknown,
|
|
1671
|
+
);
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
test("a thrown transport error is Unknown", async () => {
|
|
1675
|
+
jest.spyOn(API, "get").mockRejectedValue(new Error("socket hang up"));
|
|
1676
|
+
|
|
1677
|
+
await expect(callIsAppInstalled()).resolves.toBe(
|
|
1678
|
+
MicrosoftTeamsAppInstallState.Unknown,
|
|
1679
|
+
);
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
test("a token failure is Unknown", async () => {
|
|
1683
|
+
jest
|
|
1684
|
+
.spyOn(MicrosoftTeamsUtil, "getValidAccessToken")
|
|
1685
|
+
.mockRejectedValue(new Error("no token"));
|
|
1686
|
+
const get: jest.Mock = mockGraph([{ value: [] }]);
|
|
1687
|
+
|
|
1688
|
+
await expect(callIsAppInstalled()).resolves.toBe(
|
|
1689
|
+
MicrosoftTeamsAppInstallState.Unknown,
|
|
1690
|
+
);
|
|
1691
|
+
expect(get).not.toHaveBeenCalled();
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
test("the team id is scoped into the Graph URL and the app list is expanded", async () => {
|
|
1695
|
+
const get: jest.Mock = mockGraph([{ value: [] }]);
|
|
1696
|
+
|
|
1697
|
+
await callIsAppInstalled("team-abc");
|
|
1698
|
+
|
|
1699
|
+
const requestedUrl: string = get.mock.calls[0]![0].url.toString();
|
|
1700
|
+
expect(requestedUrl).toContain("/teams/team-abc/installedApps");
|
|
1701
|
+
// Without $expand there is no teamsApp to match externalId against.
|
|
1702
|
+
expect(requestedUrl).toContain("$expand=teamsApp");
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1705
|
+
test("the access token is sent as a bearer token", async () => {
|
|
1706
|
+
const get: jest.Mock = mockGraph([{ value: [] }]);
|
|
1707
|
+
|
|
1708
|
+
await callIsAppInstalled();
|
|
1709
|
+
|
|
1710
|
+
expect(get.mock.calls[0]![0].headers.Authorization).toBe(
|
|
1711
|
+
"Bearer app-access-token",
|
|
1712
|
+
);
|
|
1713
|
+
});
|
|
1714
|
+
});
|