@oneuptime/common 12.0.18 → 12.0.20
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/EnterpriseLicense.ts +23 -0
- package/Models/DatabaseModels/Index.ts +4 -2
- package/Models/DatabaseModels/Monitor.ts +39 -9
- package/Models/DatabaseModels/UserMicrosoftTeams.ts +344 -0
- package/Models/DatabaseModels/UserNotificationRule.ts +144 -8
- package/Models/DatabaseModels/UserNotificationSetting.ts +34 -0
- package/Models/DatabaseModels/UserOnCallLogTimeline.ts +95 -0
- package/Models/DatabaseModels/UserSlack.ts +341 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +9 -0
- package/Server/API/EnterpriseLicenseAPI.ts +94 -19
- package/Server/API/UserMicrosoftTeamsAPI.ts +127 -0
- package/Server/API/UserSlackAPI.ts +125 -0
- package/Server/EnvironmentConfig.ts +29 -107
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +117 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +197 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Infrastructure/Queue.ts +7 -0
- package/Server/Services/EnterpriseLicenseService.ts +60 -0
- package/Server/Services/Index.ts +6 -2
- package/Server/Services/LlmLogService.ts +25 -9
- package/Server/Services/OnCallReadinessService.ts +86 -2
- package/Server/Services/ProjectService.ts +171 -0
- package/Server/Services/UserMicrosoftTeamsService.ts +208 -0
- package/Server/Services/UserNotificationMethodAdminService.ts +167 -5
- package/Server/Services/UserNotificationRuleAdminService.ts +40 -6
- package/Server/Services/UserNotificationRuleService.ts +589 -11
- package/Server/Services/UserNotificationSettingService.ts +138 -0
- package/Server/Services/UserService.ts +28 -0
- package/Server/Services/UserSlackService.ts +218 -0
- package/Server/Services/WorkspaceProjectAuthTokenService.ts +67 -1
- package/Server/Services/WorkspaceUserAuthTokenService.ts +73 -0
- package/Server/Services/WorkspaceUserNotificationService.ts +190 -0
- package/Server/Utils/Marketing/MarketingEventUtil.ts +145 -0
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +114 -0
- package/Server/Utils/Monitor/MonitorLogUtil.ts +14 -1
- package/Server/Utils/Monitor/MonitorPayloadRedaction.ts +321 -0
- package/Server/Utils/Response.ts +2 -2
- package/Server/Utils/StartServer.ts +2 -2
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +191 -4
- package/Server/Views/Partials/AnalyticsConsent.ejs +533 -0
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +22 -14
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +22 -11
- package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +302 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
- package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -0
- package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +368 -0
- package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +37 -1
- package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +18 -10
- package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +27 -1
- package/Tests/Models/DatabaseModels/MonitorSecretKeyColumnAccessControl.test.ts +214 -0
- package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
- package/Tests/Server/API/OnCallReadinessAPI.test.ts +145 -26
- package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +2 -2
- package/Tests/Server/Services/AIServiceDailyBudget.test.ts +11 -1
- package/Tests/Server/Services/AdminRuleEditGuards.test.ts +22 -8
- package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +18 -2
- package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +5 -1
- package/Tests/Server/Services/LlmLogServiceTokenAggregateParams.test.ts +640 -0
- package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +246 -22
- package/Tests/Server/Services/NotificationDeletionImpact.test.ts +136 -8
- package/Tests/Server/Services/OnCallNotificationFallback.test.ts +191 -8
- package/Tests/Server/Services/OnCallReadinessService.test.ts +343 -18
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
- package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -0
- package/Tests/Server/Services/SeverityRuleBackfill.test.ts +5 -1
- package/Tests/Server/Services/UserMicrosoftTeamsService.test.ts +343 -0
- package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +209 -8
- package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +22 -6
- package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +12 -6
- package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +37 -3
- package/Tests/Server/Services/UserNotificationRuleWorkspaceDelivery.test.ts +669 -0
- package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +410 -0
- package/Tests/Server/Services/UserSlackService.test.ts +407 -0
- package/Tests/Server/Services/WorkspaceProjectAuthTokenDisconnectCascade.test.ts +224 -0
- package/Tests/Server/Services/WorkspaceUserAuthTokenNotificationMethodCascade.test.ts +189 -0
- package/Tests/Server/Services/WorkspaceUserNotificationService.test.ts +496 -0
- package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +30 -17
- package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +8 -3
- package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +72 -3
- package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +22 -18
- package/Tests/Server/Types/Database/Permissions/WorkspaceMethodStampedColumnCreate.test.ts +178 -0
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +259 -0
- package/Tests/Server/Utils/Marketing/MarketingEventWebhook.test.ts +257 -0
- package/Tests/Server/Utils/Monitor/MonitorLogUtilRedaction.test.ts +300 -0
- package/Tests/Server/Utils/Monitor/MonitorPayloadRedaction.test.ts +381 -0
- package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +53 -0
- package/Tests/Server/Utils/Runbook/RunbookExecutePermission.test.ts +228 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayErasureTombstone.test.ts +189 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayUsage.test.ts +205 -0
- package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +275 -0
- package/Tests/Server/Utils/Telemetry/TelemetryContext.test.ts +279 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsDirectMessage.test.ts +454 -0
- package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
- package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
- package/Tests/UI/Components/Charts/TooltipEntries.test.ts +155 -0
- package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
- package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
- package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
- package/Tests/UI/Utils/NotificationMethodUtil.test.ts +137 -9
- package/Tests/UI/Utils/PermissionGate.test.ts +105 -0
- package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
- package/Types/Analytics/RevenueEvent.ts +1 -0
- package/Types/Marketing/MarketingEvent.ts +140 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +8 -1
- package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +19 -4
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +20 -2
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +19 -4
- package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +105 -0
- package/UI/Components/Charts/Line/LineChart.tsx +8 -1
- package/UI/Components/Detail/Detail.tsx +18 -1
- package/UI/Utils/NotificationMethodUtil.ts +41 -1
- package/UI/Utils/PermissionGate.ts +56 -0
- package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +24 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +4 -2
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +39 -9
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js +363 -0
- package/build/dist/Models/DatabaseModels/UserMicrosoftTeams.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js +145 -8
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserNotificationSetting.js +38 -0
- package/build/dist/Models/DatabaseModels/UserNotificationSetting.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +96 -0
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserSlack.js +361 -0
- package/build/dist/Models/DatabaseModels/UserSlack.js.map +1 -0
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +66 -12
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js +82 -0
- package/build/dist/Server/API/UserMicrosoftTeamsAPI.js.map +1 -0
- package/build/dist/Server/API/UserSlackAPI.js +81 -0
- package/build/dist/Server/API/UserSlackAPI.js.map +1 -0
- package/build/dist/Server/EnvironmentConfig.js +25 -70
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788700000000-RemoveMarketingConversionUploadState.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js +37 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +106 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +78 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Queue.js +7 -0
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Services/EnterpriseLicenseService.js +65 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +6 -2
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/LlmLogService.js +25 -9
- package/build/dist/Server/Services/LlmLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallReadinessService.js +74 -2
- package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +144 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/UserMicrosoftTeamsService.js +183 -0
- package/build/dist/Server/Services/UserMicrosoftTeamsService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js +138 -2
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js +32 -4
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +499 -67
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +118 -0
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +26 -1
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/UserSlackService.js +195 -0
- package/build/dist/Server/Services/UserSlackService.js.map +1 -0
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js +64 -1
- package/build/dist/Server/Services/WorkspaceProjectAuthTokenService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js +70 -0
- package/build/dist/Server/Services/WorkspaceUserAuthTokenService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceUserNotificationService.js +129 -0
- package/build/dist/Server/Services/WorkspaceUserNotificationService.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js +94 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventUtil.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +93 -0
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js +12 -1
- package/build/dist/Server/Utils/Monitor/MonitorLogUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js +233 -0
- package/build/dist/Server/Utils/Monitor/MonitorPayloadRedaction.js.map +1 -0
- package/build/dist/Server/Utils/Response.js +2 -2
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +2 -2
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +141 -4
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/Analytics/RevenueEvent.js +1 -0
- package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
- package/build/dist/Types/Marketing/MarketingEvent.js +82 -0
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -0
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +9 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +22 -13
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +27 -15
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +27 -18
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +71 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +9 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Detail/Detail.js +17 -1
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Utils/NotificationMethodUtil.js +33 -1
- package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -1
- package/build/dist/UI/Utils/PermissionGate.js +32 -0
- package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
- package/package.json +1 -1
- package/Models/DatabaseModels/MarketingConversion.ts +0 -410
- package/Server/Services/MarketingConversionService.ts +0 -10
- package/Server/Utils/Marketing/ConversionUploadProvider.ts +0 -215
- package/Server/Utils/Marketing/ConversionUploadProviders.ts +0 -22
- package/Server/Utils/Marketing/Providers/GoogleAds.ts +0 -333
- package/Server/Utils/Marketing/Providers/LinkedIn.ts +0 -185
- package/Server/Utils/Marketing/Providers/Meta.ts +0 -182
- package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +0 -223
- package/Server/Utils/Marketing/Providers/Reddit.ts +0 -208
- package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +0 -315
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +0 -300
- package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +0 -592
- package/Tests/Server/Utils/Marketing/LinkedIn.test.ts +0 -282
- package/Tests/Server/Utils/Marketing/Meta.test.ts +0 -304
- package/Tests/Server/Utils/Marketing/MicrosoftAds.test.ts +0 -263
- package/Tests/Server/Utils/Marketing/Reddit.test.ts +0 -259
- package/Types/Marketing/MarketingConversion.ts +0 -53
- package/build/dist/Models/DatabaseModels/MarketingConversion.js +0 -445
- package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +0 -1
- package/build/dist/Server/Services/MarketingConversionService.js +0 -9
- package/build/dist/Server/Services/MarketingConversionService.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +0 -93
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js +0 -20
- package/build/dist/Server/Utils/Marketing/ConversionUploadProviders.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +0 -221
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +0 -137
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js +0 -133
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +0 -143
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +0 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +0 -145
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +0 -1
- package/build/dist/Types/Marketing/MarketingConversion.js +0 -52
- package/build/dist/Types/Marketing/MarketingConversion.js.map +0 -1
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { describe, expect, test, afterEach, beforeEach } from "@jest/globals";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Tests for the Microsoft Teams DIRECT MESSAGE path —
|
|
5
|
+
* MicrosoftTeamsUtil.sendDirectMessageToUserAsBot, the sender behind the
|
|
6
|
+
* "Microsoft Teams" user notification method.
|
|
7
|
+
*
|
|
8
|
+
* The problem it solves, and the reason it exists next to the older
|
|
9
|
+
* sendDirectMessageToUser: WorkspaceUserAuthToken stores the user's Microsoft
|
|
10
|
+
* ENTRA OBJECT ID, but the Graph-based helper posts to /chats/{chatId} and so
|
|
11
|
+
* needs a CHAT id — the two id spaces never meet. This method resolves a
|
|
12
|
+
* deliverable conversation from the Entra id instead, two ways:
|
|
13
|
+
*
|
|
14
|
+
* 1. REUSE. A personal chat previously captured from a bot activity (whose
|
|
15
|
+
* roster contained this Entra id) is known-deliverable and carries the
|
|
16
|
+
* regional serviceUrl, so it is preferred and delivered through the
|
|
17
|
+
* existing sendAdaptiveCardToChat path.
|
|
18
|
+
*
|
|
19
|
+
* 2. CREATE. Otherwise Bot Framework createConversation is asked to resolve
|
|
20
|
+
* (or create) the 1:1 conversation from the member's Entra id — which
|
|
21
|
+
* works exactly when the OneUptime app is installed for that user, so
|
|
22
|
+
* Microsoft's opaque roster rejection is translated into "install the
|
|
23
|
+
* OneUptime app" before anybody reads it off an on-call timeline.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
jest.mock("../../../../Server/EnvironmentConfig", () => {
|
|
27
|
+
return {
|
|
28
|
+
...(jest.requireActual("../../../../Server/EnvironmentConfig") as Record<
|
|
29
|
+
string,
|
|
30
|
+
unknown
|
|
31
|
+
>),
|
|
32
|
+
MicrosoftTeamsAppClientId: "11111111-2222-3333-4444-555555555555",
|
|
33
|
+
MicrosoftTeamsAppClientSecret: "test-secret",
|
|
34
|
+
MicrosoftTeamsAppTenantId: "test-tenant",
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
jest.mock("botbuilder", () => {
|
|
39
|
+
return {
|
|
40
|
+
CloudAdapter: class CloudAdapter {},
|
|
41
|
+
ConfigurationBotFrameworkAuthentication: class ConfigurationBotFrameworkAuthentication {},
|
|
42
|
+
TeamsActivityHandler: class TeamsActivityHandler {},
|
|
43
|
+
TurnContext: class TurnContext {},
|
|
44
|
+
ActivityHandler: class ActivityHandler {},
|
|
45
|
+
MessageFactory: {
|
|
46
|
+
text: jest.fn(),
|
|
47
|
+
attachment: jest.fn((attachment: unknown) => {
|
|
48
|
+
return { type: "message", attachments: [attachment] };
|
|
49
|
+
}),
|
|
50
|
+
},
|
|
51
|
+
CardFactory: { heroCard: jest.fn() },
|
|
52
|
+
TeamsInfo: {
|
|
53
|
+
getMembers: jest.fn(),
|
|
54
|
+
getPagedMembers: jest.fn(),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
import MicrosoftTeamsUtil from "../../../../Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams";
|
|
60
|
+
import WorkspaceProjectAuthTokenService from "../../../../Server/Services/WorkspaceProjectAuthTokenService";
|
|
61
|
+
import WorkspaceProjectAuthToken, {
|
|
62
|
+
MicrosoftTeamsChat,
|
|
63
|
+
MicrosoftTeamsMiscData,
|
|
64
|
+
} from "../../../../Models/DatabaseModels/WorkspaceProjectAuthToken";
|
|
65
|
+
import logger from "../../../../Server/Utils/Logger";
|
|
66
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
67
|
+
import BadDataException from "../../../../Types/Exception/BadDataException";
|
|
68
|
+
import {
|
|
69
|
+
WorkspaceMessageBlock,
|
|
70
|
+
WorkspacePayloadMarkdown,
|
|
71
|
+
} from "../../../../Types/Workspace/WorkspaceMessagePayload";
|
|
72
|
+
import type { ConversationParameters, TurnContext } from "botbuilder";
|
|
73
|
+
|
|
74
|
+
const MOCK_APP_CLIENT_ID: string = "11111111-2222-3333-4444-555555555555";
|
|
75
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
76
|
+
"11111111-1111-4111-8111-111111111111",
|
|
77
|
+
);
|
|
78
|
+
const TENANT_ID: string = "tenant-xyz";
|
|
79
|
+
const ENTRA_OBJECT_ID: string = "e6f1c1f7-aad0-4b6c-9c11-2f5b7c8d9e0f";
|
|
80
|
+
const PERSONAL_CHAT_ID: string = "a:1personalchat";
|
|
81
|
+
const DEFAULT_SERVICE_URL: string = "https://smba.trafficmanager.net/teams/";
|
|
82
|
+
const REGIONAL_SERVICE_URL: string = "https://smba.emea.teams.microsoft.com/";
|
|
83
|
+
const ROSTER_ERROR_TEXT: string =
|
|
84
|
+
"The bot is not part of the conversation roster.";
|
|
85
|
+
|
|
86
|
+
function markdownBlocks(): Array<WorkspaceMessageBlock> {
|
|
87
|
+
const block: WorkspacePayloadMarkdown = {
|
|
88
|
+
_type: "WorkspacePayloadMarkdown",
|
|
89
|
+
text: "**incident** page body",
|
|
90
|
+
};
|
|
91
|
+
return [block];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function personalChat(
|
|
95
|
+
overrides: Partial<MicrosoftTeamsChat> = {},
|
|
96
|
+
): MicrosoftTeamsChat {
|
|
97
|
+
return {
|
|
98
|
+
id: PERSONAL_CHAT_ID,
|
|
99
|
+
name: "Alice Example",
|
|
100
|
+
chatType: "personal",
|
|
101
|
+
serviceUrl: REGIONAL_SERVICE_URL,
|
|
102
|
+
memberAadObjectIds: [ENTRA_OBJECT_ID],
|
|
103
|
+
...overrides,
|
|
104
|
+
} as MicrosoftTeamsChat;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function miscData(
|
|
108
|
+
overrides: Partial<MicrosoftTeamsMiscData> = {},
|
|
109
|
+
): MicrosoftTeamsMiscData {
|
|
110
|
+
return {
|
|
111
|
+
tenantId: TENANT_ID,
|
|
112
|
+
teamId: "team-1",
|
|
113
|
+
teamName: "Engineering",
|
|
114
|
+
botId: "bot-1",
|
|
115
|
+
...overrides,
|
|
116
|
+
} as MicrosoftTeamsMiscData;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function mockProjectAuth(
|
|
120
|
+
overrides: Partial<MicrosoftTeamsMiscData> = {},
|
|
121
|
+
): jest.SpyInstance {
|
|
122
|
+
return jest
|
|
123
|
+
.spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
|
|
124
|
+
.mockResolvedValue({
|
|
125
|
+
id: PROJECT_ID,
|
|
126
|
+
workspaceProjectId: TENANT_ID,
|
|
127
|
+
miscData: miscData(overrides),
|
|
128
|
+
} as unknown as WorkspaceProjectAuthToken as never);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface FakeCreateAdapter {
|
|
132
|
+
createConversationAsync: jest.Mock;
|
|
133
|
+
capturedParams: Array<ConversationParameters>;
|
|
134
|
+
capturedServiceUrls: Array<string>;
|
|
135
|
+
sentActivities: Array<unknown>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function installFakeCreateAdapter(options?: {
|
|
139
|
+
createError?: unknown;
|
|
140
|
+
}): FakeCreateAdapter {
|
|
141
|
+
const capturedParams: Array<ConversationParameters> = [];
|
|
142
|
+
const capturedServiceUrls: Array<string> = [];
|
|
143
|
+
const sentActivities: Array<unknown> = [];
|
|
144
|
+
|
|
145
|
+
const createConversationAsync: jest.Mock = jest.fn(
|
|
146
|
+
async (
|
|
147
|
+
_appId: string,
|
|
148
|
+
_channelId: string,
|
|
149
|
+
serviceUrl: string,
|
|
150
|
+
_audience: string,
|
|
151
|
+
conversationParameters: ConversationParameters,
|
|
152
|
+
logic: (context: TurnContext) => Promise<void>,
|
|
153
|
+
): Promise<void> => {
|
|
154
|
+
capturedServiceUrls.push(serviceUrl);
|
|
155
|
+
capturedParams.push(conversationParameters);
|
|
156
|
+
|
|
157
|
+
if (options && "createError" in options) {
|
|
158
|
+
throw options.createError;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const fakeContext: TurnContext = {
|
|
162
|
+
sendActivity: async (activity: unknown): Promise<unknown> => {
|
|
163
|
+
sentActivities.push(activity);
|
|
164
|
+
return { id: "msg-123" };
|
|
165
|
+
},
|
|
166
|
+
} as unknown as TurnContext;
|
|
167
|
+
|
|
168
|
+
await logic(fakeContext);
|
|
169
|
+
},
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
jest
|
|
173
|
+
.spyOn(MicrosoftTeamsUtil as any, "getBotAdapter")
|
|
174
|
+
.mockReturnValue({ createConversationAsync: createConversationAsync });
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
createConversationAsync: createConversationAsync,
|
|
178
|
+
capturedParams: capturedParams,
|
|
179
|
+
capturedServiceUrls: capturedServiceUrls,
|
|
180
|
+
sentActivities: sentActivities,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
describe("MicrosoftTeamsUtil.sendDirectMessageToUserAsBot", () => {
|
|
185
|
+
let sendToChat: jest.SpyInstance;
|
|
186
|
+
|
|
187
|
+
beforeEach(() => {
|
|
188
|
+
jest.spyOn(logger, "error").mockImplementation((): void => {
|
|
189
|
+
return undefined;
|
|
190
|
+
});
|
|
191
|
+
jest.spyOn(logger, "debug").mockImplementation((): void => {
|
|
192
|
+
return undefined;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
sendToChat = jest
|
|
196
|
+
.spyOn(MicrosoftTeamsUtil, "sendAdaptiveCardToChat")
|
|
197
|
+
.mockResolvedValue({
|
|
198
|
+
channel: {
|
|
199
|
+
id: PERSONAL_CHAT_ID,
|
|
200
|
+
name: "Alice Example",
|
|
201
|
+
workspaceType: "MicrosoftTeams",
|
|
202
|
+
},
|
|
203
|
+
threadId: "msg-1",
|
|
204
|
+
} as never);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
afterEach(() => {
|
|
208
|
+
jest.restoreAllMocks();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
/*
|
|
212
|
+
* ----------------------------------------------------------------------- *
|
|
213
|
+
* (A) Reusing a captured personal chat.
|
|
214
|
+
* -----------------------------------------------------------------------
|
|
215
|
+
*/
|
|
216
|
+
|
|
217
|
+
describe("reusing a captured personal chat", () => {
|
|
218
|
+
test("a personal chat whose roster carries the Entra id is delivered through sendAdaptiveCardToChat", async () => {
|
|
219
|
+
mockProjectAuth({
|
|
220
|
+
availableChats: { [PERSONAL_CHAT_ID]: personalChat() },
|
|
221
|
+
});
|
|
222
|
+
const adapter: FakeCreateAdapter = installFakeCreateAdapter();
|
|
223
|
+
|
|
224
|
+
await MicrosoftTeamsUtil.sendDirectMessageToUserAsBot({
|
|
225
|
+
projectId: PROJECT_ID,
|
|
226
|
+
workspaceUserId: ENTRA_OBJECT_ID,
|
|
227
|
+
messageBlocks: markdownBlocks(),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(sendToChat).toHaveBeenCalledTimes(1);
|
|
231
|
+
const arg: { chatId: string; projectId: ObjectID } = sendToChat.mock
|
|
232
|
+
.calls[0][0] as { chatId: string; projectId: ObjectID };
|
|
233
|
+
expect(arg.chatId).toBe(PERSONAL_CHAT_ID);
|
|
234
|
+
expect(arg.projectId.toString()).toBe(PROJECT_ID.toString());
|
|
235
|
+
|
|
236
|
+
// The Bot Framework create path is not touched at all.
|
|
237
|
+
expect(adapter.createConversationAsync).not.toHaveBeenCalled();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("a personal chat belonging to somebody ELSE is not reused", async () => {
|
|
241
|
+
mockProjectAuth({
|
|
242
|
+
availableChats: {
|
|
243
|
+
[PERSONAL_CHAT_ID]: personalChat({
|
|
244
|
+
memberAadObjectIds: ["some-other-user"],
|
|
245
|
+
}),
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
installFakeCreateAdapter();
|
|
249
|
+
|
|
250
|
+
await MicrosoftTeamsUtil.sendDirectMessageToUserAsBot({
|
|
251
|
+
projectId: PROJECT_ID,
|
|
252
|
+
workspaceUserId: ENTRA_OBJECT_ID,
|
|
253
|
+
messageBlocks: markdownBlocks(),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(sendToChat).not.toHaveBeenCalled();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("a GROUP chat carrying the Entra id is not reused - a page is a direct message, not a group post", async () => {
|
|
260
|
+
mockProjectAuth({
|
|
261
|
+
availableChats: {
|
|
262
|
+
[PERSONAL_CHAT_ID]: personalChat({ chatType: "groupChat" }),
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
installFakeCreateAdapter();
|
|
266
|
+
|
|
267
|
+
await MicrosoftTeamsUtil.sendDirectMessageToUserAsBot({
|
|
268
|
+
projectId: PROJECT_ID,
|
|
269
|
+
workspaceUserId: ENTRA_OBJECT_ID,
|
|
270
|
+
messageBlocks: markdownBlocks(),
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
expect(sendToChat).not.toHaveBeenCalled();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("a legacy personal chat with NO captured roster is not matched (absent means unknown, not mine)", async () => {
|
|
277
|
+
mockProjectAuth({
|
|
278
|
+
availableChats: {
|
|
279
|
+
[PERSONAL_CHAT_ID]: personalChat({ memberAadObjectIds: undefined }),
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
installFakeCreateAdapter();
|
|
283
|
+
|
|
284
|
+
await MicrosoftTeamsUtil.sendDirectMessageToUserAsBot({
|
|
285
|
+
projectId: PROJECT_ID,
|
|
286
|
+
workspaceUserId: ENTRA_OBJECT_ID,
|
|
287
|
+
messageBlocks: markdownBlocks(),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
expect(sendToChat).not.toHaveBeenCalled();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
/*
|
|
295
|
+
* ----------------------------------------------------------------------- *
|
|
296
|
+
* (B) Creating the conversation from the Entra id.
|
|
297
|
+
* -----------------------------------------------------------------------
|
|
298
|
+
*/
|
|
299
|
+
|
|
300
|
+
describe("creating the 1:1 conversation via Bot Framework", () => {
|
|
301
|
+
test("createConversation is asked for a non-group conversation with the member's Entra id and the tenant", async () => {
|
|
302
|
+
mockProjectAuth();
|
|
303
|
+
const adapter: FakeCreateAdapter = installFakeCreateAdapter();
|
|
304
|
+
|
|
305
|
+
await MicrosoftTeamsUtil.sendDirectMessageToUserAsBot({
|
|
306
|
+
projectId: PROJECT_ID,
|
|
307
|
+
workspaceUserId: ENTRA_OBJECT_ID,
|
|
308
|
+
messageBlocks: markdownBlocks(),
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(adapter.createConversationAsync).toHaveBeenCalledTimes(1);
|
|
312
|
+
const params: ConversationParameters = adapter
|
|
313
|
+
.capturedParams[0] as ConversationParameters;
|
|
314
|
+
expect(params.isGroup).toBe(false);
|
|
315
|
+
expect(params.members?.[0]?.id).toBe(ENTRA_OBJECT_ID);
|
|
316
|
+
expect(params.tenantId).toBe(TENANT_ID);
|
|
317
|
+
expect((params.channelData as { tenant: { id: string } }).tenant.id).toBe(
|
|
318
|
+
TENANT_ID,
|
|
319
|
+
);
|
|
320
|
+
// 28:<appId> is the Teams-side bot account id.
|
|
321
|
+
expect(params.bot?.id).toBe(`28:${MOCK_APP_CLIENT_ID}`);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("the adaptive card is sent inside the created conversation", async () => {
|
|
325
|
+
mockProjectAuth();
|
|
326
|
+
const adapter: FakeCreateAdapter = installFakeCreateAdapter();
|
|
327
|
+
|
|
328
|
+
await MicrosoftTeamsUtil.sendDirectMessageToUserAsBot({
|
|
329
|
+
projectId: PROJECT_ID,
|
|
330
|
+
workspaceUserId: ENTRA_OBJECT_ID,
|
|
331
|
+
messageBlocks: markdownBlocks(),
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
expect(adapter.sentActivities).toHaveLength(1);
|
|
335
|
+
const activity: { attachments: Array<{ contentType: string }> } = adapter
|
|
336
|
+
.sentActivities[0] as { attachments: Array<{ contentType: string }> };
|
|
337
|
+
expect(activity.attachments[0]?.contentType).toBe(
|
|
338
|
+
"application/vnd.microsoft.card.adaptive",
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("falls back to the commercial-cloud serviceUrl when nothing regional was ever captured", async () => {
|
|
343
|
+
mockProjectAuth();
|
|
344
|
+
const adapter: FakeCreateAdapter = installFakeCreateAdapter();
|
|
345
|
+
|
|
346
|
+
await MicrosoftTeamsUtil.sendDirectMessageToUserAsBot({
|
|
347
|
+
projectId: PROJECT_ID,
|
|
348
|
+
workspaceUserId: ENTRA_OBJECT_ID,
|
|
349
|
+
messageBlocks: markdownBlocks(),
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
expect(adapter.capturedServiceUrls[0]).toBe(DEFAULT_SERVICE_URL);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("prefers a regional serviceUrl captured on ANY chat (required for GCC/DoD)", async () => {
|
|
356
|
+
mockProjectAuth({
|
|
357
|
+
availableChats: {
|
|
358
|
+
"a:someoneelse": personalChat({
|
|
359
|
+
id: "a:someoneelse",
|
|
360
|
+
memberAadObjectIds: ["some-other-user"],
|
|
361
|
+
serviceUrl: REGIONAL_SERVICE_URL,
|
|
362
|
+
}),
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
const adapter: FakeCreateAdapter = installFakeCreateAdapter();
|
|
366
|
+
|
|
367
|
+
await MicrosoftTeamsUtil.sendDirectMessageToUserAsBot({
|
|
368
|
+
projectId: PROJECT_ID,
|
|
369
|
+
workspaceUserId: ENTRA_OBJECT_ID,
|
|
370
|
+
messageBlocks: markdownBlocks(),
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
expect(adapter.capturedServiceUrls[0]).toBe(REGIONAL_SERVICE_URL);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
/*
|
|
378
|
+
* ----------------------------------------------------------------------- *
|
|
379
|
+
* (C) Failure translation and refusals.
|
|
380
|
+
* -----------------------------------------------------------------------
|
|
381
|
+
*/
|
|
382
|
+
|
|
383
|
+
describe("failure translation and refusals", () => {
|
|
384
|
+
test("Microsoft's roster rejection becomes 'install the OneUptime app', the one fix that works", async () => {
|
|
385
|
+
mockProjectAuth();
|
|
386
|
+
installFakeCreateAdapter({
|
|
387
|
+
createError: new Error(ROSTER_ERROR_TEXT),
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
await expect(
|
|
391
|
+
MicrosoftTeamsUtil.sendDirectMessageToUserAsBot({
|
|
392
|
+
projectId: PROJECT_ID,
|
|
393
|
+
workspaceUserId: ENTRA_OBJECT_ID,
|
|
394
|
+
messageBlocks: markdownBlocks(),
|
|
395
|
+
}),
|
|
396
|
+
).rejects.toThrow(
|
|
397
|
+
"The OneUptime app is not installed for this Microsoft Teams user. Ask them to add the OneUptime app in Microsoft Teams (personal scope) so the bot can message them directly.",
|
|
398
|
+
);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("every other Bot Framework error passes through untouched", async () => {
|
|
402
|
+
mockProjectAuth();
|
|
403
|
+
installFakeCreateAdapter({
|
|
404
|
+
createError: new Error("throttled: too many requests"),
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
await expect(
|
|
408
|
+
MicrosoftTeamsUtil.sendDirectMessageToUserAsBot({
|
|
409
|
+
projectId: PROJECT_ID,
|
|
410
|
+
workspaceUserId: ENTRA_OBJECT_ID,
|
|
411
|
+
messageBlocks: markdownBlocks(),
|
|
412
|
+
}),
|
|
413
|
+
).rejects.toThrow("throttled: too many requests");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("a project with no Teams integration is refused before the Bot Framework is touched", async () => {
|
|
417
|
+
jest
|
|
418
|
+
.spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
|
|
419
|
+
.mockResolvedValue(null as never);
|
|
420
|
+
const adapter: FakeCreateAdapter = installFakeCreateAdapter();
|
|
421
|
+
|
|
422
|
+
await expect(
|
|
423
|
+
MicrosoftTeamsUtil.sendDirectMessageToUserAsBot({
|
|
424
|
+
projectId: PROJECT_ID,
|
|
425
|
+
workspaceUserId: ENTRA_OBJECT_ID,
|
|
426
|
+
messageBlocks: markdownBlocks(),
|
|
427
|
+
}),
|
|
428
|
+
).rejects.toThrow(
|
|
429
|
+
"Microsoft Teams integration not found for this project",
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
expect(adapter.createConversationAsync).not.toHaveBeenCalled();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("a project auth row with no tenant id is refused", async () => {
|
|
436
|
+
jest
|
|
437
|
+
.spyOn(WorkspaceProjectAuthTokenService, "getProjectAuth")
|
|
438
|
+
.mockResolvedValue({
|
|
439
|
+
id: PROJECT_ID,
|
|
440
|
+
workspaceProjectId: undefined,
|
|
441
|
+
miscData: miscData(),
|
|
442
|
+
} as unknown as WorkspaceProjectAuthToken as never);
|
|
443
|
+
installFakeCreateAdapter();
|
|
444
|
+
|
|
445
|
+
await expect(
|
|
446
|
+
MicrosoftTeamsUtil.sendDirectMessageToUserAsBot({
|
|
447
|
+
projectId: PROJECT_ID,
|
|
448
|
+
workspaceUserId: ENTRA_OBJECT_ID,
|
|
449
|
+
messageBlocks: markdownBlocks(),
|
|
450
|
+
}),
|
|
451
|
+
).rejects.toThrow(BadDataException);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
});
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import ejs from "ejs";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Consent and attribution have to behave the same on every origin that loads
|
|
8
|
+
* the Google Tag Manager container.
|
|
9
|
+
*
|
|
10
|
+
* They did not. Consent Mode v2 and the attribution capture lived only in
|
|
11
|
+
* Home/Views/head-basic.ejs, so the marketing site denied storage by default
|
|
12
|
+
* and asked, while /accounts and /dashboard loaded the same container with no
|
|
13
|
+
* consent signal and captured nothing. A visitor who pressed "Reject all" was
|
|
14
|
+
* tracked anyway on the signup page — which is precisely where the conversion
|
|
15
|
+
* fires — and an ad pointed at /accounts/register?gclid=... stored no click id,
|
|
16
|
+
* so that signup could never be attributed to the campaign that bought it.
|
|
17
|
+
*
|
|
18
|
+
* These render each origin the way Express does and assert the invariants that
|
|
19
|
+
* were being violated. They are deliberately about ORDER and SAMENESS: a
|
|
20
|
+
* consent default that arrives after the container has read the dataLayer is
|
|
21
|
+
* not a consent default, and three copies of the storage contract is how the
|
|
22
|
+
* copies stop agreeing.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const REPOSITORY_ROOT: string = path.resolve(__dirname, "..", "..", "..", "..");
|
|
26
|
+
|
|
27
|
+
const PARTIAL_PATH: string = path.join(
|
|
28
|
+
REPOSITORY_ROOT,
|
|
29
|
+
"Common",
|
|
30
|
+
"Server",
|
|
31
|
+
"Views",
|
|
32
|
+
"Partials",
|
|
33
|
+
"AnalyticsConsent.ejs",
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
interface Origin {
|
|
37
|
+
label: string;
|
|
38
|
+
viewPath: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/*
|
|
42
|
+
* Every OneUptime-owned origin that loads GTM-PKQD5WH. Status pages and public
|
|
43
|
+
* dashboards deliberately are not here: they render on customer-owned custom
|
|
44
|
+
* domains, where the answer is to remove the tag rather than to ask the
|
|
45
|
+
* customer's visitors for consent on OneUptime's behalf.
|
|
46
|
+
*/
|
|
47
|
+
const ORIGINS: Array<Origin> = [
|
|
48
|
+
{
|
|
49
|
+
label: "marketing site",
|
|
50
|
+
viewPath: path.join(REPOSITORY_ROOT, "Home", "Views", "head-basic.ejs"),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: "accounts (signup)",
|
|
54
|
+
viewPath: path.join(
|
|
55
|
+
REPOSITORY_ROOT,
|
|
56
|
+
"App",
|
|
57
|
+
"FeatureSet",
|
|
58
|
+
"Accounts",
|
|
59
|
+
"views",
|
|
60
|
+
"index.ejs",
|
|
61
|
+
),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
label: "dashboard",
|
|
65
|
+
viewPath: path.join(
|
|
66
|
+
REPOSITORY_ROOT,
|
|
67
|
+
"App",
|
|
68
|
+
"FeatureSet",
|
|
69
|
+
"Dashboard",
|
|
70
|
+
"views",
|
|
71
|
+
"index.ejs",
|
|
72
|
+
),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
label: "admin dashboard",
|
|
76
|
+
viewPath: path.join(
|
|
77
|
+
REPOSITORY_ROOT,
|
|
78
|
+
"App",
|
|
79
|
+
"FeatureSet",
|
|
80
|
+
"AdminDashboard",
|
|
81
|
+
"views",
|
|
82
|
+
"index.ejs",
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
type RenderOriginFunction = (
|
|
88
|
+
origin: Origin,
|
|
89
|
+
enableGoogleTagManager: boolean,
|
|
90
|
+
) => string;
|
|
91
|
+
|
|
92
|
+
const renderOrigin: RenderOriginFunction = (
|
|
93
|
+
origin: Origin,
|
|
94
|
+
enableGoogleTagManager: boolean,
|
|
95
|
+
): string => {
|
|
96
|
+
return ejs.render(
|
|
97
|
+
fs.readFileSync(origin.viewPath, "utf-8"),
|
|
98
|
+
{ enableGoogleTagManager: enableGoogleTagManager },
|
|
99
|
+
{ filename: origin.viewPath },
|
|
100
|
+
);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const GTM_SCRIPT: string = "googletagmanager.com/gtm.js";
|
|
104
|
+
const CONSENT_DEFAULT: string = "pushConsentSignal('default', false)";
|
|
105
|
+
|
|
106
|
+
describe("the shared analytics consent partial", () => {
|
|
107
|
+
test("exists in Common, so every origin can reach it", () => {
|
|
108
|
+
expect(fs.existsSync(PARTIAL_PATH)).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("does not carry Home-only concerns", () => {
|
|
112
|
+
const source: string = fs.readFileSync(PARTIAL_PATH, "utf-8");
|
|
113
|
+
|
|
114
|
+
/*
|
|
115
|
+
* The PostHog loader carries Home's own project key, and the home/-prefixed
|
|
116
|
+
* event names are the marketing site's. Moving either here would start the
|
|
117
|
+
* product reporting into Home's funnel. Reporting is handed back to the
|
|
118
|
+
* page through window.oneUptimeOnAttributionCaptured instead.
|
|
119
|
+
*/
|
|
120
|
+
expect(source).not.toContain("posthog.init(");
|
|
121
|
+
// The prose may mention them; no code here may emit one.
|
|
122
|
+
expect(source).not.toContain("capture('home/");
|
|
123
|
+
expect(source).toContain("window.oneUptimeOnAttributionCaptured");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
for (const origin of ORIGINS) {
|
|
128
|
+
describe(origin.label, () => {
|
|
129
|
+
test("defines the consent state machine", () => {
|
|
130
|
+
expect(renderOrigin(origin, true)).toContain(
|
|
131
|
+
"window.oneUptimeConsent = (function",
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("captures ad click identifiers", () => {
|
|
136
|
+
const html: string = renderOrigin(origin, true);
|
|
137
|
+
|
|
138
|
+
expect(html).toContain("window.oneUptimeGetAttribution");
|
|
139
|
+
expect(html).toContain("gclid");
|
|
140
|
+
expect(html).toContain("wbraid");
|
|
141
|
+
expect(html).toContain("gbraid");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("reads and writes the same consent storage key as every other origin", () => {
|
|
145
|
+
/*
|
|
146
|
+
* These origins share one hostname on OneUptime Cloud, so they share one
|
|
147
|
+
* localStorage. A different key here would mean the banner answer given
|
|
148
|
+
* on the marketing site simply did not apply in the product.
|
|
149
|
+
*/
|
|
150
|
+
expect(renderOrigin(origin, true)).toContain("'cookiesAccepted'");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("pushes the Consent Mode defaults before the container loads", () => {
|
|
154
|
+
const html: string = renderOrigin(origin, true);
|
|
155
|
+
|
|
156
|
+
const consentIndex: number = html.indexOf(CONSENT_DEFAULT);
|
|
157
|
+
const containerIndex: number = html.indexOf(GTM_SCRIPT);
|
|
158
|
+
|
|
159
|
+
expect(consentIndex).toBeGreaterThan(-1);
|
|
160
|
+
expect(containerIndex).toBeGreaterThan(-1);
|
|
161
|
+
|
|
162
|
+
/*
|
|
163
|
+
* The whole point. Consent Mode defaults are only read once, when the
|
|
164
|
+
* container boots; arriving afterwards is the same as never arriving.
|
|
165
|
+
*/
|
|
166
|
+
expect(consentIndex).toBeLessThan(containerIndex);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("denies ad and analytics storage until the visitor answers", () => {
|
|
170
|
+
const html: string = renderOrigin(origin, true);
|
|
171
|
+
|
|
172
|
+
expect(html).toContain("ad_storage");
|
|
173
|
+
expect(html).toContain("ad_user_data");
|
|
174
|
+
expect(html).toContain("ad_personalization");
|
|
175
|
+
expect(html).toContain("analytics_storage");
|
|
176
|
+
// `unset` is treated as denied rather than as permission-by-silence.
|
|
177
|
+
expect(html).toContain(CONSENT_DEFAULT);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("loads no Google tag at all when analytics are switched off", () => {
|
|
181
|
+
const html: string = renderOrigin(origin, false);
|
|
182
|
+
|
|
183
|
+
expect(html).not.toContain(GTM_SCRIPT);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
describe("consent can be withdrawn", () => {
|
|
189
|
+
const bannerPath: string = path.join(
|
|
190
|
+
REPOSITORY_ROOT,
|
|
191
|
+
"Home",
|
|
192
|
+
"Views",
|
|
193
|
+
"cookie-banner.ejs",
|
|
194
|
+
);
|
|
195
|
+
const footerPath: string = path.join(
|
|
196
|
+
REPOSITORY_ROOT,
|
|
197
|
+
"Home",
|
|
198
|
+
"Views",
|
|
199
|
+
"footer.ejs",
|
|
200
|
+
);
|
|
201
|
+
const cookiePolicyPath: string = path.join(
|
|
202
|
+
REPOSITORY_ROOT,
|
|
203
|
+
"Home",
|
|
204
|
+
"Views",
|
|
205
|
+
"cookies.ejs",
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
test("the banner exposes a way to reopen itself", () => {
|
|
209
|
+
expect(fs.readFileSync(bannerPath, "utf-8")).toContain(
|
|
210
|
+
"window.oneUptimeShowCookieSettings",
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("the footer carries the settings link the policy promises", () => {
|
|
215
|
+
/*
|
|
216
|
+
* The Cookie Policy has always told visitors to use "the cookie settings
|
|
217
|
+
* link in the footer". There was no such link, so a decision could be made
|
|
218
|
+
* once and never revisited - the most commonly enforced banner defect
|
|
219
|
+
* there is.
|
|
220
|
+
*/
|
|
221
|
+
const footer: string = fs.readFileSync(footerPath, "utf-8");
|
|
222
|
+
|
|
223
|
+
expect(footer).toContain("Cookie settings");
|
|
224
|
+
expect(footer).toContain("window.oneUptimeShowCookieSettings");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("the cookie policy no longer denies using marketing cookies in the product", () => {
|
|
228
|
+
/*
|
|
229
|
+
* It said "We do not use marketing cookies inside the product" while the
|
|
230
|
+
* product loaded the same container - and therefore the same advertising
|
|
231
|
+
* tags - as the marketing site.
|
|
232
|
+
*/
|
|
233
|
+
const policy: string = fs.readFileSync(cookiePolicyPath, "utf-8");
|
|
234
|
+
|
|
235
|
+
expect(policy).not.toContain(
|
|
236
|
+
"We do not use marketing cookies inside the product",
|
|
237
|
+
);
|
|
238
|
+
expect(policy).toContain("governed by the same");
|
|
239
|
+
expect(policy).toContain("consent choice");
|
|
240
|
+
});
|
|
241
|
+
});
|