@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,181 @@
|
|
|
1
|
+
import SmsLogService, {
|
|
2
|
+
Service as SmsLogServiceClass,
|
|
3
|
+
} from "../../../Server/Services/SmsLogService";
|
|
4
|
+
import WorkflowLogService, {
|
|
5
|
+
Service as WorkflowLogServiceClass,
|
|
6
|
+
} from "../../../Server/Services/WorkflowLogService";
|
|
7
|
+
import DatabaseService from "../../../Server/Services/DatabaseService";
|
|
8
|
+
import SmsLog from "../../../Models/DatabaseModels/SmsLog";
|
|
9
|
+
import WorkflowLog from "../../../Models/DatabaseModels/WorkflowLog";
|
|
10
|
+
import { describe, expect, test } from "@jest/globals";
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Second-pass hookless-write conversions: SmsLog and WorkflowLog status
|
|
14
|
+
* stamps were moved off the full updateOneById pipeline onto
|
|
15
|
+
* DatabaseService.updateColumnsByIdWithoutHooks (one raw parameterized
|
|
16
|
+
* UPDATE; skips ALL on-update hooks — workflow HTTP triggers, audit-log
|
|
17
|
+
* inserts, realtime events, service onBeforeUpdate/onUpdateSuccess):
|
|
18
|
+
*
|
|
19
|
+
* - App/FeatureSet/Notification/Services/SmsService.ts
|
|
20
|
+
* SmsLog.status/statusMessage/smsCostInUSDCents/errorCode after send
|
|
21
|
+
* - App/FeatureSet/Notification/API/SMS.ts
|
|
22
|
+
* SmsLog.status/statusMessage/errorCode on delivery callbacks
|
|
23
|
+
* - App/FeatureSet/Workflow/Services/RunWorkflow.ts
|
|
24
|
+
* WorkflowLog.workflowStatus/logs/started-completed-resume stamps
|
|
25
|
+
* - App/FeatureSet/Workers/Jobs/Workflow/TimeoutJobs.ts
|
|
26
|
+
* WorkflowLog.workflowStatus/completedAt/logs on stalled runs
|
|
27
|
+
*
|
|
28
|
+
* These conversions dropped NOTHING: neither model has update workflows,
|
|
29
|
+
* audit logging, or realtime events, and neither service overrides the
|
|
30
|
+
* update hooks — so the old pipeline's hook stages were inert for these
|
|
31
|
+
* writes. But the fast path skips hooks UNCONDITIONALLY: if someone later
|
|
32
|
+
* adds a decorator to SmsLog/WorkflowLog or an onUpdateSuccess override to
|
|
33
|
+
* their services, nothing at the call sites fails — the new hook is just
|
|
34
|
+
* silently never fired for these status stamps. This suite turns that
|
|
35
|
+
* silent drift into a loud test failure: when an assertion here starts
|
|
36
|
+
* failing, revisit the fast-path call sites above before changing it.
|
|
37
|
+
*
|
|
38
|
+
* Pure model-metadata + class-shape tests — no Postgres, no Redis.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
describe("second-pass hookless write safety preconditions", () => {
|
|
42
|
+
describe("SmsLog model (status stamps in SmsService.ts / SMS.ts)", () => {
|
|
43
|
+
/*
|
|
44
|
+
* SmsLog DOES declare @EnableWorkflow — but with update: false. The
|
|
45
|
+
* distinguishing shape (metadata present, update explicitly off) is
|
|
46
|
+
* pinned exactly so that flipping update to true is caught even though
|
|
47
|
+
* `enableWorkflowOn?.update` would also be falsy on a model with no
|
|
48
|
+
* decorator at all.
|
|
49
|
+
*/
|
|
50
|
+
test("declares @EnableWorkflow with update explicitly disabled", () => {
|
|
51
|
+
const smsLog: SmsLog = new SmsLog();
|
|
52
|
+
expect(smsLog.enableWorkflowOn).toBeDefined();
|
|
53
|
+
expect(smsLog.enableWorkflowOn?.create).toBe(true);
|
|
54
|
+
expect(smsLog.enableWorkflowOn?.update).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("has no on-update audit log", () => {
|
|
58
|
+
const smsLog: SmsLog = new SmsLog();
|
|
59
|
+
expect(smsLog.enableAuditLogOn?.update).toBeFalsy();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("has no realtime events", () => {
|
|
63
|
+
const smsLog: SmsLog = new SmsLog();
|
|
64
|
+
expect(smsLog.enableRealtimeEventsOn).toBeFalsy();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("WorkflowLog model (status stamps in RunWorkflow.ts / TimeoutJobs.ts)", () => {
|
|
69
|
+
/*
|
|
70
|
+
* WorkflowLog has no @EnableWorkflow decorator AT ALL — the accessor is
|
|
71
|
+
* entirely unset, which is the other shape that resolves to "no update
|
|
72
|
+
* workflow". If this becomes defined, someone added @EnableWorkflow to
|
|
73
|
+
* WorkflowLog and must decide whether the fast-path stamps should keep
|
|
74
|
+
* skipping it.
|
|
75
|
+
*/
|
|
76
|
+
test("has no @EnableWorkflow metadata at all", () => {
|
|
77
|
+
const workflowLog: WorkflowLog = new WorkflowLog();
|
|
78
|
+
expect(workflowLog.enableWorkflowOn).toBeFalsy();
|
|
79
|
+
expect(workflowLog.enableWorkflowOn?.update).toBeFalsy();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("has no on-update audit log", () => {
|
|
83
|
+
const workflowLog: WorkflowLog = new WorkflowLog();
|
|
84
|
+
expect(workflowLog.enableAuditLogOn?.update).toBeFalsy();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("has no realtime events", () => {
|
|
88
|
+
const workflowLog: WorkflowLog = new WorkflowLog();
|
|
89
|
+
expect(workflowLog.enableRealtimeEventsOn).toBeFalsy();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("fast-path columns exist on their entities", () => {
|
|
94
|
+
/*
|
|
95
|
+
* updateColumnsByIdWithoutHooks validates column names against entity
|
|
96
|
+
* metadata at runtime and throws BadDataException on an unknown column.
|
|
97
|
+
* Pinning column existence here means a rename breaks this suite at CI
|
|
98
|
+
* time instead of breaking every SMS/workflow status stamp at runtime.
|
|
99
|
+
*/
|
|
100
|
+
test("SmsLog has every column the SMS status stamps write", () => {
|
|
101
|
+
const smsLog: SmsLog = new SmsLog();
|
|
102
|
+
const fastPathColumns: Array<string> = [
|
|
103
|
+
"status",
|
|
104
|
+
"statusMessage",
|
|
105
|
+
"smsCostInUSDCents",
|
|
106
|
+
"errorCode",
|
|
107
|
+
];
|
|
108
|
+
for (const column of fastPathColumns) {
|
|
109
|
+
expect(smsLog.isTableColumn(column)).toBe(true);
|
|
110
|
+
}
|
|
111
|
+
// Negative control: isTableColumn actually discriminates.
|
|
112
|
+
expect(smsLog.isTableColumn("notARealColumn")).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("WorkflowLog has every column the workflow status stamps write", () => {
|
|
116
|
+
const workflowLog: WorkflowLog = new WorkflowLog();
|
|
117
|
+
const fastPathColumns: Array<string> = [
|
|
118
|
+
"workflowStatus",
|
|
119
|
+
"logs",
|
|
120
|
+
"startedAt",
|
|
121
|
+
"completedAt",
|
|
122
|
+
"resumeAt",
|
|
123
|
+
"resumeData",
|
|
124
|
+
];
|
|
125
|
+
for (const column of fastPathColumns) {
|
|
126
|
+
expect(workflowLog.isTableColumn(column)).toBe(true);
|
|
127
|
+
}
|
|
128
|
+
expect(workflowLog.isTableColumn("notARealColumn")).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("services define no update hooks of their own", () => {
|
|
133
|
+
/*
|
|
134
|
+
* DatabaseService's base onBeforeUpdate/onUpdateSuccess are no-op
|
|
135
|
+
* pass-throughs; a service only gets update behavior by OVERRIDING them.
|
|
136
|
+
* Own-property checks on the concrete service prototypes pin that
|
|
137
|
+
* neither SmsLogService nor WorkflowLogService does — so the fast path
|
|
138
|
+
* skips nothing. If an override appears, these fail loudly and the
|
|
139
|
+
* call sites above must be re-evaluated.
|
|
140
|
+
*/
|
|
141
|
+
const updateHooks: Array<string> = ["onBeforeUpdate", "onUpdateSuccess"];
|
|
142
|
+
|
|
143
|
+
test("SmsLogService does not override onBeforeUpdate/onUpdateSuccess", () => {
|
|
144
|
+
for (const hook of updateHooks) {
|
|
145
|
+
expect(
|
|
146
|
+
Object.prototype.hasOwnProperty.call(
|
|
147
|
+
SmsLogServiceClass.prototype,
|
|
148
|
+
hook,
|
|
149
|
+
),
|
|
150
|
+
).toBe(false);
|
|
151
|
+
}
|
|
152
|
+
// The default export is an instance of the class checked above.
|
|
153
|
+
expect(SmsLogService).toBeInstanceOf(SmsLogServiceClass);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("WorkflowLogService does not override onBeforeUpdate/onUpdateSuccess", () => {
|
|
157
|
+
for (const hook of updateHooks) {
|
|
158
|
+
expect(
|
|
159
|
+
Object.prototype.hasOwnProperty.call(
|
|
160
|
+
WorkflowLogServiceClass.prototype,
|
|
161
|
+
hook,
|
|
162
|
+
),
|
|
163
|
+
).toBe(false);
|
|
164
|
+
}
|
|
165
|
+
expect(WorkflowLogService).toBeInstanceOf(WorkflowLogServiceClass);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
/*
|
|
169
|
+
* Positive control: the hooks DO exist on the DatabaseService base
|
|
170
|
+
* prototype, so the own-property checks above cannot pass vacuously
|
|
171
|
+
* (e.g. after a rename of the hook methods themselves).
|
|
172
|
+
*/
|
|
173
|
+
test("the base DatabaseService prototype defines both hooks", () => {
|
|
174
|
+
for (const hook of updateHooks) {
|
|
175
|
+
expect(
|
|
176
|
+
Object.prototype.hasOwnProperty.call(DatabaseService.prototype, hook),
|
|
177
|
+
).toBe(true);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* MonitorGroupService batched status helpers.
|
|
3
|
+
*
|
|
4
|
+
* The public status-page read paths used to issue one MonitorGroupResource
|
|
5
|
+
* query per monitor group per page view (plus a MonitorGroup and a
|
|
6
|
+
* MonitorStatus fetch each in getCurrentStatus) — an N+1 of 3-4 queries per
|
|
7
|
+
* group per request. getMonitorGroupResourcesByGroupIds /
|
|
8
|
+
* getMonitorIdsInMonitorGroups / getCurrentStatusesForMonitorGroups collapse
|
|
9
|
+
* that to at most ONE batched query. These tests pin:
|
|
10
|
+
* - exactly one findAllBy regardless of group count, keyed by
|
|
11
|
+
* QueryHelper.any over DEDUPED ids, with an entry ([] if empty) per
|
|
12
|
+
* requested group and zero queries for empty input,
|
|
13
|
+
* - getCurrentStatusesForMonitorGroups preserving getCurrentStatus's EXACT
|
|
14
|
+
* semantics (operational default, HIGHER priority number wins, throws
|
|
15
|
+
* when no operational state exists), including a direct equivalence check
|
|
16
|
+
* against the single-group path so the two cannot silently drift, and
|
|
17
|
+
* - no resource fetch at all when the caller already supplies the
|
|
18
|
+
* group-resource dictionary.
|
|
19
|
+
*
|
|
20
|
+
* All service reads are spied on — no database is touched.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import MonitorGroupService from "../../../Server/Services/MonitorGroupService";
|
|
24
|
+
import MonitorGroupResourceService from "../../../Server/Services/MonitorGroupResourceService";
|
|
25
|
+
import MonitorStatusService from "../../../Server/Services/MonitorStatusService";
|
|
26
|
+
import QueryHelper from "../../../Server/Types/Database/QueryHelper";
|
|
27
|
+
import MonitorGroup from "../../../Models/DatabaseModels/MonitorGroup";
|
|
28
|
+
import MonitorGroupResource from "../../../Models/DatabaseModels/MonitorGroupResource";
|
|
29
|
+
import MonitorStatus from "../../../Models/DatabaseModels/MonitorStatus";
|
|
30
|
+
import Monitor from "../../../Models/DatabaseModels/Monitor";
|
|
31
|
+
import Dictionary from "../../../Types/Dictionary";
|
|
32
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
33
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
34
|
+
import { afterEach, describe, expect, jest, test } from "@jest/globals";
|
|
35
|
+
|
|
36
|
+
function makeStatus(data: {
|
|
37
|
+
priority: number;
|
|
38
|
+
isOperationalState?: boolean;
|
|
39
|
+
name?: string;
|
|
40
|
+
}): MonitorStatus {
|
|
41
|
+
const status: MonitorStatus = new MonitorStatus();
|
|
42
|
+
status.id = ObjectID.generate();
|
|
43
|
+
status.priority = data.priority;
|
|
44
|
+
status.isOperationalState = data.isOperationalState || false;
|
|
45
|
+
status.name = data.name || `status-p${data.priority}`;
|
|
46
|
+
return status;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function makeGroupResource(data: {
|
|
50
|
+
monitorGroupId?: ObjectID | undefined;
|
|
51
|
+
monitorId?: ObjectID | undefined;
|
|
52
|
+
currentMonitorStatusId?: ObjectID | undefined;
|
|
53
|
+
omitMonitor?: boolean;
|
|
54
|
+
}): MonitorGroupResource {
|
|
55
|
+
const resource: MonitorGroupResource = new MonitorGroupResource();
|
|
56
|
+
if (data.monitorGroupId) {
|
|
57
|
+
resource.monitorGroupId = data.monitorGroupId;
|
|
58
|
+
}
|
|
59
|
+
if (data.monitorId) {
|
|
60
|
+
resource.monitorId = data.monitorId;
|
|
61
|
+
}
|
|
62
|
+
if (!data.omitMonitor) {
|
|
63
|
+
const monitor: Monitor = new Monitor();
|
|
64
|
+
if (data.currentMonitorStatusId) {
|
|
65
|
+
monitor.currentMonitorStatusId = data.currentMonitorStatusId;
|
|
66
|
+
}
|
|
67
|
+
resource.monitor = monitor;
|
|
68
|
+
}
|
|
69
|
+
return resource;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function spyOnFindAllBy(rows: Array<MonitorGroupResource>): jest.SpyInstance {
|
|
73
|
+
return (
|
|
74
|
+
jest.spyOn(
|
|
75
|
+
MonitorGroupResourceService,
|
|
76
|
+
"findAllBy",
|
|
77
|
+
) as unknown as jest.SpyInstance
|
|
78
|
+
).mockResolvedValue(rows as never);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toStrings(ids: Array<ObjectID>): Array<string> {
|
|
82
|
+
return ids.map((id: ObjectID) => {
|
|
83
|
+
return id.toString();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("MonitorGroupService batched status helpers", () => {
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
jest.restoreAllMocks();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("getMonitorGroupResourcesByGroupIds", () => {
|
|
93
|
+
test("issues exactly one findAllBy for N groups, keyed by QueryHelper.any over deduped ids", async () => {
|
|
94
|
+
const groupA: ObjectID = ObjectID.generate();
|
|
95
|
+
const groupB: ObjectID = ObjectID.generate();
|
|
96
|
+
const groupC: ObjectID = ObjectID.generate();
|
|
97
|
+
|
|
98
|
+
const rowA1: MonitorGroupResource = makeGroupResource({
|
|
99
|
+
monitorGroupId: groupA,
|
|
100
|
+
monitorId: ObjectID.generate(),
|
|
101
|
+
});
|
|
102
|
+
const rowA2: MonitorGroupResource = makeGroupResource({
|
|
103
|
+
monitorGroupId: groupA,
|
|
104
|
+
monitorId: ObjectID.generate(),
|
|
105
|
+
});
|
|
106
|
+
const rowB1: MonitorGroupResource = makeGroupResource({
|
|
107
|
+
monitorGroupId: groupB,
|
|
108
|
+
monitorId: ObjectID.generate(),
|
|
109
|
+
});
|
|
110
|
+
// Defensive-guard row: a row without monitorGroupId must be dropped, not crash.
|
|
111
|
+
const rowNoGroup: MonitorGroupResource = makeGroupResource({
|
|
112
|
+
monitorId: ObjectID.generate(),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const findAllBySpy: jest.SpyInstance = spyOnFindAllBy([
|
|
116
|
+
rowA1,
|
|
117
|
+
rowB1,
|
|
118
|
+
rowA2,
|
|
119
|
+
rowNoGroup,
|
|
120
|
+
]);
|
|
121
|
+
const anySpy: jest.SpyInstance = jest.spyOn(
|
|
122
|
+
QueryHelper,
|
|
123
|
+
"any",
|
|
124
|
+
) as unknown as jest.SpyInstance;
|
|
125
|
+
|
|
126
|
+
const result: Dictionary<Array<MonitorGroupResource>> =
|
|
127
|
+
await MonitorGroupService.getMonitorGroupResourcesByGroupIds([
|
|
128
|
+
groupA,
|
|
129
|
+
groupB,
|
|
130
|
+
groupA, // duplicate must collapse
|
|
131
|
+
groupC,
|
|
132
|
+
groupB, // duplicate must collapse
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
// The whole point of the batching: ONE query for all groups.
|
|
136
|
+
expect(findAllBySpy).toHaveBeenCalledTimes(1);
|
|
137
|
+
|
|
138
|
+
// The IN-list is built via QueryHelper.any over DEDUPED ids, in first-seen order.
|
|
139
|
+
expect(anySpy).toHaveBeenCalledTimes(1);
|
|
140
|
+
expect(toStrings(anySpy.mock.calls[0]![0] as Array<ObjectID>)).toEqual(
|
|
141
|
+
toStrings([groupA, groupB, groupC]),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const callArg: any = findAllBySpy.mock.calls[0]![0];
|
|
145
|
+
// The exact FindOperator QueryHelper.any built is what findAllBy receives.
|
|
146
|
+
expect(callArg.query.monitorGroupId).toBe(anySpy.mock.results[0]!.value);
|
|
147
|
+
// The select must carry the grouping key and the member's current status.
|
|
148
|
+
expect(callArg.select).toEqual({
|
|
149
|
+
monitorGroupId: true,
|
|
150
|
+
monitorId: true,
|
|
151
|
+
monitor: {
|
|
152
|
+
currentMonitorStatusId: true,
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
expect(callArg.props).toEqual({ isRoot: true });
|
|
156
|
+
|
|
157
|
+
// Every requested group gets an entry; a group with no rows gets [].
|
|
158
|
+
expect(Object.keys(result).sort()).toEqual(
|
|
159
|
+
[groupA.toString(), groupB.toString(), groupC.toString()].sort(),
|
|
160
|
+
);
|
|
161
|
+
expect(result[groupA.toString()]).toEqual([rowA1, rowA2]);
|
|
162
|
+
expect(result[groupB.toString()]).toEqual([rowB1]);
|
|
163
|
+
expect(result[groupC.toString()]).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("empty input returns {} without querying at all", async () => {
|
|
167
|
+
const findAllBySpy: jest.SpyInstance = spyOnFindAllBy([]);
|
|
168
|
+
|
|
169
|
+
const result: Dictionary<Array<MonitorGroupResource>> =
|
|
170
|
+
await MonitorGroupService.getMonitorGroupResourcesByGroupIds([]);
|
|
171
|
+
|
|
172
|
+
expect(result).toEqual({});
|
|
173
|
+
expect(findAllBySpy).not.toHaveBeenCalled();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("getMonitorIdsInMonitorGroups", () => {
|
|
178
|
+
test("maps rows to monitor ids, drops null monitorIds, keeps empty-group keys", async () => {
|
|
179
|
+
const groupA: ObjectID = ObjectID.generate();
|
|
180
|
+
const groupB: ObjectID = ObjectID.generate();
|
|
181
|
+
const monitor1: ObjectID = ObjectID.generate();
|
|
182
|
+
const monitor2: ObjectID = ObjectID.generate();
|
|
183
|
+
|
|
184
|
+
spyOnFindAllBy([
|
|
185
|
+
makeGroupResource({ monitorGroupId: groupA, monitorId: monitor1 }),
|
|
186
|
+
// A resource row with no monitorId must not surface as a null id.
|
|
187
|
+
makeGroupResource({ monitorGroupId: groupA }),
|
|
188
|
+
makeGroupResource({ monitorGroupId: groupA, monitorId: monitor2 }),
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
const result: Dictionary<Array<ObjectID>> =
|
|
192
|
+
await MonitorGroupService.getMonitorIdsInMonitorGroups([
|
|
193
|
+
groupA,
|
|
194
|
+
groupB,
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
expect(Object.keys(result).sort()).toEqual(
|
|
198
|
+
[groupA.toString(), groupB.toString()].sort(),
|
|
199
|
+
);
|
|
200
|
+
expect(toStrings(result[groupA.toString()]!)).toEqual(
|
|
201
|
+
toStrings([monitor1, monitor2]),
|
|
202
|
+
);
|
|
203
|
+
// The empty group still gets an (empty) entry — callers key off it.
|
|
204
|
+
expect(result[groupB.toString()]).toEqual([]);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("getCurrentStatusesForMonitorGroups", () => {
|
|
209
|
+
const operational: MonitorStatus = makeStatus({
|
|
210
|
+
priority: 2,
|
|
211
|
+
isOperationalState: true,
|
|
212
|
+
name: "Operational",
|
|
213
|
+
});
|
|
214
|
+
// Lower priority NUMBER than operational: must never replace it here.
|
|
215
|
+
const lowerThanOperational: MonitorStatus = makeStatus({
|
|
216
|
+
priority: 1,
|
|
217
|
+
name: "Maintenance",
|
|
218
|
+
});
|
|
219
|
+
// Same priority number as operational: a tie must not replace either.
|
|
220
|
+
const tiedWithOperational: MonitorStatus = makeStatus({
|
|
221
|
+
priority: 2,
|
|
222
|
+
name: "Degraded-Tie",
|
|
223
|
+
});
|
|
224
|
+
const degraded: MonitorStatus = makeStatus({
|
|
225
|
+
priority: 3,
|
|
226
|
+
name: "Degraded",
|
|
227
|
+
});
|
|
228
|
+
const offline: MonitorStatus = makeStatus({ priority: 4, name: "Offline" });
|
|
229
|
+
const allStatuses: Array<MonitorStatus> = [
|
|
230
|
+
operational,
|
|
231
|
+
lowerThanOperational,
|
|
232
|
+
tiedWithOperational,
|
|
233
|
+
degraded,
|
|
234
|
+
offline,
|
|
235
|
+
];
|
|
236
|
+
|
|
237
|
+
test("empty group defaults to the operational status", async () => {
|
|
238
|
+
const groupA: ObjectID = ObjectID.generate();
|
|
239
|
+
spyOnFindAllBy([]);
|
|
240
|
+
|
|
241
|
+
const result: Dictionary<MonitorStatus> =
|
|
242
|
+
await MonitorGroupService.getCurrentStatusesForMonitorGroups({
|
|
243
|
+
monitorGroupIds: [groupA],
|
|
244
|
+
monitorStatuses: allStatuses,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
expect(result[groupA.toString()]).toBe(operational);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("a member with a HIGHER priority number replaces the current status; ties and lower do not", async () => {
|
|
251
|
+
const groupHigh: ObjectID = ObjectID.generate();
|
|
252
|
+
const groupTie: ObjectID = ObjectID.generate();
|
|
253
|
+
const groupLower: ObjectID = ObjectID.generate();
|
|
254
|
+
|
|
255
|
+
spyOnFindAllBy([
|
|
256
|
+
makeGroupResource({
|
|
257
|
+
monitorGroupId: groupHigh,
|
|
258
|
+
monitorId: ObjectID.generate(),
|
|
259
|
+
currentMonitorStatusId: degraded.id!,
|
|
260
|
+
}),
|
|
261
|
+
makeGroupResource({
|
|
262
|
+
monitorGroupId: groupHigh,
|
|
263
|
+
monitorId: ObjectID.generate(),
|
|
264
|
+
currentMonitorStatusId: offline.id!,
|
|
265
|
+
}),
|
|
266
|
+
makeGroupResource({
|
|
267
|
+
monitorGroupId: groupTie,
|
|
268
|
+
monitorId: ObjectID.generate(),
|
|
269
|
+
currentMonitorStatusId: tiedWithOperational.id!,
|
|
270
|
+
}),
|
|
271
|
+
makeGroupResource({
|
|
272
|
+
monitorGroupId: groupLower,
|
|
273
|
+
monitorId: ObjectID.generate(),
|
|
274
|
+
currentMonitorStatusId: lowerThanOperational.id!,
|
|
275
|
+
}),
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
const result: Dictionary<MonitorStatus> =
|
|
279
|
+
await MonitorGroupService.getCurrentStatusesForMonitorGroups({
|
|
280
|
+
monitorGroupIds: [groupHigh, groupTie, groupLower],
|
|
281
|
+
monitorStatuses: allStatuses,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// In MonitorGroupService semantics the HIGHEST priority number wins.
|
|
285
|
+
expect(result[groupHigh.toString()]).toBe(offline);
|
|
286
|
+
// Strict `<` comparison: a tie keeps the operational default.
|
|
287
|
+
expect(result[groupTie.toString()]).toBe(operational);
|
|
288
|
+
// A lower priority number never demotes the operational default.
|
|
289
|
+
expect(result[groupLower.toString()]).toBe(operational);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("members with unknown statuses or missing monitor relation are ignored", async () => {
|
|
293
|
+
const groupA: ObjectID = ObjectID.generate();
|
|
294
|
+
|
|
295
|
+
spyOnFindAllBy([
|
|
296
|
+
// Status id that resolves to nothing in monitorStatuses.
|
|
297
|
+
makeGroupResource({
|
|
298
|
+
monitorGroupId: groupA,
|
|
299
|
+
monitorId: ObjectID.generate(),
|
|
300
|
+
currentMonitorStatusId: ObjectID.generate(),
|
|
301
|
+
}),
|
|
302
|
+
// No monitor relation loaded at all.
|
|
303
|
+
makeGroupResource({
|
|
304
|
+
monitorGroupId: groupA,
|
|
305
|
+
monitorId: ObjectID.generate(),
|
|
306
|
+
omitMonitor: true,
|
|
307
|
+
}),
|
|
308
|
+
]);
|
|
309
|
+
|
|
310
|
+
const result: Dictionary<MonitorStatus> =
|
|
311
|
+
await MonitorGroupService.getCurrentStatusesForMonitorGroups({
|
|
312
|
+
monitorGroupIds: [groupA],
|
|
313
|
+
monitorStatuses: allStatuses,
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
expect(result[groupA.toString()]).toBe(operational);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("throws 'Operational state not found.' when groups are requested but no operational status exists", async () => {
|
|
320
|
+
const findAllBySpy: jest.SpyInstance = spyOnFindAllBy([]);
|
|
321
|
+
|
|
322
|
+
await expect(
|
|
323
|
+
MonitorGroupService.getCurrentStatusesForMonitorGroups({
|
|
324
|
+
monitorGroupIds: [ObjectID.generate()],
|
|
325
|
+
monitorStatuses: [degraded, offline],
|
|
326
|
+
}),
|
|
327
|
+
).rejects.toThrow(new BadDataException("Operational state not found."));
|
|
328
|
+
|
|
329
|
+
// The throw happens before any resource fetch is attempted.
|
|
330
|
+
expect(findAllBySpy).not.toHaveBeenCalled();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("empty group ids return {} without fetching, even when no operational status exists", async () => {
|
|
334
|
+
const findAllBySpy: jest.SpyInstance = spyOnFindAllBy([]);
|
|
335
|
+
const resourcesSpy: jest.SpyInstance = jest.spyOn(
|
|
336
|
+
MonitorGroupService,
|
|
337
|
+
"getMonitorGroupResourcesByGroupIds",
|
|
338
|
+
) as unknown as jest.SpyInstance;
|
|
339
|
+
|
|
340
|
+
const result: Dictionary<MonitorStatus> =
|
|
341
|
+
await MonitorGroupService.getCurrentStatusesForMonitorGroups({
|
|
342
|
+
monitorGroupIds: [],
|
|
343
|
+
// No operational entry: the empty-input early return wins over the throw.
|
|
344
|
+
monitorStatuses: [degraded],
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
expect(result).toEqual({});
|
|
348
|
+
expect(resourcesSpy).not.toHaveBeenCalled();
|
|
349
|
+
expect(findAllBySpy).not.toHaveBeenCalled();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("uses supplied monitorGroupResources without fetching", async () => {
|
|
353
|
+
const groupA: ObjectID = ObjectID.generate();
|
|
354
|
+
const findAllBySpy: jest.SpyInstance = spyOnFindAllBy([]);
|
|
355
|
+
const resourcesSpy: jest.SpyInstance = jest.spyOn(
|
|
356
|
+
MonitorGroupService,
|
|
357
|
+
"getMonitorGroupResourcesByGroupIds",
|
|
358
|
+
) as unknown as jest.SpyInstance;
|
|
359
|
+
|
|
360
|
+
const supplied: Dictionary<Array<MonitorGroupResource>> = {
|
|
361
|
+
[groupA.toString()]: [
|
|
362
|
+
makeGroupResource({
|
|
363
|
+
monitorGroupId: groupA,
|
|
364
|
+
monitorId: ObjectID.generate(),
|
|
365
|
+
currentMonitorStatusId: offline.id!,
|
|
366
|
+
}),
|
|
367
|
+
],
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const result: Dictionary<MonitorStatus> =
|
|
371
|
+
await MonitorGroupService.getCurrentStatusesForMonitorGroups({
|
|
372
|
+
monitorGroupIds: [groupA],
|
|
373
|
+
monitorStatuses: allStatuses,
|
|
374
|
+
monitorGroupResources: supplied,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
expect(result[groupA.toString()]).toBe(offline);
|
|
378
|
+
expect(resourcesSpy).not.toHaveBeenCalled();
|
|
379
|
+
expect(findAllBySpy).not.toHaveBeenCalled();
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe("equivalence with the single-group getCurrentStatus path", () => {
|
|
384
|
+
/*
|
|
385
|
+
* The batched method claims to be an exact twin of getCurrentStatus. This
|
|
386
|
+
* runs BOTH implementations over the same fixture (3 groups: empty /
|
|
387
|
+
* partially degraded / offline-with-noise) and requires identical
|
|
388
|
+
* per-group answers, so a semantic change to either path fails here.
|
|
389
|
+
*/
|
|
390
|
+
test("returns the same status per group as getCurrentStatus", async () => {
|
|
391
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
392
|
+
|
|
393
|
+
const operational: MonitorStatus = makeStatus({
|
|
394
|
+
priority: 1,
|
|
395
|
+
isOperationalState: true,
|
|
396
|
+
name: "Operational",
|
|
397
|
+
});
|
|
398
|
+
const degraded: MonitorStatus = makeStatus({
|
|
399
|
+
priority: 2,
|
|
400
|
+
name: "Degraded",
|
|
401
|
+
});
|
|
402
|
+
const offline: MonitorStatus = makeStatus({
|
|
403
|
+
priority: 3,
|
|
404
|
+
name: "Offline",
|
|
405
|
+
});
|
|
406
|
+
const statuses: Array<MonitorStatus> = [operational, degraded, offline];
|
|
407
|
+
|
|
408
|
+
const groupEmpty: ObjectID = ObjectID.generate();
|
|
409
|
+
const groupDegraded: ObjectID = ObjectID.generate();
|
|
410
|
+
const groupOffline: ObjectID = ObjectID.generate();
|
|
411
|
+
|
|
412
|
+
const rowsByGroup: Dictionary<Array<MonitorGroupResource>> = {
|
|
413
|
+
[groupEmpty.toString()]: [],
|
|
414
|
+
[groupDegraded.toString()]: [
|
|
415
|
+
makeGroupResource({
|
|
416
|
+
monitorGroupId: groupDegraded,
|
|
417
|
+
monitorId: ObjectID.generate(),
|
|
418
|
+
currentMonitorStatusId: operational.id!,
|
|
419
|
+
}),
|
|
420
|
+
makeGroupResource({
|
|
421
|
+
monitorGroupId: groupDegraded,
|
|
422
|
+
monitorId: ObjectID.generate(),
|
|
423
|
+
currentMonitorStatusId: degraded.id!,
|
|
424
|
+
}),
|
|
425
|
+
],
|
|
426
|
+
[groupOffline.toString()]: [
|
|
427
|
+
makeGroupResource({
|
|
428
|
+
monitorGroupId: groupOffline,
|
|
429
|
+
monitorId: ObjectID.generate(),
|
|
430
|
+
currentMonitorStatusId: offline.id!,
|
|
431
|
+
}),
|
|
432
|
+
makeGroupResource({
|
|
433
|
+
monitorGroupId: groupOffline,
|
|
434
|
+
monitorId: ObjectID.generate(),
|
|
435
|
+
currentMonitorStatusId: degraded.id!,
|
|
436
|
+
}),
|
|
437
|
+
// Noise both paths must ignore: unknown status + missing relation.
|
|
438
|
+
makeGroupResource({
|
|
439
|
+
monitorGroupId: groupOffline,
|
|
440
|
+
monitorId: ObjectID.generate(),
|
|
441
|
+
currentMonitorStatusId: ObjectID.generate(),
|
|
442
|
+
}),
|
|
443
|
+
makeGroupResource({
|
|
444
|
+
monitorGroupId: groupOffline,
|
|
445
|
+
monitorId: ObjectID.generate(),
|
|
446
|
+
omitMonitor: true,
|
|
447
|
+
}),
|
|
448
|
+
],
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
// --- mocks for the single-group path (3 queries per group) ---
|
|
452
|
+
(
|
|
453
|
+
jest.spyOn(
|
|
454
|
+
MonitorGroupService,
|
|
455
|
+
"findOneById",
|
|
456
|
+
) as unknown as jest.SpyInstance
|
|
457
|
+
).mockImplementation((...args: Array<unknown>): Promise<MonitorGroup> => {
|
|
458
|
+
const findOneById: { id: ObjectID } = args[0] as { id: ObjectID };
|
|
459
|
+
const group: MonitorGroup = new MonitorGroup();
|
|
460
|
+
group.id = findOneById.id;
|
|
461
|
+
group.projectId = projectId;
|
|
462
|
+
return Promise.resolve(group);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
(
|
|
466
|
+
jest.spyOn(
|
|
467
|
+
MonitorGroupResourceService,
|
|
468
|
+
"findBy",
|
|
469
|
+
) as unknown as jest.SpyInstance
|
|
470
|
+
).mockImplementation(
|
|
471
|
+
(...args: Array<unknown>): Promise<Array<MonitorGroupResource>> => {
|
|
472
|
+
const findBy: { query: { monitorGroupId: ObjectID } } = args[0] as {
|
|
473
|
+
query: { monitorGroupId: ObjectID };
|
|
474
|
+
};
|
|
475
|
+
return Promise.resolve(
|
|
476
|
+
rowsByGroup[findBy.query.monitorGroupId.toString()] || [],
|
|
477
|
+
);
|
|
478
|
+
},
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
(
|
|
482
|
+
jest.spyOn(
|
|
483
|
+
MonitorStatusService,
|
|
484
|
+
"findBy",
|
|
485
|
+
) as unknown as jest.SpyInstance
|
|
486
|
+
).mockResolvedValue(statuses as never);
|
|
487
|
+
|
|
488
|
+
// --- mock for the batched path (ONE query) ---
|
|
489
|
+
spyOnFindAllBy([
|
|
490
|
+
...rowsByGroup[groupEmpty.toString()]!,
|
|
491
|
+
...rowsByGroup[groupDegraded.toString()]!,
|
|
492
|
+
...rowsByGroup[groupOffline.toString()]!,
|
|
493
|
+
]);
|
|
494
|
+
|
|
495
|
+
const batched: Dictionary<MonitorStatus> =
|
|
496
|
+
await MonitorGroupService.getCurrentStatusesForMonitorGroups({
|
|
497
|
+
monitorGroupIds: [groupEmpty, groupDegraded, groupOffline],
|
|
498
|
+
monitorStatuses: statuses,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
for (const groupId of [groupEmpty, groupDegraded, groupOffline]) {
|
|
502
|
+
const single: MonitorStatus =
|
|
503
|
+
await MonitorGroupService.getCurrentStatus(groupId, { isRoot: true });
|
|
504
|
+
expect(batched[groupId.toString()]).toBe(single);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Anchor the shared expectation so both paths being wrong together fails too.
|
|
508
|
+
expect(batched[groupEmpty.toString()]).toBe(operational);
|
|
509
|
+
expect(batched[groupDegraded.toString()]).toBe(degraded);
|
|
510
|
+
expect(batched[groupOffline.toString()]).toBe(offline);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
});
|