@oneuptime/common 11.7.1 → 11.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/AnalyticsModels/KubernetesCostAllocation.ts +103 -0
- 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/TableView.ts +40 -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/1785241000000-AddColumnsToTableView.ts +13 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -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/AnalyticsModels/KubernetesCostAllocation.test.ts +16 -0
- 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/JSONFunctions.test.ts +103 -1
- 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/ModelTable/BaseModelTableColumnCustomization.test.tsx +679 -0
- package/Tests/UI/Components/ModelTable/ColumnCustomizationModal.test.tsx +572 -0
- package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +1678 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +1094 -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/Tests/Utils/UserPreferences.test.ts +563 -0
- package/Types/JSONFunctions.ts +7 -2
- package/Types/Kubernetes/KubernetesCostIngest.ts +37 -1
- 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/MasterPage/MasterPage.tsx +65 -0
- package/UI/Components/ModelDetail/CardModelDetail.tsx +23 -1
- package/UI/Components/ModelDetail/ModelDetail.tsx +46 -1
- package/UI/Components/ModelTable/BaseModelTable.tsx +687 -34
- package/UI/Components/ModelTable/Column.ts +29 -0
- package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +420 -0
- package/UI/Components/ModelTable/ColumnPreference.ts +482 -0
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +326 -0
- package/UI/Components/ModelTable/ExportFromColumns.ts +57 -0
- package/UI/Components/ModelTable/TableView.tsx +24 -2
- package/UI/Components/ModelTable/useCustomFieldColumns.ts +150 -0
- package/UI/Components/Navbar/NavBar.tsx +66 -1
- package/UI/Components/SideMenu/SideMenu.tsx +24 -4
- package/UI/Components/Table/Table.tsx +66 -14
- package/UI/Components/Table/TableRow.tsx +180 -175
- package/UI/Components/Table/Types/Column.ts +20 -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/Utils/UserPreferences.ts +53 -0
- package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js +86 -0
- package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js.map +1 -1
- 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/TableView.js +40 -0
- package/build/dist/Models/DatabaseModels/TableView.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/1785241000000-AddColumnsToTableView.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/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/JSONFunctions.js +8 -3
- package/build/dist/Types/JSONFunctions.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesCostIngest.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/MasterPage/MasterPage.js +50 -1
- package/build/dist/UI/Components/MasterPage/MasterPage.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 +472 -27
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +189 -0
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js +258 -0
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +168 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -0
- 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/ModelTable/TableView.js +13 -2
- package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +84 -0
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.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/SideMenu/SideMenu.js +15 -5
- package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +46 -13
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +11 -6
- package/build/dist/UI/Components/Table/TableRow.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/build/dist/Utils/UserPreferences.js +33 -0
- package/build/dist/Utils/UserPreferences.js.map +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StatusPageService domain-to-id resolution cache tests.
|
|
3
|
+
*
|
|
4
|
+
* Every public status-page API call served on a custom domain (overview,
|
|
5
|
+
* incidents, announcements, polling — several per page view) starts by
|
|
6
|
+
* resolving Host header -> statusPageId, so resolveStatusPageIdOrNull now
|
|
7
|
+
* consults a per-process 60s TTL cache before querying
|
|
8
|
+
* StatusPageDomainService. The contract this pins is load-bearing in three
|
|
9
|
+
* ways:
|
|
10
|
+
* - only SUCCESSFUL resolutions are cached: a freshly verified domain works
|
|
11
|
+
* immediately, and attacker-controlled Host headers cannot fill the cache
|
|
12
|
+
* with negative entries
|
|
13
|
+
* - the cache stores the id as a STRING and hands out a fresh ObjectID per
|
|
14
|
+
* hit, so a caller mutating the returned id cannot poison every subsequent
|
|
15
|
+
* request on that domain
|
|
16
|
+
* - keys are exactly as case-sensitive as the underlying query, so the cache
|
|
17
|
+
* can never resolve a domain the database would not
|
|
18
|
+
*
|
|
19
|
+
* StatusPageDomainService.findOneBy is spied on, so no database is touched.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import StatusPageService from "../../../Server/Services/StatusPageService";
|
|
23
|
+
import StatusPageDomainService from "../../../Server/Services/StatusPageDomainService";
|
|
24
|
+
import StatusPageDomain from "../../../Models/DatabaseModels/StatusPageDomain";
|
|
25
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
26
|
+
import {
|
|
27
|
+
afterEach,
|
|
28
|
+
beforeEach,
|
|
29
|
+
describe,
|
|
30
|
+
expect,
|
|
31
|
+
test,
|
|
32
|
+
jest,
|
|
33
|
+
} from "@jest/globals";
|
|
34
|
+
|
|
35
|
+
const VALID_UUID: string = "550e8400-e29b-41d4-a716-446655440000";
|
|
36
|
+
const SECOND_UUID: string = "660e8400-e29b-41d4-a716-446655440111";
|
|
37
|
+
const DOMAIN: string = "status.company.com";
|
|
38
|
+
const OTHER_DOMAIN: string = "status.other.com";
|
|
39
|
+
const TTL_MS: number = 60 * 1000;
|
|
40
|
+
|
|
41
|
+
function statusPageDomainWith(statusPageId: ObjectID): StatusPageDomain {
|
|
42
|
+
return {
|
|
43
|
+
statusPageId: statusPageId,
|
|
44
|
+
} as unknown as StatusPageDomain;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function spyOnDomainFindOneBy(): jest.SpyInstance {
|
|
48
|
+
return jest.spyOn(
|
|
49
|
+
StatusPageDomainService,
|
|
50
|
+
"findOneBy",
|
|
51
|
+
) as unknown as jest.SpyInstance;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function queriedDomains(spy: jest.SpyInstance): Array<string> {
|
|
55
|
+
return spy.mock.calls.map((call: Array<unknown>) => {
|
|
56
|
+
const args: Record<string, any> = call[0] as Record<string, any>;
|
|
57
|
+
return args["query"]["fullDomain"] as string;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("StatusPageService domain resolution cache", () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
/*
|
|
64
|
+
* The cache lives on the singleton service, so entries written by one
|
|
65
|
+
* test would leak into the next (and into other suites sharing this
|
|
66
|
+
* module registry) without an explicit clear.
|
|
67
|
+
*/
|
|
68
|
+
StatusPageService.clearStatusPageDomainToIdCache();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
afterEach(() => {
|
|
72
|
+
jest.restoreAllMocks();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("caches a successful resolution: second call within TTL issues no query", async () => {
|
|
76
|
+
const domainSpy: jest.SpyInstance =
|
|
77
|
+
spyOnDomainFindOneBy().mockResolvedValue(
|
|
78
|
+
statusPageDomainWith(new ObjectID(VALID_UUID)) as never,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const first: ObjectID | null =
|
|
82
|
+
await StatusPageService.resolveStatusPageIdOrNull(DOMAIN);
|
|
83
|
+
const second: ObjectID | null =
|
|
84
|
+
await StatusPageService.resolveStatusPageIdOrNull(DOMAIN);
|
|
85
|
+
|
|
86
|
+
expect(first?.toString()).toBe(VALID_UUID);
|
|
87
|
+
expect(second?.toString()).toBe(VALID_UUID);
|
|
88
|
+
expect(domainSpy).toHaveBeenCalledTimes(1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("does not cache a miss: two calls for an unknown domain issue two queries", async () => {
|
|
92
|
+
const domainSpy: jest.SpyInstance =
|
|
93
|
+
spyOnDomainFindOneBy().mockResolvedValue(null as never);
|
|
94
|
+
|
|
95
|
+
await expect(
|
|
96
|
+
StatusPageService.resolveStatusPageIdOrNull(DOMAIN),
|
|
97
|
+
).resolves.toBeNull();
|
|
98
|
+
await expect(
|
|
99
|
+
StatusPageService.resolveStatusPageIdOrNull(DOMAIN),
|
|
100
|
+
).resolves.toBeNull();
|
|
101
|
+
|
|
102
|
+
expect(domainSpy).toHaveBeenCalledTimes(2);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("expires after the 60s TTL and re-queries", async () => {
|
|
106
|
+
const base: number = Date.now();
|
|
107
|
+
let nowMs: number = base;
|
|
108
|
+
jest.spyOn(Date, "now").mockImplementation((): number => {
|
|
109
|
+
return nowMs;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const domainSpy: jest.SpyInstance =
|
|
113
|
+
spyOnDomainFindOneBy().mockResolvedValue(
|
|
114
|
+
statusPageDomainWith(new ObjectID(VALID_UUID)) as never,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
await StatusPageService.resolveStatusPageIdOrNull(DOMAIN);
|
|
118
|
+
expect(domainSpy).toHaveBeenCalledTimes(1);
|
|
119
|
+
|
|
120
|
+
// Just inside the TTL: still served from cache.
|
|
121
|
+
nowMs = base + TTL_MS - 1;
|
|
122
|
+
const withinTtl: ObjectID | null =
|
|
123
|
+
await StatusPageService.resolveStatusPageIdOrNull(DOMAIN);
|
|
124
|
+
expect(withinTtl?.toString()).toBe(VALID_UUID);
|
|
125
|
+
expect(domainSpy).toHaveBeenCalledTimes(1);
|
|
126
|
+
|
|
127
|
+
// Past the TTL: the entry has expired and the domain is re-queried.
|
|
128
|
+
nowMs = base + TTL_MS + 1;
|
|
129
|
+
const afterTtl: ObjectID | null =
|
|
130
|
+
await StatusPageService.resolveStatusPageIdOrNull(DOMAIN);
|
|
131
|
+
expect(afterTtl?.toString()).toBe(VALID_UUID);
|
|
132
|
+
expect(domainSpy).toHaveBeenCalledTimes(2);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("keys are exactly as case-sensitive as the query: differently-cased domains each query", async () => {
|
|
136
|
+
const domainSpy: jest.SpyInstance =
|
|
137
|
+
spyOnDomainFindOneBy().mockResolvedValue(
|
|
138
|
+
statusPageDomainWith(new ObjectID(VALID_UUID)) as never,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
await StatusPageService.resolveStatusPageIdOrNull("status.example.com");
|
|
142
|
+
await StatusPageService.resolveStatusPageIdOrNull("Status.Example.com");
|
|
143
|
+
|
|
144
|
+
/*
|
|
145
|
+
* If the cache normalized case it would resolve hostnames the underlying
|
|
146
|
+
* (case-sensitive) query would not, so a cached hit for one casing must
|
|
147
|
+
* not serve the other.
|
|
148
|
+
*/
|
|
149
|
+
expect(domainSpy).toHaveBeenCalledTimes(2);
|
|
150
|
+
expect(queriedDomains(domainSpy)).toEqual([
|
|
151
|
+
"status.example.com",
|
|
152
|
+
"Status.Example.com",
|
|
153
|
+
]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("clearStatusPageDomainToIdCache(domain) evicts only that domain", async () => {
|
|
157
|
+
const domainSpy: jest.SpyInstance =
|
|
158
|
+
spyOnDomainFindOneBy().mockImplementation(((
|
|
159
|
+
options: Record<string, any>,
|
|
160
|
+
): Promise<StatusPageDomain> => {
|
|
161
|
+
const fullDomain: string = options["query"]["fullDomain"] as string;
|
|
162
|
+
return Promise.resolve(
|
|
163
|
+
statusPageDomainWith(
|
|
164
|
+
new ObjectID(fullDomain === DOMAIN ? VALID_UUID : SECOND_UUID),
|
|
165
|
+
),
|
|
166
|
+
);
|
|
167
|
+
}) as never);
|
|
168
|
+
|
|
169
|
+
await StatusPageService.resolveStatusPageIdOrNull(DOMAIN);
|
|
170
|
+
await StatusPageService.resolveStatusPageIdOrNull(OTHER_DOMAIN);
|
|
171
|
+
expect(domainSpy).toHaveBeenCalledTimes(2);
|
|
172
|
+
|
|
173
|
+
StatusPageService.clearStatusPageDomainToIdCache(DOMAIN);
|
|
174
|
+
|
|
175
|
+
const reresolved: ObjectID | null =
|
|
176
|
+
await StatusPageService.resolveStatusPageIdOrNull(DOMAIN);
|
|
177
|
+
const stillCached: ObjectID | null =
|
|
178
|
+
await StatusPageService.resolveStatusPageIdOrNull(OTHER_DOMAIN);
|
|
179
|
+
|
|
180
|
+
expect(reresolved?.toString()).toBe(VALID_UUID);
|
|
181
|
+
expect(stillCached?.toString()).toBe(SECOND_UUID);
|
|
182
|
+
// Only the evicted domain re-queried; the other stayed cached.
|
|
183
|
+
expect(domainSpy).toHaveBeenCalledTimes(3);
|
|
184
|
+
expect(queriedDomains(domainSpy)).toEqual([DOMAIN, OTHER_DOMAIN, DOMAIN]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("clearStatusPageDomainToIdCache() with no argument clears everything", async () => {
|
|
188
|
+
const domainSpy: jest.SpyInstance =
|
|
189
|
+
spyOnDomainFindOneBy().mockResolvedValue(
|
|
190
|
+
statusPageDomainWith(new ObjectID(VALID_UUID)) as never,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
await StatusPageService.resolveStatusPageIdOrNull(DOMAIN);
|
|
194
|
+
await StatusPageService.resolveStatusPageIdOrNull(OTHER_DOMAIN);
|
|
195
|
+
expect(domainSpy).toHaveBeenCalledTimes(2);
|
|
196
|
+
|
|
197
|
+
StatusPageService.clearStatusPageDomainToIdCache();
|
|
198
|
+
|
|
199
|
+
await StatusPageService.resolveStatusPageIdOrNull(DOMAIN);
|
|
200
|
+
await StatusPageService.resolveStatusPageIdOrNull(OTHER_DOMAIN);
|
|
201
|
+
expect(domainSpy).toHaveBeenCalledTimes(4);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("UUID input never touches the domain cache or the domain service", async () => {
|
|
205
|
+
const domainSpy: jest.SpyInstance = spyOnDomainFindOneBy();
|
|
206
|
+
|
|
207
|
+
const result: ObjectID | null =
|
|
208
|
+
await StatusPageService.resolveStatusPageIdOrNull(VALID_UUID);
|
|
209
|
+
|
|
210
|
+
expect(result?.toString()).toBe(VALID_UUID);
|
|
211
|
+
expect(domainSpy).not.toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("invalid dotless input returns null without querying", async () => {
|
|
215
|
+
const domainSpy: jest.SpyInstance = spyOnDomainFindOneBy();
|
|
216
|
+
|
|
217
|
+
await expect(
|
|
218
|
+
StatusPageService.resolveStatusPageIdOrNull("localhost"),
|
|
219
|
+
).resolves.toBeNull();
|
|
220
|
+
expect(domainSpy).not.toHaveBeenCalled();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("cached hits return a fresh ObjectID each call, so mutation cannot poison the cache", async () => {
|
|
224
|
+
spyOnDomainFindOneBy().mockResolvedValue(
|
|
225
|
+
statusPageDomainWith(new ObjectID(VALID_UUID)) as never,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
// First call fills the cache; the next two are served from it.
|
|
229
|
+
await StatusPageService.resolveStatusPageIdOrNull(DOMAIN);
|
|
230
|
+
const firstHit: ObjectID | null =
|
|
231
|
+
await StatusPageService.resolveStatusPageIdOrNull(DOMAIN);
|
|
232
|
+
const secondHit: ObjectID | null =
|
|
233
|
+
await StatusPageService.resolveStatusPageIdOrNull(DOMAIN);
|
|
234
|
+
|
|
235
|
+
expect(firstHit?.toString()).toBe(VALID_UUID);
|
|
236
|
+
expect(secondHit?.toString()).toBe(VALID_UUID);
|
|
237
|
+
expect(firstHit).not.toBe(secondHit);
|
|
238
|
+
|
|
239
|
+
// Mutating a returned instance must not leak into later hits.
|
|
240
|
+
firstHit!.id = "poisoned";
|
|
241
|
+
const thirdHit: ObjectID | null =
|
|
242
|
+
await StatusPageService.resolveStatusPageIdOrNull(DOMAIN);
|
|
243
|
+
expect(thirdHit?.toString()).toBe(VALID_UUID);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import StatusPageAPI from "../../../Server/API/StatusPageAPI";
|
|
2
|
+
import StatusPageService from "../../../Server/Services/StatusPageService";
|
|
3
|
+
import StatusPageGroupService from "../../../Server/Services/StatusPageGroupService";
|
|
4
|
+
import StatusPageResourceService from "../../../Server/Services/StatusPageResourceService";
|
|
5
|
+
import MonitorStatusService from "../../../Server/Services/MonitorStatusService";
|
|
6
|
+
import MonitorGroupService from "../../../Server/Services/MonitorGroupService";
|
|
7
|
+
import StatusPage from "../../../Models/DatabaseModels/StatusPage";
|
|
8
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
9
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
10
|
+
import { afterEach, describe, expect, jest, test } from "@jest/globals";
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* StatusPageAPI.getStatusPageResourcesAndTimelines timeline-window
|
|
14
|
+
* computation.
|
|
15
|
+
*
|
|
16
|
+
* The overview endpoint used to issue a SEPARATE one-column StatusPage query
|
|
17
|
+
* (statusPageForDays) just to read showUptimeHistoryInDays and compute the
|
|
18
|
+
* monitor-timeline window. That query was deleted: the helper's
|
|
19
|
+
* startDateForMonitorTimeline/endDateForMonitorTimeline params became
|
|
20
|
+
* optional, and when omitted the window is computed from the status page row
|
|
21
|
+
* the helper ALREADY fetches — showUptimeHistoryInDays clamped to 1..90 days
|
|
22
|
+
* (falsy hits `|| 90` BEFORE the < 1 clamp, so 0 means 90, not 1), ending
|
|
23
|
+
* now. The computed window is also RETURNED so the overview handler can keep
|
|
24
|
+
* serving it to the client.
|
|
25
|
+
*
|
|
26
|
+
* These tests pin: verbatim forwarding of explicit dates, the computed
|
|
27
|
+
* window and its clamp ORDER, that the returned dates are exactly what the
|
|
28
|
+
* timeline query received, and that the helper issues exactly ONE
|
|
29
|
+
* StatusPageService.findOneBy — so the deleted per-request query cannot
|
|
30
|
+
* sneak back on the hottest public endpoint in the product.
|
|
31
|
+
*
|
|
32
|
+
* All service reads are spied on — no database is touched.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
type ResourcesAndTimelinesResult = Awaited<
|
|
36
|
+
ReturnType<StatusPageAPI["getStatusPageResourcesAndTimelines"]>
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
type TimelineHelperArgs = {
|
|
40
|
+
monitorIds: Array<ObjectID>;
|
|
41
|
+
startDate: Date;
|
|
42
|
+
endDate: Date;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function makeStatusPage(showUptimeHistoryInDays: number | undefined): {
|
|
46
|
+
statusPageId: ObjectID;
|
|
47
|
+
statusPage: StatusPage;
|
|
48
|
+
} {
|
|
49
|
+
const statusPageId: ObjectID = ObjectID.generate();
|
|
50
|
+
const statusPage: StatusPage = new StatusPage();
|
|
51
|
+
statusPage._id = statusPageId.toString();
|
|
52
|
+
statusPage.projectId = ObjectID.generate();
|
|
53
|
+
if (showUptimeHistoryInDays !== undefined) {
|
|
54
|
+
statusPage.showUptimeHistoryInDays = showUptimeHistoryInDays;
|
|
55
|
+
}
|
|
56
|
+
return { statusPageId, statusPage };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type Spies = {
|
|
60
|
+
findOneBySpy: jest.SpyInstance;
|
|
61
|
+
timelineSpy: jest.SpyInstance;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/*
|
|
65
|
+
* The page is kept EMPTY (no resources, no groups) so every downstream loop
|
|
66
|
+
* short-circuits and the only interesting behavior left is the window
|
|
67
|
+
* computation itself.
|
|
68
|
+
*/
|
|
69
|
+
function mockEmptyStatusPage(statusPage: StatusPage): Spies {
|
|
70
|
+
const findOneBySpy: jest.SpyInstance = (
|
|
71
|
+
jest.spyOn(StatusPageService, "findOneBy") as unknown as jest.SpyInstance
|
|
72
|
+
).mockResolvedValue(statusPage as never);
|
|
73
|
+
|
|
74
|
+
(
|
|
75
|
+
jest.spyOn(MonitorStatusService, "findBy") as unknown as jest.SpyInstance
|
|
76
|
+
).mockResolvedValue([] as never);
|
|
77
|
+
|
|
78
|
+
(
|
|
79
|
+
jest.spyOn(StatusPageGroupService, "findBy") as unknown as jest.SpyInstance
|
|
80
|
+
).mockResolvedValue([] as never);
|
|
81
|
+
|
|
82
|
+
(
|
|
83
|
+
jest.spyOn(
|
|
84
|
+
StatusPageResourceService,
|
|
85
|
+
"findBy",
|
|
86
|
+
) as unknown as jest.SpyInstance
|
|
87
|
+
).mockResolvedValue([] as never);
|
|
88
|
+
|
|
89
|
+
(
|
|
90
|
+
jest.spyOn(
|
|
91
|
+
MonitorGroupService,
|
|
92
|
+
"getMonitorGroupResourcesByGroupIds",
|
|
93
|
+
) as unknown as jest.SpyInstance
|
|
94
|
+
).mockResolvedValue({} as never);
|
|
95
|
+
|
|
96
|
+
(
|
|
97
|
+
jest.spyOn(
|
|
98
|
+
MonitorGroupService,
|
|
99
|
+
"getCurrentStatusesForMonitorGroups",
|
|
100
|
+
) as unknown as jest.SpyInstance
|
|
101
|
+
).mockResolvedValue({} as never);
|
|
102
|
+
|
|
103
|
+
const timelineSpy: jest.SpyInstance = (
|
|
104
|
+
jest.spyOn(
|
|
105
|
+
StatusPageService,
|
|
106
|
+
"getMonitorStatusTimelineForStatusPage",
|
|
107
|
+
) as unknown as jest.SpyInstance
|
|
108
|
+
).mockResolvedValue([] as never);
|
|
109
|
+
|
|
110
|
+
return { findOneBySpy, timelineSpy };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function timelineArgs(timelineSpy: jest.SpyInstance): TimelineHelperArgs {
|
|
114
|
+
expect(timelineSpy).toHaveBeenCalledTimes(1);
|
|
115
|
+
return timelineSpy.mock.calls[0]![0] as TimelineHelperArgs;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function dayDifference(startDate: Date, endDate: Date): number {
|
|
119
|
+
const msPerDay: number = 24 * 60 * 60 * 1000;
|
|
120
|
+
return (endDate.getTime() - startDate.getTime()) / msPerDay;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function runWithConfiguredDays(data: {
|
|
124
|
+
showUptimeHistoryInDays: number | undefined;
|
|
125
|
+
}): Promise<{
|
|
126
|
+
result: ResourcesAndTimelinesResult;
|
|
127
|
+
spies: Spies;
|
|
128
|
+
}> {
|
|
129
|
+
const { statusPageId, statusPage } = makeStatusPage(
|
|
130
|
+
data.showUptimeHistoryInDays,
|
|
131
|
+
);
|
|
132
|
+
const spies: Spies = mockEmptyStatusPage(statusPage);
|
|
133
|
+
const api: StatusPageAPI = new StatusPageAPI();
|
|
134
|
+
|
|
135
|
+
const result: ResourcesAndTimelinesResult =
|
|
136
|
+
await api.getStatusPageResourcesAndTimelines({
|
|
137
|
+
statusPageId: statusPageId,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return { result, spies };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function expectComputedWindowOfDays(data: {
|
|
144
|
+
showUptimeHistoryInDays: number | undefined;
|
|
145
|
+
expectedDays: number;
|
|
146
|
+
}): Promise<void> {
|
|
147
|
+
const { result, spies } = await runWithConfiguredDays({
|
|
148
|
+
showUptimeHistoryInDays: data.showUptimeHistoryInDays,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const args: TimelineHelperArgs = timelineArgs(spies.timelineSpy);
|
|
152
|
+
|
|
153
|
+
// endDate is "now" (generous tolerance for slow CI).
|
|
154
|
+
expect(
|
|
155
|
+
Math.abs(args.endDate.getTime() - OneUptimeDate.getCurrentDate().getTime()),
|
|
156
|
+
).toBeLessThan(10 * 1000);
|
|
157
|
+
|
|
158
|
+
// The window spans exactly the clamped number of days.
|
|
159
|
+
expect(dayDifference(args.startDate, args.endDate)).toBeCloseTo(
|
|
160
|
+
data.expectedDays,
|
|
161
|
+
2,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// The returned window is EXACTLY what the timeline query received.
|
|
165
|
+
expect(result.startDateForMonitorTimeline).toBe(args.startDate);
|
|
166
|
+
expect(result.endDateForMonitorTimeline).toBe(args.endDate);
|
|
167
|
+
|
|
168
|
+
// Exactly one StatusPage row fetch — the deleted query must not return.
|
|
169
|
+
expect(spies.findOneBySpy).toHaveBeenCalledTimes(1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
describe("StatusPageAPI.getStatusPageResourcesAndTimelines timeline window", () => {
|
|
173
|
+
afterEach(() => {
|
|
174
|
+
jest.restoreAllMocks();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe("explicit dates", () => {
|
|
178
|
+
test("are forwarded verbatim to the timeline query and returned unchanged", async () => {
|
|
179
|
+
const { statusPageId, statusPage } = makeStatusPage(45);
|
|
180
|
+
const spies: Spies = mockEmptyStatusPage(statusPage);
|
|
181
|
+
const getSomeDaysAgoSpy: jest.SpyInstance = jest.spyOn(
|
|
182
|
+
OneUptimeDate,
|
|
183
|
+
"getSomeDaysAgo",
|
|
184
|
+
) as unknown as jest.SpyInstance;
|
|
185
|
+
const api: StatusPageAPI = new StatusPageAPI();
|
|
186
|
+
|
|
187
|
+
const explicitStartDate: Date = new Date("2026-01-05T00:00:00.000Z");
|
|
188
|
+
const explicitEndDate: Date = new Date("2026-01-19T00:00:00.000Z");
|
|
189
|
+
|
|
190
|
+
const result: ResourcesAndTimelinesResult =
|
|
191
|
+
await api.getStatusPageResourcesAndTimelines({
|
|
192
|
+
statusPageId: statusPageId,
|
|
193
|
+
startDateForMonitorTimeline: explicitStartDate,
|
|
194
|
+
endDateForMonitorTimeline: explicitEndDate,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const args: TimelineHelperArgs = timelineArgs(spies.timelineSpy);
|
|
198
|
+
// Same Date INSTANCES, not recomputed lookalikes.
|
|
199
|
+
expect(args.startDate).toBe(explicitStartDate);
|
|
200
|
+
expect(args.endDate).toBe(explicitEndDate);
|
|
201
|
+
expect(result.startDateForMonitorTimeline).toBe(explicitStartDate);
|
|
202
|
+
expect(result.endDateForMonitorTimeline).toBe(explicitEndDate);
|
|
203
|
+
|
|
204
|
+
// No window computation happened (showUptimeHistoryInDays ignored).
|
|
205
|
+
expect(getSomeDaysAgoSpy).not.toHaveBeenCalled();
|
|
206
|
+
expect(spies.findOneBySpy).toHaveBeenCalledTimes(1);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("supplying only ONE of the two dates recomputes BOTH from the page config", async () => {
|
|
210
|
+
/*
|
|
211
|
+
* The guard is `!startDate || !endDate` — a half-specified window is
|
|
212
|
+
* treated as unspecified, so the caller-provided half is DISCARDED in
|
|
213
|
+
* favor of the page-configured window.
|
|
214
|
+
*/
|
|
215
|
+
const { statusPageId, statusPage } = makeStatusPage(45);
|
|
216
|
+
const spies: Spies = mockEmptyStatusPage(statusPage);
|
|
217
|
+
const api: StatusPageAPI = new StatusPageAPI();
|
|
218
|
+
|
|
219
|
+
const explicitStartDate: Date = new Date("2026-01-05T00:00:00.000Z");
|
|
220
|
+
|
|
221
|
+
const result: ResourcesAndTimelinesResult =
|
|
222
|
+
await api.getStatusPageResourcesAndTimelines({
|
|
223
|
+
statusPageId: statusPageId,
|
|
224
|
+
startDateForMonitorTimeline: explicitStartDate,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const args: TimelineHelperArgs = timelineArgs(spies.timelineSpy);
|
|
228
|
+
expect(args.startDate).not.toBe(explicitStartDate);
|
|
229
|
+
expect(dayDifference(args.startDate, args.endDate)).toBeCloseTo(45, 2);
|
|
230
|
+
expect(result.startDateForMonitorTimeline).toBe(args.startDate);
|
|
231
|
+
expect(result.endDateForMonitorTimeline).toBe(args.endDate);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe("omitted dates: window computed from showUptimeHistoryInDays", () => {
|
|
236
|
+
test("45 configured days -> 45-day window ending now", async () => {
|
|
237
|
+
await expectComputedWindowOfDays({
|
|
238
|
+
showUptimeHistoryInDays: 45,
|
|
239
|
+
expectedDays: 45,
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("200 configured days clamps DOWN to a 90-day window", async () => {
|
|
244
|
+
await expectComputedWindowOfDays({
|
|
245
|
+
showUptimeHistoryInDays: 200,
|
|
246
|
+
expectedDays: 90,
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("undefined (column never set) defaults to a 90-day window", async () => {
|
|
251
|
+
await expectComputedWindowOfDays({
|
|
252
|
+
showUptimeHistoryInDays: undefined,
|
|
253
|
+
expectedDays: 90,
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("0 hits the `|| 90` falsy default BEFORE the < 1 clamp -> 90 days, NOT 1", async () => {
|
|
258
|
+
/*
|
|
259
|
+
* ORDER matters and is pinned: `showUptimeHistoryInDays || 90` runs
|
|
260
|
+
* first, so a falsy 0 becomes the 90-day default. Only truthy
|
|
261
|
+
* sub-1 values (fractions, negatives) reach the < 1 clamp below it.
|
|
262
|
+
*/
|
|
263
|
+
await expectComputedWindowOfDays({
|
|
264
|
+
showUptimeHistoryInDays: 0,
|
|
265
|
+
expectedDays: 90,
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("0.5 (truthy but < 1) clamps UP to a 1-day window", async () => {
|
|
270
|
+
await expectComputedWindowOfDays({
|
|
271
|
+
showUptimeHistoryInDays: 0.5,
|
|
272
|
+
expectedDays: 1,
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("-5 (truthy, < 1) clamps UP to a 1-day window", async () => {
|
|
277
|
+
await expectComputedWindowOfDays({
|
|
278
|
+
showUptimeHistoryInDays: -5,
|
|
279
|
+
expectedDays: 1,
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
@@ -18,7 +18,14 @@ import StatusPageDomainService from "../../../Server/Services/StatusPageDomainSe
|
|
|
18
18
|
import StatusPage from "../../../Models/DatabaseModels/StatusPage";
|
|
19
19
|
import StatusPageDomain from "../../../Models/DatabaseModels/StatusPageDomain";
|
|
20
20
|
import ObjectID from "../../../Types/ObjectID";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
afterEach,
|
|
23
|
+
beforeEach,
|
|
24
|
+
describe,
|
|
25
|
+
expect,
|
|
26
|
+
test,
|
|
27
|
+
jest,
|
|
28
|
+
} from "@jest/globals";
|
|
22
29
|
|
|
23
30
|
const VALID_UUID: string = "550e8400-e29b-41d4-a716-446655440000";
|
|
24
31
|
const DOMAIN: string = "status.company.com";
|
|
@@ -48,6 +55,15 @@ function spyOnDomainFindOneBy(): jest.SpyInstance {
|
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
describe("StatusPageService MCP gate", () => {
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
/*
|
|
60
|
+
* resolveStatusPageIdOrNull caches successful domain resolutions on the
|
|
61
|
+
* singleton service, so a hit in one test would short-circuit the mocked
|
|
62
|
+
* domain query in the next.
|
|
63
|
+
*/
|
|
64
|
+
StatusPageService.clearStatusPageDomainToIdCache();
|
|
65
|
+
});
|
|
66
|
+
|
|
51
67
|
afterEach(() => {
|
|
52
68
|
jest.restoreAllMocks();
|
|
53
69
|
});
|