@oneuptime/common 11.7.1 → 11.7.2
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/DashboardDomain.ts +1 -0
- package/Models/DatabaseModels/Incident.ts +2 -0
- package/Models/DatabaseModels/IncidentEpisode.ts +1 -0
- package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +1 -0
- package/Models/DatabaseModels/IncidentEpisodeStateTimeline.ts +1 -0
- package/Models/DatabaseModels/IncidentPublicNote.ts +1 -0
- package/Models/DatabaseModels/IncidentStateTimeline.ts +1 -0
- package/Models/DatabaseModels/Monitor.ts +1 -0
- package/Models/DatabaseModels/MonitorStatusTimeline.ts +1 -0
- package/Models/DatabaseModels/NetworkSite.ts +1 -0
- package/Models/DatabaseModels/NetworkSiteStatusTimeline.ts +1 -0
- package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +1 -0
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +2 -0
- package/Models/DatabaseModels/Probe.ts +13 -13
- package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -0
- package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +1 -0
- package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +1 -0
- package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -0
- package/Models/DatabaseModels/StatusPage.ts +1 -0
- package/Models/DatabaseModels/StatusPageAnnouncement.ts +1 -0
- package/Models/DatabaseModels/StatusPageDomain.ts +1 -0
- package/Models/DatabaseModels/WorkspaceNotificationSummary.ts +1 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +11 -0
- package/Server/API/BaseAPI.ts +17 -1
- package/Server/API/CommonAPI.ts +32 -0
- package/Server/API/MicrosoftTeamsAPI.ts +99 -2
- package/Server/API/ProbeAPI.ts +2 -0
- package/Server/API/SlackAPI.ts +62 -0
- package/Server/API/StatusPageAPI.ts +157 -192
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.ts +89 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.ts +61 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.ts +280 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Services/DatabaseService.ts +11 -3
- package/Server/Services/MonitorGroupService.ts +145 -0
- package/Server/Services/MonitorService.ts +231 -30
- package/Server/Services/ProbeService.ts +319 -16
- package/Server/Services/StatusPageService.ts +72 -42
- package/Server/Services/WorkspaceNotificationRuleService.ts +232 -2
- package/Server/Utils/Database/PostgresErrorTranslator.ts +131 -0
- package/Server/Utils/Monitor/MonitorResource.ts +88 -35
- package/Server/Utils/Monitor/MonitorStatusTimeline.ts +21 -1
- package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +158 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +706 -6
- package/Tests/Models/DatabaseModels/ProbeColumnAccessControl.test.ts +103 -0
- package/Tests/Server/API/BaseAPIUpdateNoOp.test.ts +135 -0
- package/Tests/Server/API/CommonAPIAuthGuard.test.ts +317 -0
- package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +228 -0
- package/Tests/Server/API/ProbeAPI.test.ts +6 -0
- package/Tests/Server/API/ZZVerifyChatsAuth.test.ts +167 -0
- package/Tests/Server/Services/AddHotQueryIndexesMigration.test.ts +255 -0
- package/Tests/Server/Services/AddHotQueryIndexesSecondPassMigration.test.ts +258 -0
- package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +1 -1
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +121 -0
- package/Tests/Server/Services/HeartbeatWriteFastPathSafety.test.ts +200 -0
- package/Tests/Server/Services/HookFreeWriteSafetySecondPass.test.ts +181 -0
- package/Tests/Server/Services/MonitorGroupServiceBatchedStatuses.test.ts +513 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +492 -0
- package/Tests/Server/Services/MonitorServiceProbeSelection.test.ts +401 -0
- package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +453 -0
- package/Tests/Server/Services/ProbeAuthKeyCacheAndHeartbeat.test.ts +679 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +3 -3
- package/Tests/Server/Services/StatusPageDomainResolutionCache.test.ts +245 -0
- package/Tests/Server/Services/StatusPageOverviewTimelineWindow.test.ts +283 -0
- package/Tests/Server/Services/StatusPageServiceMcp.test.ts +17 -1
- package/Tests/Server/Services/StatusPageServiceMonitorGroupBatching.test.ts +331 -0
- package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +12 -12
- package/Tests/Server/Services/WorkspaceNotificationRuleChats.test.ts +1390 -0
- package/Tests/Server/Utils/AI/Insights/InsightFixRouting.test.ts +4 -4
- package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +2 -2
- package/Tests/Server/Utils/AI/Insights/InsightStore.test.ts +1 -3
- package/Tests/Server/Utils/AI/Insights/InsightStoreHardening.test.ts +1 -3
- package/Tests/Server/Utils/AI/Insights/InsightTriage.test.ts +1 -1
- package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +5 -5
- package/Tests/Server/Utils/Alert/AlertPrivacyFilter.test.ts +187 -0
- package/Tests/Server/Utils/AlertEpisode/AlertEpisodePrivacyFilter.test.ts +170 -0
- package/Tests/Server/Utils/Database/PostgresErrorTranslator.test.ts +125 -0
- package/Tests/Server/Utils/Incident/IncidentPrivacyFilter.test.ts +264 -0
- package/Tests/Server/Utils/IncidentEpisode/IncidentEpisodePrivacyFilter.test.ts +175 -0
- package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +181 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +1 -1
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +2 -2
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelsList.test.ts +341 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +2175 -0
- package/Tests/Server/Utils/Workspace/SlackChannelsList.test.ts +585 -0
- package/Tests/Types/Billing/MeteredPlan.test.ts +46 -0
- package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +219 -1
- package/Tests/UI/Components/BulkUpdateForm.test.tsx +174 -0
- package/Tests/UI/Components/CardModelDetailEdit.test.tsx +315 -0
- package/Tests/UI/Components/ModelDetailSelect.test.tsx +229 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkSelectAll.test.tsx +1258 -0
- package/Tests/UI/Components/ModelTableExportFromColumns.test.ts +153 -0
- package/Tests/UI/Components/MonacoLoader.test.ts +67 -0
- package/Tests/UI/Components/MonacoRuntime.test.ts +121 -0
- package/Tests/UI/Components/TableBulkCsvExport.test.tsx +212 -3
- package/Tests/UI/Utils/Project.test.ts +809 -0
- package/Tests/UI/Utils/TableColumnsToCsv.test.ts +360 -0
- package/Tests/Utils/Alerts/AlertMetricType.test.ts +143 -0
- package/Tests/Utils/Dashboard/VariableUrlState.test.ts +219 -0
- package/Tests/Utils/Incident/IncidentMetricType.test.ts +174 -0
- package/Tests/Utils/Monitor/MonitorProbeSelectionUtil.test.ts +145 -0
- package/Tests/Utils/StatusPage/ResourceUptime.test.ts +263 -0
- package/Types/Workspace/NotificationRules/BaseNotificationRule.ts +4 -0
- package/Types/Workspace/NotificationRules/NotificationRuleCondition.ts +25 -10
- package/Types/Workspace/WorkspaceMessagePayload.ts +1 -0
- package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +63 -8
- package/UI/Components/CodeEditor/CodeEditor.tsx +3 -0
- package/UI/Components/CodeEditor/MonacoLoader.ts +36 -0
- package/UI/Components/Forms/ModelForm.tsx +41 -18
- package/UI/Components/ModelDetail/CardModelDetail.tsx +23 -1
- package/UI/Components/ModelDetail/ModelDetail.tsx +46 -1
- package/UI/Components/ModelTable/BaseModelTable.tsx +300 -30
- package/UI/Components/ModelTable/Column.ts +12 -0
- package/UI/Components/ModelTable/ExportFromColumns.ts +57 -0
- package/UI/Components/Navbar/NavBar.tsx +66 -1
- package/UI/Components/Table/Table.tsx +52 -13
- package/UI/Components/Table/Types/Column.ts +18 -0
- package/UI/Utils/Project.ts +214 -18
- package/UI/Utils/TableColumnsToCsv.ts +86 -6
- package/UI/esbuild-config.js +34 -0
- package/Utils/Monitor/MonitorProbeSelectionUtil.ts +72 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js +1 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +2 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +1 -0
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkSite.js +1 -0
- package/build/dist/Models/DatabaseModels/NetworkSite.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +1 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +2 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Probe.js +13 -13
- package/build/dist/Models/DatabaseModels/Probe.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +1 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js +1 -0
- package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +12 -1
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/CommonAPI.js +21 -0
- package/build/dist/Server/API/CommonAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +72 -2
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/ProbeAPI.js +2 -0
- package/build/dist/Server/API/ProbeAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +44 -0
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +98 -152
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js +36 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js +254 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +10 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/MonitorGroupService.js +118 -0
- package/build/dist/Server/Services/MonitorGroupService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +198 -24
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/ProbeService.js +238 -9
- package/build/dist/Server/Services/ProbeService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +46 -39
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +178 -3
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js +80 -0
- package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +67 -29
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +31 -14
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +117 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +549 -7
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js +17 -7
- package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js.map +1 -1
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +37 -11
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js +2 -0
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/MonacoLoader.js +34 -0
- package/build/dist/UI/Components/CodeEditor/MonacoLoader.js.map +1 -0
- package/build/dist/UI/Components/Forms/ModelForm.js +37 -17
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +22 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/ModelDetail.js +31 -1
- package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +234 -21
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ExportFromColumns.js +30 -0
- package/build/dist/UI/Components/ModelTable/ExportFromColumns.js.map +1 -0
- package/build/dist/UI/Components/Navbar/NavBar.js +39 -1
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +31 -11
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Utils/Project.js +161 -15
- package/build/dist/UI/Utils/Project.js.map +1 -1
- package/build/dist/UI/Utils/TableColumnsToCsv.js +67 -6
- package/build/dist/UI/Utils/TableColumnsToCsv.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js +52 -0
- package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js.map +1 -0
- package/package.json +4 -3
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { describe, expect, test, afterEach } from "@jest/globals";
|
|
2
|
+
import { JSONArray, JSONObject } from "../../../Types/JSON";
|
|
3
|
+
import MicrosoftTeamsAPI from "../../../Server/API/MicrosoftTeamsAPI";
|
|
4
|
+
|
|
5
|
+
const MOCK_TEAMS_CLIENT_ID: string = "11111111-2222-3333-4444-555555555555";
|
|
6
|
+
|
|
7
|
+
jest.mock("../../../Server/EnvironmentConfig", () => {
|
|
8
|
+
return {
|
|
9
|
+
...(jest.requireActual("../../../Server/EnvironmentConfig") as Record<
|
|
10
|
+
string,
|
|
11
|
+
unknown
|
|
12
|
+
>),
|
|
13
|
+
MicrosoftTeamsAppClientId: "11111111-2222-3333-4444-555555555555",
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
type GetManifestFunction = () => JSONObject;
|
|
18
|
+
|
|
19
|
+
const getManifest: GetManifestFunction = (): JSONObject => {
|
|
20
|
+
return (
|
|
21
|
+
MicrosoftTeamsAPI as unknown as {
|
|
22
|
+
getTeamsAppManifest: GetManifestFunction;
|
|
23
|
+
}
|
|
24
|
+
).getTeamsAppManifest();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type GetBotFunction = (manifest: JSONObject) => JSONObject;
|
|
28
|
+
|
|
29
|
+
const getBot: GetBotFunction = (manifest: JSONObject): JSONObject => {
|
|
30
|
+
const bots: JSONArray = manifest["bots"] as JSONArray;
|
|
31
|
+
return bots[0] as JSONObject;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type GetResourceSpecificPermissionsFunction = (
|
|
35
|
+
manifest: JSONObject,
|
|
36
|
+
) => Array<JSONObject>;
|
|
37
|
+
|
|
38
|
+
const getResourceSpecificPermissions: GetResourceSpecificPermissionsFunction = (
|
|
39
|
+
manifest: JSONObject,
|
|
40
|
+
): Array<JSONObject> => {
|
|
41
|
+
const authorization: JSONObject = manifest["authorization"] as JSONObject;
|
|
42
|
+
const permissions: JSONObject = authorization["permissions"] as JSONObject;
|
|
43
|
+
return permissions["resourceSpecific"] as Array<JSONObject>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
describe("MicrosoftTeamsAPI.getTeamsAppManifest", () => {
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
jest.restoreAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("bot scopes (chat support)", () => {
|
|
52
|
+
test("declares exactly one bot", () => {
|
|
53
|
+
const manifest: JSONObject = getManifest();
|
|
54
|
+
const bots: JSONArray = manifest["bots"] as JSONArray;
|
|
55
|
+
expect(bots).toHaveLength(1);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("bot scopes contain exactly team, personal and groupChat", () => {
|
|
59
|
+
const bot: JSONObject = getBot(getManifest());
|
|
60
|
+
const scopes: Array<string> = bot["scopes"] as Array<string>;
|
|
61
|
+
expect([...scopes].sort()).toEqual(
|
|
62
|
+
["groupChat", "personal", "team"].sort(),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("bot scopes include groupChat so the bot can be added to group chats", () => {
|
|
67
|
+
const bot: JSONObject = getBot(getManifest());
|
|
68
|
+
expect(bot["scopes"] as Array<string>).toContain("groupChat");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("bot scopes include personal so the bot can be added to 1:1 chats", () => {
|
|
72
|
+
const bot: JSONObject = getBot(getManifest());
|
|
73
|
+
expect(bot["scopes"] as Array<string>).toContain("personal");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("bot is not notification-only (it must receive conversationUpdate events to capture chats)", () => {
|
|
77
|
+
const bot: JSONObject = getBot(getManifest());
|
|
78
|
+
expect(bot["isNotificationOnly"]).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("identity", () => {
|
|
83
|
+
test("bot id equals the Microsoft Teams app client id", () => {
|
|
84
|
+
const bot: JSONObject = getBot(getManifest());
|
|
85
|
+
expect(bot["botId"]).toBe(MOCK_TEAMS_CLIENT_ID);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("manifest id equals the Microsoft Teams app client id", () => {
|
|
89
|
+
const manifest: JSONObject = getManifest();
|
|
90
|
+
expect(manifest["id"]).toBe(MOCK_TEAMS_CLIENT_ID);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("webApplicationInfo id equals the Microsoft Teams app client id", () => {
|
|
94
|
+
const manifest: JSONObject = getManifest();
|
|
95
|
+
const webApplicationInfo: JSONObject = manifest[
|
|
96
|
+
"webApplicationInfo"
|
|
97
|
+
] as JSONObject;
|
|
98
|
+
expect(webApplicationInfo["id"]).toBe(MOCK_TEAMS_CLIENT_ID);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("resource-specific consent permissions", () => {
|
|
103
|
+
test.each([
|
|
104
|
+
"ChannelMessage.Send.Group",
|
|
105
|
+
"ChannelMessage.Read.Group",
|
|
106
|
+
"Channel.Create.Group",
|
|
107
|
+
"ChatMessage.Read.Chat",
|
|
108
|
+
"ChatMember.Read.Chat",
|
|
109
|
+
])("includes the %s permission with type Application", (name: string) => {
|
|
110
|
+
const permissions: Array<JSONObject> =
|
|
111
|
+
getResourceSpecificPermissions(getManifest());
|
|
112
|
+
const match: JSONObject | undefined = permissions.find(
|
|
113
|
+
(permission: JSONObject) => {
|
|
114
|
+
return permission["name"] === name;
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
expect(match).toBeDefined();
|
|
118
|
+
expect(match!["type"]).toBe("Application");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("does NOT include a ChatMessage.Send.Chat permission (not part of Microsoft's RSC catalog - chat sends go through the bot)", () => {
|
|
122
|
+
const permissions: Array<JSONObject> =
|
|
123
|
+
getResourceSpecificPermissions(getManifest());
|
|
124
|
+
const names: Array<string> = permissions.map((permission: JSONObject) => {
|
|
125
|
+
return permission["name"] as string;
|
|
126
|
+
});
|
|
127
|
+
expect(names).not.toContain("ChatMessage.Send.Chat");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("declares exactly the five expected resource-specific permissions", () => {
|
|
131
|
+
const permissions: Array<JSONObject> =
|
|
132
|
+
getResourceSpecificPermissions(getManifest());
|
|
133
|
+
const names: Array<string> = permissions.map((permission: JSONObject) => {
|
|
134
|
+
return permission["name"] as string;
|
|
135
|
+
});
|
|
136
|
+
expect(names.sort()).toEqual(
|
|
137
|
+
[
|
|
138
|
+
"ChannelMessage.Send.Group",
|
|
139
|
+
"ChannelMessage.Read.Group",
|
|
140
|
+
"Channel.Create.Group",
|
|
141
|
+
"ChatMessage.Read.Chat",
|
|
142
|
+
"ChatMember.Read.Chat",
|
|
143
|
+
].sort(),
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("every resource-specific permission is of type Application", () => {
|
|
148
|
+
const permissions: Array<JSONObject> =
|
|
149
|
+
getResourceSpecificPermissions(getManifest());
|
|
150
|
+
for (const permission of permissions) {
|
|
151
|
+
expect(permission["type"]).toBe("Application");
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("top-level permissions stay identity + messageTeamMembers", () => {
|
|
156
|
+
const manifest: JSONObject = getManifest();
|
|
157
|
+
expect(manifest["permissions"]).toEqual([
|
|
158
|
+
"identity",
|
|
159
|
+
"messageTeamMembers",
|
|
160
|
+
]);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe("command lists", () => {
|
|
165
|
+
test("commandLists cover all three scopes (team, groupChat, personal)", () => {
|
|
166
|
+
const bot: JSONObject = getBot(getManifest());
|
|
167
|
+
const commandLists: Array<JSONObject> = bot[
|
|
168
|
+
"commandLists"
|
|
169
|
+
] as Array<JSONObject>;
|
|
170
|
+
const coveredScopes: Set<string> = new Set<string>();
|
|
171
|
+
for (const commandList of commandLists) {
|
|
172
|
+
for (const scope of commandList["scopes"] as Array<string>) {
|
|
173
|
+
coveredScopes.add(scope);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
expect([...coveredScopes].sort()).toEqual(
|
|
177
|
+
["groupChat", "personal", "team"].sort(),
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("every commandList scope is a declared bot scope", () => {
|
|
182
|
+
const bot: JSONObject = getBot(getManifest());
|
|
183
|
+
const botScopes: Array<string> = bot["scopes"] as Array<string>;
|
|
184
|
+
const commandLists: Array<JSONObject> = bot[
|
|
185
|
+
"commandLists"
|
|
186
|
+
] as Array<JSONObject>;
|
|
187
|
+
for (const commandList of commandLists) {
|
|
188
|
+
for (const scope of commandList["scopes"] as Array<string>) {
|
|
189
|
+
expect(botScopes).toContain(scope);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("commandLists include a help command", () => {
|
|
195
|
+
const bot: JSONObject = getBot(getManifest());
|
|
196
|
+
const commandLists: Array<JSONObject> = bot[
|
|
197
|
+
"commandLists"
|
|
198
|
+
] as Array<JSONObject>;
|
|
199
|
+
const commandTitles: Array<string> = commandLists.flatMap(
|
|
200
|
+
(commandList: JSONObject) => {
|
|
201
|
+
return (commandList["commands"] as Array<JSONObject>).map(
|
|
202
|
+
(command: JSONObject) => {
|
|
203
|
+
return command["title"] as string;
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
expect(commandTitles).toContain("help");
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("manifest schema basics", () => {
|
|
213
|
+
test("uses manifest schema version 1.23", () => {
|
|
214
|
+
const manifest: JSONObject = getManifest();
|
|
215
|
+
expect(manifest["manifestVersion"]).toBe("1.23");
|
|
216
|
+
expect(manifest["$schema"]).toBe(
|
|
217
|
+
"https://developer.microsoft.com/json-schemas/teams/v1.23/MicrosoftTeams.schema.json",
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("declares color and outline icons", () => {
|
|
222
|
+
const manifest: JSONObject = getManifest();
|
|
223
|
+
const icons: JSONObject = manifest["icons"] as JSONObject;
|
|
224
|
+
expect(icons["color"]).toBe("color.png");
|
|
225
|
+
expect(icons["outline"]).toBe("outline.png");
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
@@ -71,6 +71,12 @@ describe("ProbeAPI", () => {
|
|
|
71
71
|
connectionStatus: true,
|
|
72
72
|
lastAlive: true,
|
|
73
73
|
iconFileId: true,
|
|
74
|
+
/*
|
|
75
|
+
* The monitor create form pre-selects the probes that auto-enable on
|
|
76
|
+
* new monitors, and global probes are not project rows - this endpoint
|
|
77
|
+
* is the only place the browser can learn the flag for them.
|
|
78
|
+
*/
|
|
79
|
+
shouldAutoEnableProbeOnNewMonitors: true,
|
|
74
80
|
},
|
|
75
81
|
props: {
|
|
76
82
|
isRoot: true,
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import http from "http";
|
|
4
|
+
import { AddressInfo } from "net";
|
|
5
|
+
|
|
6
|
+
jest.mock("../../../Server/Services/WorkspaceProjectAuthTokenService", () => {
|
|
7
|
+
return {
|
|
8
|
+
__esModule: true,
|
|
9
|
+
default: {
|
|
10
|
+
getProjectAuth: jest.fn(async () => {
|
|
11
|
+
return {
|
|
12
|
+
miscData: {
|
|
13
|
+
availableChats: {
|
|
14
|
+
"19:chat-abc": {
|
|
15
|
+
id: "19:chat-abc",
|
|
16
|
+
name: "Jane Doe",
|
|
17
|
+
chatType: "personal",
|
|
18
|
+
addedAt: "2026-07-01T00:00:00.000Z",
|
|
19
|
+
},
|
|
20
|
+
"19:chat-def": {
|
|
21
|
+
id: "19:chat-def",
|
|
22
|
+
name: "Alice, Bob + 2 more",
|
|
23
|
+
chatType: "group",
|
|
24
|
+
addedAt: "2026-07-02T00:00:00.000Z",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
jest.mock("../../../Server/Services/ProjectService", () => {
|
|
35
|
+
return {
|
|
36
|
+
__esModule: true,
|
|
37
|
+
default: {
|
|
38
|
+
updateLastActive: jest.fn(async () => {
|
|
39
|
+
return undefined;
|
|
40
|
+
}),
|
|
41
|
+
getCurrentPlan: jest.fn(async () => {
|
|
42
|
+
return { plan: null, isSubscriptionUnpaid: false };
|
|
43
|
+
}),
|
|
44
|
+
getRequireSsoForLogin: jest.fn(async () => {
|
|
45
|
+
return true;
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
interface HttpProbeResult {
|
|
52
|
+
status: number;
|
|
53
|
+
body: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/*
|
|
57
|
+
* The jest test environment is jsdom, which does not expose a global fetch, so
|
|
58
|
+
* this probe talks to the server with Node's http client. Everything runs
|
|
59
|
+
* inside the caller's try/finally so the server is always closed - a leaked
|
|
60
|
+
* listener keeps the event loop alive and hangs the whole `jest` run at exit.
|
|
61
|
+
*/
|
|
62
|
+
type HttpGetJsonFunction = (data: {
|
|
63
|
+
port: number;
|
|
64
|
+
path: string;
|
|
65
|
+
headers: http.OutgoingHttpHeaders;
|
|
66
|
+
}) => Promise<HttpProbeResult>;
|
|
67
|
+
|
|
68
|
+
const httpGetJson: HttpGetJsonFunction = (data: {
|
|
69
|
+
port: number;
|
|
70
|
+
path: string;
|
|
71
|
+
headers: http.OutgoingHttpHeaders;
|
|
72
|
+
}): Promise<HttpProbeResult> => {
|
|
73
|
+
return new Promise<HttpProbeResult>(
|
|
74
|
+
(
|
|
75
|
+
resolve: (result: HttpProbeResult) => void,
|
|
76
|
+
reject: (error: Error) => void,
|
|
77
|
+
) => {
|
|
78
|
+
const request: http.ClientRequest = http.request(
|
|
79
|
+
{
|
|
80
|
+
host: "127.0.0.1",
|
|
81
|
+
port: data.port,
|
|
82
|
+
path: data.path,
|
|
83
|
+
method: "GET",
|
|
84
|
+
headers: data.headers,
|
|
85
|
+
},
|
|
86
|
+
(response: http.IncomingMessage) => {
|
|
87
|
+
const chunks: Array<Buffer> = [];
|
|
88
|
+
|
|
89
|
+
response.on("data", (chunk: Buffer) => {
|
|
90
|
+
chunks.push(chunk);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
response.on("end", () => {
|
|
94
|
+
const raw: string = Buffer.concat(chunks).toString("utf8");
|
|
95
|
+
|
|
96
|
+
let parsed: unknown = null;
|
|
97
|
+
try {
|
|
98
|
+
parsed = raw ? JSON.parse(raw) : null;
|
|
99
|
+
} catch {
|
|
100
|
+
parsed = raw;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
resolve({
|
|
104
|
+
status: response.statusCode || 0,
|
|
105
|
+
body: parsed,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
request.on("error", (error: Error) => {
|
|
112
|
+
return reject(error);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
request.end();
|
|
116
|
+
},
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
describe("ANON ACCESS PROBE /microsoft-teams/chats", () => {
|
|
121
|
+
test("unauthenticated GET with only a tenantid header", async () => {
|
|
122
|
+
const MicrosoftTeamsAPI: any = (
|
|
123
|
+
await import("../../../Server/API/MicrosoftTeamsAPI")
|
|
124
|
+
).default;
|
|
125
|
+
|
|
126
|
+
const app: express.Express = express();
|
|
127
|
+
app.use("/api", new MicrosoftTeamsAPI().getRouter());
|
|
128
|
+
|
|
129
|
+
const server: http.Server = http.createServer(app);
|
|
130
|
+
await new Promise<void>((resolve: () => void) => {
|
|
131
|
+
server.listen(0, resolve);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
const port: number = (server.address() as AddressInfo).port;
|
|
136
|
+
|
|
137
|
+
const result: HttpProbeResult = await httpGetJson({
|
|
138
|
+
port,
|
|
139
|
+
path: "/api/microsoft-teams/chats",
|
|
140
|
+
headers: {
|
|
141
|
+
tenantid: "6f4e4b2c-1111-4c2a-9e3d-abcdefabcdef",
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
/*
|
|
146
|
+
* The hardening in this branch requires an authenticated project member:
|
|
147
|
+
* getUserMiddleware admits unauthenticated requests as Public, so the
|
|
148
|
+
* route must reject one carrying only a tenantid header. A 200 here would
|
|
149
|
+
* mean the captured chat list leaks to anonymous callers - the exact
|
|
150
|
+
* regression this probe guards against.
|
|
151
|
+
*/
|
|
152
|
+
expect(result.status).toBeGreaterThanOrEqual(400);
|
|
153
|
+
expect(result.status).toBeLessThan(500);
|
|
154
|
+
|
|
155
|
+
const serializedBody: string = JSON.stringify(result.body);
|
|
156
|
+
expect(serializedBody).not.toContain("Jane Doe");
|
|
157
|
+
expect(serializedBody).not.toContain("19:chat-abc");
|
|
158
|
+
expect(serializedBody).not.toContain("19:chat-def");
|
|
159
|
+
} finally {
|
|
160
|
+
await new Promise<void>((resolve: () => void) => {
|
|
161
|
+
server.close(() => {
|
|
162
|
+
return resolve();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}, 30000);
|
|
167
|
+
});
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import Incident from "../../../Models/DatabaseModels/Incident";
|
|
2
|
+
import IncidentEpisode from "../../../Models/DatabaseModels/IncidentEpisode";
|
|
3
|
+
import IncidentEpisodePublicNote from "../../../Models/DatabaseModels/IncidentEpisodePublicNote";
|
|
4
|
+
import IncidentEpisodeStateTimeline from "../../../Models/DatabaseModels/IncidentEpisodeStateTimeline";
|
|
5
|
+
import IncidentPublicNote from "../../../Models/DatabaseModels/IncidentPublicNote";
|
|
6
|
+
import IncidentStateTimeline from "../../../Models/DatabaseModels/IncidentStateTimeline";
|
|
7
|
+
import Monitor from "../../../Models/DatabaseModels/Monitor";
|
|
8
|
+
import OnCallDutyPolicyExecutionLog from "../../../Models/DatabaseModels/OnCallDutyPolicyExecutionLog";
|
|
9
|
+
import ScheduledMaintenance from "../../../Models/DatabaseModels/ScheduledMaintenance";
|
|
10
|
+
import ScheduledMaintenancePublicNote from "../../../Models/DatabaseModels/ScheduledMaintenancePublicNote";
|
|
11
|
+
import ScheduledMaintenanceStateTimeline from "../../../Models/DatabaseModels/ScheduledMaintenanceStateTimeline";
|
|
12
|
+
import StatusPageAnnouncement from "../../../Models/DatabaseModels/StatusPageAnnouncement";
|
|
13
|
+
import { AddHotQueryIndexes1785140242697 } from "../../../Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes";
|
|
14
|
+
import SchemaMigrations from "../../../Server/Infrastructure/Postgres/SchemaMigrations/Index";
|
|
15
|
+
import { QueryRunner, getMetadataArgsStorage } from "typeorm";
|
|
16
|
+
import type { IndexMetadataArgs } from "typeorm/metadata-args/IndexMetadataArgs";
|
|
17
|
+
import { describe, expect, test } from "@jest/globals";
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* The 13 columns below are the filter columns of ~13 EVERY_MINUTE worker
|
|
21
|
+
* queries (the on-call executor crons scan OnCallDutyPolicyExecutionLog.status,
|
|
22
|
+
* the every-30s heartbeat crons branch on Monitor.monitorType, and the 11
|
|
23
|
+
* subscriberNotificationStatus* columns map 1:1 to the EVERY_MINUTE
|
|
24
|
+
* SendNotificationToSubscribers jobs that poll ever-growing tables for
|
|
25
|
+
* usually-zero Pending rows). Before AddHotQueryIndexes1785140242697 every one
|
|
26
|
+
* of those queries was a full-table seq scan.
|
|
27
|
+
*
|
|
28
|
+
* These tests pin the three pieces that must all hold for the scans to stay
|
|
29
|
+
* gone — a regression in ANY one silently reintroduces them:
|
|
30
|
+
* 1. the @Index() decorator on each entity property (keeps future
|
|
31
|
+
* schema:generate runs from emitting a DROP INDEX as "drift"),
|
|
32
|
+
* 2. the migration's SQL contract: up() creates exactly these 13 indexes
|
|
33
|
+
* and nothing else (no ALTER TABLE drift statements from a regenerated
|
|
34
|
+
* migration), down() drops exactly the indexes up() created,
|
|
35
|
+
* 3. the migration's registration in SchemaMigrations/Index.ts (an
|
|
36
|
+
* unregistered migration is dead code and never runs on boot).
|
|
37
|
+
*
|
|
38
|
+
* Pure metadata/mocks — no Postgres connection anywhere.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
type ModelClass = { new (): unknown };
|
|
42
|
+
|
|
43
|
+
// [entity class, table name (=== class name for all of these), indexed column]
|
|
44
|
+
const HOT_INDEX_PAIRS: Array<[ModelClass, string, string]> = [
|
|
45
|
+
[Monitor, "Monitor", "monitorType"],
|
|
46
|
+
[OnCallDutyPolicyExecutionLog, "OnCallDutyPolicyExecutionLog", "status"],
|
|
47
|
+
[
|
|
48
|
+
IncidentStateTimeline,
|
|
49
|
+
"IncidentStateTimeline",
|
|
50
|
+
"subscriberNotificationStatus",
|
|
51
|
+
],
|
|
52
|
+
[Incident, "Incident", "subscriberNotificationStatusOnIncidentCreated"],
|
|
53
|
+
[Incident, "Incident", "subscriberNotificationStatusOnPostmortemPublished"],
|
|
54
|
+
[
|
|
55
|
+
IncidentPublicNote,
|
|
56
|
+
"IncidentPublicNote",
|
|
57
|
+
"subscriberNotificationStatusOnNoteCreated",
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
IncidentEpisode,
|
|
61
|
+
"IncidentEpisode",
|
|
62
|
+
"subscriberNotificationStatusOnEpisodeCreated",
|
|
63
|
+
],
|
|
64
|
+
[
|
|
65
|
+
IncidentEpisodeStateTimeline,
|
|
66
|
+
"IncidentEpisodeStateTimeline",
|
|
67
|
+
"subscriberNotificationStatus",
|
|
68
|
+
],
|
|
69
|
+
[
|
|
70
|
+
IncidentEpisodePublicNote,
|
|
71
|
+
"IncidentEpisodePublicNote",
|
|
72
|
+
"subscriberNotificationStatusOnNoteCreated",
|
|
73
|
+
],
|
|
74
|
+
[
|
|
75
|
+
ScheduledMaintenance,
|
|
76
|
+
"ScheduledMaintenance",
|
|
77
|
+
"subscriberNotificationStatusOnEventScheduled",
|
|
78
|
+
],
|
|
79
|
+
[
|
|
80
|
+
ScheduledMaintenanceStateTimeline,
|
|
81
|
+
"ScheduledMaintenanceStateTimeline",
|
|
82
|
+
"subscriberNotificationStatus",
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
ScheduledMaintenancePublicNote,
|
|
86
|
+
"ScheduledMaintenancePublicNote",
|
|
87
|
+
"subscriberNotificationStatusOnNoteCreated",
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
StatusPageAnnouncement,
|
|
91
|
+
"StatusPageAnnouncement",
|
|
92
|
+
"subscriberNotificationStatus",
|
|
93
|
+
],
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
/*
|
|
97
|
+
* IndexMetadataArgs.columns is either an array of property names (property-
|
|
98
|
+
* level @Index()) or a function over the entity's properties map (class-level
|
|
99
|
+
* @Index((o) => [o.a, o.b])). Resolve both to plain property-name arrays; the
|
|
100
|
+
* function form is fed a proxy whose every property reads as its own name,
|
|
101
|
+
* which is exactly how TypeORM's propertiesMap behaves for flat columns.
|
|
102
|
+
*/
|
|
103
|
+
function resolveIndexColumns(
|
|
104
|
+
columns: IndexMetadataArgs["columns"],
|
|
105
|
+
): Array<string> {
|
|
106
|
+
if (!columns) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(columns)) {
|
|
110
|
+
return columns.map(String);
|
|
111
|
+
}
|
|
112
|
+
const propertiesMap: Record<string, string> = new Proxy(
|
|
113
|
+
{},
|
|
114
|
+
{
|
|
115
|
+
get: (_target: Record<string, string>, property: string | symbol) => {
|
|
116
|
+
return String(property);
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
const resolved: Array<unknown> | { [key: string]: number } =
|
|
121
|
+
columns(propertiesMap);
|
|
122
|
+
if (Array.isArray(resolved)) {
|
|
123
|
+
return resolved.map(String);
|
|
124
|
+
}
|
|
125
|
+
return Object.keys(resolved);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
interface CreateIndexStatement {
|
|
129
|
+
indexName: string;
|
|
130
|
+
tableName: string;
|
|
131
|
+
columnName: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const CREATE_INDEX_REGEX: RegExp =
|
|
135
|
+
/^CREATE INDEX "([^"]+)" ON "([^"]+)" \("([^"]+)"\)\s*$/;
|
|
136
|
+
const DROP_INDEX_REGEX: RegExp = /^DROP INDEX "public"\."([^"]+)"$/;
|
|
137
|
+
|
|
138
|
+
function makeQueryRunner(): { runner: QueryRunner; query: jest.Mock } {
|
|
139
|
+
const query: jest.Mock = jest.fn().mockResolvedValue(undefined);
|
|
140
|
+
return { runner: { query } as unknown as QueryRunner, query };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function executedSql(query: jest.Mock): Array<string> {
|
|
144
|
+
return query.mock.calls.map((call: Array<unknown>) => {
|
|
145
|
+
return String(call[0]);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
describe("AddHotQueryIndexes: entity @Index() decorators", () => {
|
|
150
|
+
test.each(
|
|
151
|
+
HOT_INDEX_PAIRS.map(
|
|
152
|
+
([model, table, column]: [ModelClass, string, string]) => {
|
|
153
|
+
return [table, column, model] as [string, string, ModelClass];
|
|
154
|
+
},
|
|
155
|
+
),
|
|
156
|
+
)(
|
|
157
|
+
"%s.%s has a single-column index decorator",
|
|
158
|
+
(_table: string, column: string, model: ModelClass) => {
|
|
159
|
+
const singleColumnIndexes: Array<string> = getMetadataArgsStorage()
|
|
160
|
+
.indices.filter((index: IndexMetadataArgs) => {
|
|
161
|
+
return index.target === model;
|
|
162
|
+
})
|
|
163
|
+
.map((index: IndexMetadataArgs) => {
|
|
164
|
+
return resolveIndexColumns(index.columns);
|
|
165
|
+
})
|
|
166
|
+
.filter((columns: Array<string>) => {
|
|
167
|
+
return columns.length === 1;
|
|
168
|
+
})
|
|
169
|
+
.map((columns: Array<string>) => {
|
|
170
|
+
return columns[0] as string;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(singleColumnIndexes).toContain(column);
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("AddHotQueryIndexes1785140242697 migration SQL contract", () => {
|
|
179
|
+
const migration: AddHotQueryIndexes1785140242697 =
|
|
180
|
+
new AddHotQueryIndexes1785140242697();
|
|
181
|
+
|
|
182
|
+
test("up() issues exactly 13 CREATE INDEX statements covering exactly the 13 hot columns — and nothing else", async () => {
|
|
183
|
+
const { runner, query } = makeQueryRunner();
|
|
184
|
+
await migration.up(runner);
|
|
185
|
+
|
|
186
|
+
const statements: Array<string> = executedSql(query);
|
|
187
|
+
expect(statements).toHaveLength(13);
|
|
188
|
+
|
|
189
|
+
/*
|
|
190
|
+
* A regenerated migration on a drifted schema would interleave ALTER
|
|
191
|
+
* TABLE / DROP statements with the index creates; none may sneak in.
|
|
192
|
+
*/
|
|
193
|
+
for (const sql of statements) {
|
|
194
|
+
expect(sql).not.toContain("ALTER TABLE");
|
|
195
|
+
expect(sql).not.toContain("DROP");
|
|
196
|
+
expect(sql).toMatch(CREATE_INDEX_REGEX);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const created: Array<CreateIndexStatement> = statements.map(
|
|
200
|
+
(sql: string) => {
|
|
201
|
+
const match: RegExpMatchArray = sql.match(
|
|
202
|
+
CREATE_INDEX_REGEX,
|
|
203
|
+
) as RegExpMatchArray;
|
|
204
|
+
return {
|
|
205
|
+
indexName: match[1] as string,
|
|
206
|
+
tableName: match[2] as string,
|
|
207
|
+
columnName: match[3] as string,
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const createdPairs: Array<string> = created
|
|
213
|
+
.map((statement: CreateIndexStatement) => {
|
|
214
|
+
return `"${statement.tableName}"."${statement.columnName}"`;
|
|
215
|
+
})
|
|
216
|
+
.sort();
|
|
217
|
+
const expectedPairs: Array<string> = HOT_INDEX_PAIRS.map(
|
|
218
|
+
([, table, column]: [ModelClass, string, string]) => {
|
|
219
|
+
return `"${table}"."${column}"`;
|
|
220
|
+
},
|
|
221
|
+
).sort();
|
|
222
|
+
|
|
223
|
+
expect(createdPairs).toEqual(expectedPairs);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("down() drops exactly the 13 indexes up() created", async () => {
|
|
227
|
+
const upRunner: { runner: QueryRunner; query: jest.Mock } =
|
|
228
|
+
makeQueryRunner();
|
|
229
|
+
await migration.up(upRunner.runner);
|
|
230
|
+
const createdNames: Array<string> = executedSql(upRunner.query).map(
|
|
231
|
+
(sql: string) => {
|
|
232
|
+
return (sql.match(CREATE_INDEX_REGEX) as RegExpMatchArray)[1] as string;
|
|
233
|
+
},
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const downRunner: { runner: QueryRunner; query: jest.Mock } =
|
|
237
|
+
makeQueryRunner();
|
|
238
|
+
await migration.down(downRunner.runner);
|
|
239
|
+
const statements: Array<string> = executedSql(downRunner.query);
|
|
240
|
+
expect(statements).toHaveLength(13);
|
|
241
|
+
|
|
242
|
+
const droppedNames: Array<string> = statements.map((sql: string) => {
|
|
243
|
+
expect(sql).toMatch(DROP_INDEX_REGEX);
|
|
244
|
+
return (sql.match(DROP_INDEX_REGEX) as RegExpMatchArray)[1] as string;
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect([...droppedNames].sort()).toEqual([...createdNames].sort());
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe("AddHotQueryIndexes1785140242697 registration", () => {
|
|
252
|
+
test("is registered in SchemaMigrations/Index.ts so it actually runs on boot", () => {
|
|
253
|
+
expect(SchemaMigrations).toContain(AddHotQueryIndexes1785140242697);
|
|
254
|
+
});
|
|
255
|
+
});
|