@oneuptime/common 12.0.2 → 12.0.4
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/CephCluster.ts +14 -0
- package/Models/DatabaseModels/CodeRepository.ts +14 -6
- package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
- package/Models/DatabaseModels/DockerHost.ts +14 -0
- package/Models/DatabaseModels/GlobalOidcProject.ts +18 -0
- package/Models/DatabaseModels/GlobalSsoProject.ts +18 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/IoTFleet.ts +14 -0
- package/Models/DatabaseModels/KubernetesCluster.ts +14 -0
- package/Models/DatabaseModels/LogDropFilter.ts +9 -20
- package/Models/DatabaseModels/NetworkDevice.ts +14 -0
- package/Models/DatabaseModels/NetworkInterface.ts +18 -0
- package/Models/DatabaseModels/Project.ts +16 -2
- package/Models/DatabaseModels/ProxmoxCluster.ts +14 -0
- package/Models/DatabaseModels/Service.ts +30 -0
- package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
- package/Models/DatabaseModels/StatusPagePrivateUser.ts +30 -0
- package/Models/DatabaseModels/StatusPageResource.ts +73 -0
- package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
- package/Models/DatabaseModels/User.ts +30 -0
- package/Server/API/AIAgentDataAPI.ts +31 -0
- package/Server/API/BaseAPI.ts +13 -2
- package/Server/API/DashboardAPI.ts +458 -12
- package/Server/API/GitHubAPI.ts +119 -283
- package/Server/API/UserAPI.ts +263 -1
- package/Server/EnvironmentConfig.ts +38 -4
- package/Server/Infrastructure/GlobalCache.ts +152 -0
- package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.ts +21 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.ts +68 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.ts +100 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.ts +214 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.ts +69 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +12 -0
- package/Server/Services/AlertStateTimelineService.ts +20 -0
- package/Server/Services/AnalyticsDatabaseService.ts +72 -1
- package/Server/Services/CephClusterService.ts +26 -86
- package/Server/Services/CloudResourceService.ts +23 -83
- package/Server/Services/CodeRepositoryService.ts +105 -2
- package/Server/Services/DatabaseService.ts +296 -2
- package/Server/Services/DockerHostService.ts +19 -79
- package/Server/Services/DockerSwarmClusterService.ts +27 -88
- package/Server/Services/HostService.ts +46 -99
- package/Server/Services/IncidentStateTimelineService.ts +20 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/IoTFleetService.ts +19 -79
- package/Server/Services/KubernetesClusterService.ts +17 -78
- package/Server/Services/LabelService.ts +57 -10
- package/Server/Services/MetricTypeService.ts +84 -0
- package/Server/Services/MonitorService.ts +89 -14
- package/Server/Services/MonitorStatusTimelineService.ts +29 -0
- package/Server/Services/MonitorTemplateService.ts +65 -0
- package/Server/Services/OpenTelemetryIngestService.ts +230 -26
- package/Server/Services/PodmanHostService.ts +19 -79
- package/Server/Services/ProxmoxClusterService.ts +23 -83
- package/Server/Services/RumApplicationService.ts +19 -80
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
- package/Server/Services/ServerlessFunctionService.ts +24 -85
- package/Server/Services/ServiceService.ts +299 -73
- package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
- package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
- package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
- package/Server/Services/TelemetryEntityService.ts +1 -0
- package/Server/Services/UserService.ts +184 -0
- package/Server/Utils/AI/Toolbox/CodeTools.ts +45 -4
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +13 -0
- package/Server/Utils/CodeRepository/GitHub/GitHub.ts +157 -4
- package/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.ts +130 -0
- package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
- package/Server/Utils/Monitor/MonitorResource.ts +63 -36
- package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
- package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
- package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
- package/Server/Utils/PasswordHash.ts +306 -0
- package/Server/Utils/SingleFlight.ts +83 -0
- package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
- package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
- package/Server/Utils/Telemetry/Telemetry.ts +351 -122
- package/Tests/App/Dashboard/OverviewCustomFields.test.tsx +333 -0
- package/Tests/App/Dashboard/UserCustomFields.test.tsx +392 -0
- package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
- package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
- package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
- package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
- package/Tests/Server/API/AIAgentDataRepositoryToken.test.ts +381 -0
- package/Tests/Server/API/DashboardPublicAttributeValuesAPI.test.ts +943 -0
- package/Tests/Server/API/DashboardPublicMetricsAggregateAPI.test.ts +1296 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
- package/Tests/Server/API/DashboardPublicTemplatePayloads.test.ts +598 -0
- package/Tests/Server/API/GitHubAppInstallationBindingAPI.test.ts +539 -0
- package/Tests/Server/API/Helpers.ts +6 -1
- package/Tests/Server/API/UserProjectsAPI.test.ts +852 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +354 -0
- package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
- package/Tests/Server/Infrastructure/SemaphoreMutex.test.ts +215 -0
- package/Tests/Server/Services/AddPerUserPasswordSaltMigration.test.ts +178 -0
- package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +582 -0
- package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +5 -0
- package/Tests/Server/Services/CodeRepositoryInstallationBinding.test.ts +305 -0
- package/Tests/Server/Services/CodeRepositoryResolutionBinding.test.ts +147 -0
- package/Tests/Server/Services/DatabaseServicePerUserPasswordSalt.test.ts +870 -0
- package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
- package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
- package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +163 -15
- package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
- package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
- package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
- package/Tests/Server/Services/OpenTelemetryServiceResolutionCache.test.ts +568 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +305 -56
- package/Tests/Server/Services/RestoreDroppedUniqueIndexesMigration.test.ts +386 -0
- package/Tests/Server/Services/RestoreServiceLowerNameIndexMigration.test.ts +240 -0
- package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
- package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
- package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
- package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
- package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +885 -0
- package/Tests/Server/Services/WidenHashedStringColumnsForScryptMigration.test.ts +220 -0
- package/Tests/Server/Utils/AI/CodeTools.test.ts +47 -1
- package/Tests/Server/Utils/AI/CodeWriteTools.test.ts +10 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +120 -0
- package/Tests/Server/Utils/CodeRepository/GitHubInstallationBinding.test.ts +292 -0
- package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
- package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
- package/Tests/Server/Utils/GitHubInstallationOwnershipVerification.test.ts +340 -0
- package/Tests/Server/Utils/GitHubWebhookAndTreeCacheIsolation.test.ts +250 -0
- package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
- package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
- package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
- package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
- package/Tests/Server/Utils/PasswordHash.test.ts +555 -0
- package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
- package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
- package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
- package/Tests/Types/Database/ColumnLength.test.ts +7 -1
- package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
- package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
- package/Tests/Types/Decimal.test.ts +37 -0
- package/Tests/Types/HashedStringPerUserSalt.test.ts +476 -0
- package/Tests/Types/Monitor/DockerAlertTemplates.test.ts +293 -0
- package/Tests/Types/Monitor/HostAlertTemplates.test.ts +249 -0
- package/Tests/Types/Monitor/IotAlertTemplates.test.ts +320 -0
- package/Tests/Types/Monitor/PodmanAlertTemplates.test.ts +271 -0
- package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
- package/Tests/Types/Port.test.ts +38 -0
- package/Tests/UI/Components/CustomFields/CustomFieldsDetail.test.tsx +696 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
- package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
- package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
- package/Tests/UI/Utils/UserProjectsModelAPI.test.ts +714 -0
- package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
- package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
- package/Tests/Utils/TeamMembersByProject.test.ts +655 -0
- package/Types/Dashboard/DashboardComponentType.ts +1 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
- package/Types/Dashboard/DashboardTemplates.ts +397 -0
- package/Types/Database/BigIntColumnTransformer.ts +74 -0
- package/Types/Database/ColumnLength.ts +9 -1
- package/Types/Database/DatabaseProperty.ts +26 -0
- package/Types/Database/NumericColumnValue.ts +128 -0
- package/Types/Database/TableColumn.ts +11 -0
- package/Types/Database/UnsynchronizedIndex.ts +48 -0
- package/Types/Decimal.ts +16 -4
- package/Types/HashedString.ts +139 -4
- package/Types/Permission.ts +48 -0
- package/Types/Port.ts +16 -4
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +85 -11
- package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
- package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
- package/UI/Components/Navbar/NavBar.tsx +12 -2
- package/UI/Components/Navbar/NavBarMenuModal.tsx +19 -7
- package/UI/Utils/ModelAPI/UserProjectsModelAPI.ts +257 -0
- package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
- package/Utils/Dashboard/Components/Index.ts +7 -0
- package/Utils/TeamMembersByProject.ts +237 -0
- package/build/dist/Models/DatabaseModels/CephCluster.js +16 -1
- package/build/dist/Models/DatabaseModels/CephCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/CodeRepository.js +14 -6
- package/build/dist/Models/DatabaseModels/CodeRepository.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DockerHost.js +16 -1
- package/build/dist/Models/DatabaseModels/DockerHost.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalOidcProject.js +16 -1
- package/build/dist/Models/DatabaseModels/GlobalOidcProject.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalSsoProject.js +16 -1
- package/build/dist/Models/DatabaseModels/GlobalSsoProject.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IoTFleet.js +16 -1
- package/build/dist/Models/DatabaseModels/IoTFleet.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js +16 -1
- package/build/dist/Models/DatabaseModels/KubernetesCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
- package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +16 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkInterface.js +16 -1
- package/build/dist/Models/DatabaseModels/NetworkInterface.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +16 -2
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ProxmoxCluster.js +16 -1
- package/build/dist/Models/DatabaseModels/ProxmoxCluster.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Service.js +28 -1
- package/build/dist/Models/DatabaseModels/Service.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
- package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +32 -0
- package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
- package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
- package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
- package/build/dist/Models/DatabaseModels/User.js +32 -0
- package/build/dist/Models/DatabaseModels/User.js.map +1 -1
- package/build/dist/Server/API/AIAgentDataAPI.js +21 -2
- package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +11 -1
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +343 -10
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/API/GitHubAPI.js +94 -167
- package/build/dist/Server/API/GitHubAPI.js.map +1 -1
- package/build/dist/Server/API/UserAPI.js +187 -2
- package/build/dist/Server/API/UserAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +34 -4
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/GlobalCache.js +120 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786018109307-AddPerUserPasswordSalt.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js +59 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786023262402-WidenHashedStringColumnsForScrypt.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js +93 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js +101 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js +66 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786300000000-QuarantineUnboundGitHubInstallations.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
- package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +57 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +26 -76
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceService.js +23 -73
- package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
- package/build/dist/Server/Services/CodeRepositoryService.js +90 -2
- package/build/dist/Server/Services/CodeRepositoryService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +246 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +19 -69
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -78
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +58 -102
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetService.js +19 -69
- package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +17 -68
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/LabelService.js +42 -7
- package/build/dist/Server/Services/LabelService.js.map +1 -1
- package/build/dist/Server/Services/MetricTypeService.js +74 -0
- package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +81 -9
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
- package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/OpenTelemetryIngestService.js +155 -25
- package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +19 -69
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +23 -73
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationService.js +19 -70
- package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionService.js +24 -75
- package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
- package/build/dist/Server/Services/ServiceService.js +236 -61
- package/build/dist/Server/Services/ServiceService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
- package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +165 -0
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js +31 -1
- package/build/dist/Server/Utils/AI/Toolbox/CodeTools.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +13 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js +126 -4
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHub.js.map +1 -1
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js +136 -0
- package/build/dist/Server/Utils/CodeRepository/GitHub/GitHubInstallationBinding.js.map +1 -0
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
- package/build/dist/Server/Utils/PasswordHash.js +237 -0
- package/build/dist/Server/Utils/PasswordHash.js.map +1 -0
- package/build/dist/Server/Utils/SingleFlight.js +76 -0
- package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
- package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
- package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
- package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
- package/build/dist/Types/Database/ColumnLength.js +9 -1
- package/build/dist/Types/Database/ColumnLength.js.map +1 -1
- package/build/dist/Types/Database/DatabaseProperty.js +25 -0
- package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
- package/build/dist/Types/Database/NumericColumnValue.js +97 -0
- package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
- package/build/dist/Types/Database/TableColumn.js.map +1 -1
- package/build/dist/Types/Database/UnsynchronizedIndex.js +46 -0
- package/build/dist/Types/Database/UnsynchronizedIndex.js.map +1 -0
- package/build/dist/Types/Decimal.js +14 -3
- package/build/dist/Types/Decimal.js.map +1 -1
- package/build/dist/Types/HashedString.js +95 -5
- package/build/dist/Types/HashedString.js.map +1 -1
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Port.js +15 -5
- package/build/dist/Types/Port.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +51 -10
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
- package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
- package/build/dist/UI/Components/Navbar/NavBar.js +3 -1
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +19 -7
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js +156 -0
- package/build/dist/UI/Utils/ModelAPI/UserProjectsModelAPI.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
- package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- package/build/dist/Utils/TeamMembersByProject.js +145 -0
- package/build/dist/Utils/TeamMembersByProject.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1719 @@
|
|
|
1
|
+
import Label from "../../../Models/DatabaseModels/Label";
|
|
2
|
+
import Monitor from "../../../Models/DatabaseModels/Monitor";
|
|
3
|
+
import StatusPageMonitorRule from "../../../Models/DatabaseModels/StatusPageMonitorRule";
|
|
4
|
+
import StatusPageResource from "../../../Models/DatabaseModels/StatusPageResource";
|
|
5
|
+
import MonitorService from "../../../Server/Services/MonitorService";
|
|
6
|
+
import StatusPageMonitorRuleEngineService, {
|
|
7
|
+
StatusPageMonitorRuleSyncResult,
|
|
8
|
+
} from "../../../Server/Services/StatusPageMonitorRuleEngineService";
|
|
9
|
+
import StatusPageMonitorRuleService from "../../../Server/Services/StatusPageMonitorRuleService";
|
|
10
|
+
import StatusPageResourceService from "../../../Server/Services/StatusPageResourceService";
|
|
11
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
12
|
+
import UptimePrecision from "../../../Types/StatusPage/UptimePrecision";
|
|
13
|
+
import { describe, expect, it, beforeEach, afterEach } from "@jest/globals";
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* Contract under test - status page monitor rules.
|
|
17
|
+
*
|
|
18
|
+
* A status page can say "show every monitor labelled Production in the Core
|
|
19
|
+
* Platform group" instead of picking monitors one at a time and typing a
|
|
20
|
+
* display name for each. That promise has two halves, and both have to hold
|
|
21
|
+
* or the page quietly shows the wrong thing:
|
|
22
|
+
*
|
|
23
|
+
* - a monitor that starts matching is added to the group the rule names,
|
|
24
|
+
* and a monitor that stops matching is removed again (whether the rule
|
|
25
|
+
* moved or the monitor did);
|
|
26
|
+
*
|
|
27
|
+
* - a resource a human added by hand is never touched. It is not adopted
|
|
28
|
+
* into the rule when it happens to match, and - the case that would
|
|
29
|
+
* actually lose someone's configuration - it is never removed when it
|
|
30
|
+
* stops matching, or when the rule is deleted outright.
|
|
31
|
+
*
|
|
32
|
+
* StatusPageResource.statusPageMonitorRuleId is what the second half rests
|
|
33
|
+
* on, so most of what follows is about that boundary. The other thing worth
|
|
34
|
+
* pinning down is that a monitor already on the page is never added twice,
|
|
35
|
+
* because that is the failure a public page shows to customers.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
39
|
+
"22222222-2222-4222-8222-222222222222",
|
|
40
|
+
);
|
|
41
|
+
const STATUS_PAGE_ID: ObjectID = new ObjectID(
|
|
42
|
+
"33333333-3333-4333-8333-333333333333",
|
|
43
|
+
);
|
|
44
|
+
const GROUP_ID: ObjectID = new ObjectID("44444444-4444-4444-8444-444444444444");
|
|
45
|
+
const OTHER_GROUP_ID: ObjectID = new ObjectID(
|
|
46
|
+
"45454545-4545-4545-8545-454545454545",
|
|
47
|
+
);
|
|
48
|
+
const RULE_ID: ObjectID = new ObjectID("11111111-1111-4111-8111-111111111111");
|
|
49
|
+
const OTHER_RULE_ID: ObjectID = new ObjectID(
|
|
50
|
+
"1a1a1a1a-1a1a-4a1a-8a1a-1a1a1a1a1a1a",
|
|
51
|
+
);
|
|
52
|
+
const MONITOR_A_ID: ObjectID = new ObjectID(
|
|
53
|
+
"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
54
|
+
);
|
|
55
|
+
const MONITOR_B_ID: ObjectID = new ObjectID(
|
|
56
|
+
"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
|
|
57
|
+
);
|
|
58
|
+
const MONITOR_C_ID: ObjectID = new ObjectID(
|
|
59
|
+
"cccccccc-cccc-4ccc-8ccc-cccccccccccc",
|
|
60
|
+
);
|
|
61
|
+
const RESOURCE_A_ID: ObjectID = new ObjectID(
|
|
62
|
+
"a1a1a1a1-a1a1-4a1a-8a1a-a1a1a1a1a1a1",
|
|
63
|
+
);
|
|
64
|
+
const RESOURCE_B_ID: ObjectID = new ObjectID(
|
|
65
|
+
"b1b1b1b1-b1b1-4b1b-8b1b-b1b1b1b1b1b1",
|
|
66
|
+
);
|
|
67
|
+
const LABEL_PRODUCTION_ID: ObjectID = new ObjectID(
|
|
68
|
+
"dddddddd-dddd-4ddd-8ddd-dddddddddddd",
|
|
69
|
+
);
|
|
70
|
+
const LABEL_TIER1_ID: ObjectID = new ObjectID(
|
|
71
|
+
"eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
|
|
72
|
+
);
|
|
73
|
+
const LABEL_STAGING_ID: ObjectID = new ObjectID(
|
|
74
|
+
"ffffffff-ffff-4fff-8fff-ffffffffffff",
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
function fakeLabel(id: ObjectID): Label {
|
|
78
|
+
return { id: id, _id: id.toString() } as unknown as Label;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function fakeMonitor(
|
|
82
|
+
id: ObjectID,
|
|
83
|
+
fields?: {
|
|
84
|
+
name?: string | undefined;
|
|
85
|
+
description?: string | undefined;
|
|
86
|
+
labels?: Array<ObjectID> | undefined;
|
|
87
|
+
projectId?: ObjectID | null | undefined;
|
|
88
|
+
},
|
|
89
|
+
): Monitor {
|
|
90
|
+
return {
|
|
91
|
+
id: id,
|
|
92
|
+
_id: id.toString(),
|
|
93
|
+
projectId: fields?.projectId === undefined ? PROJECT_ID : fields.projectId,
|
|
94
|
+
name: fields?.name === undefined ? "API Monitor" : fields.name,
|
|
95
|
+
description: fields?.description,
|
|
96
|
+
labels: (fields?.labels || []).map(fakeLabel),
|
|
97
|
+
} as unknown as Monitor;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function fakeRule(fields?: {
|
|
101
|
+
id?: ObjectID | undefined;
|
|
102
|
+
projectId?: ObjectID | null | undefined;
|
|
103
|
+
statusPageId?: ObjectID | null | undefined;
|
|
104
|
+
statusPageGroupId?: ObjectID | undefined;
|
|
105
|
+
isEnabled?: boolean | undefined;
|
|
106
|
+
monitorLabels?: Array<ObjectID> | undefined;
|
|
107
|
+
monitorNamePattern?: string | undefined;
|
|
108
|
+
monitorDescriptionPattern?: string | undefined;
|
|
109
|
+
showCurrentStatus?: boolean | undefined;
|
|
110
|
+
showUptimePercent?: boolean | undefined;
|
|
111
|
+
showStatusHistoryChart?: boolean | undefined;
|
|
112
|
+
uptimePercentPrecision?: UptimePrecision | undefined;
|
|
113
|
+
}): StatusPageMonitorRule {
|
|
114
|
+
const id: ObjectID = fields?.id || RULE_ID;
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
id: id,
|
|
118
|
+
_id: id.toString(),
|
|
119
|
+
projectId: fields?.projectId === undefined ? PROJECT_ID : fields.projectId,
|
|
120
|
+
statusPageId:
|
|
121
|
+
fields?.statusPageId === undefined ? STATUS_PAGE_ID : fields.statusPageId,
|
|
122
|
+
statusPageGroupId: fields?.statusPageGroupId,
|
|
123
|
+
isEnabled: fields?.isEnabled === undefined ? true : fields.isEnabled,
|
|
124
|
+
monitorLabels: (fields?.monitorLabels || []).map(fakeLabel),
|
|
125
|
+
monitorNamePattern: fields?.monitorNamePattern,
|
|
126
|
+
monitorDescriptionPattern: fields?.monitorDescriptionPattern,
|
|
127
|
+
showCurrentStatus: fields?.showCurrentStatus,
|
|
128
|
+
showUptimePercent: fields?.showUptimePercent,
|
|
129
|
+
showStatusHistoryChart: fields?.showStatusHistoryChart,
|
|
130
|
+
uptimePercentPrecision: fields?.uptimePercentPrecision,
|
|
131
|
+
} as unknown as StatusPageMonitorRule;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function fakeResource(fields: {
|
|
135
|
+
id: ObjectID;
|
|
136
|
+
monitorId: ObjectID;
|
|
137
|
+
statusPageMonitorRuleId?: ObjectID | undefined;
|
|
138
|
+
statusPageGroupId?: ObjectID | undefined;
|
|
139
|
+
showCurrentStatus?: boolean | undefined;
|
|
140
|
+
showUptimePercent?: boolean | undefined;
|
|
141
|
+
showStatusHistoryChart?: boolean | undefined;
|
|
142
|
+
uptimePercentPrecision?: UptimePrecision | undefined;
|
|
143
|
+
}): StatusPageResource {
|
|
144
|
+
return {
|
|
145
|
+
id: fields.id,
|
|
146
|
+
_id: fields.id.toString(),
|
|
147
|
+
monitorId: fields.monitorId,
|
|
148
|
+
statusPageMonitorRuleId: fields.statusPageMonitorRuleId,
|
|
149
|
+
statusPageGroupId: fields.statusPageGroupId,
|
|
150
|
+
showCurrentStatus: fields.showCurrentStatus,
|
|
151
|
+
showUptimePercent: fields.showUptimePercent,
|
|
152
|
+
showStatusHistoryChart: fields.showStatusHistoryChart,
|
|
153
|
+
uptimePercentPrecision: fields.uptimePercentPrecision,
|
|
154
|
+
} as unknown as StatusPageResource;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The resource the engine handed to StatusPageResourceService.create. */
|
|
158
|
+
function createdResource(
|
|
159
|
+
createSpy: jest.SpyInstance,
|
|
160
|
+
callIndex: number = 0,
|
|
161
|
+
): StatusPageResource {
|
|
162
|
+
const call: { data: StatusPageResource } = createSpy.mock.calls[
|
|
163
|
+
callIndex
|
|
164
|
+
]![0] as { data: StatusPageResource };
|
|
165
|
+
|
|
166
|
+
return call.data;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
describe("StatusPageMonitorRuleEngineService.doesMonitorMatchRule", () => {
|
|
170
|
+
it("matches a monitor carrying any one of the rule's labels", () => {
|
|
171
|
+
expect(
|
|
172
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
173
|
+
monitor: fakeMonitor(MONITOR_A_ID, {
|
|
174
|
+
labels: [LABEL_STAGING_ID, LABEL_TIER1_ID],
|
|
175
|
+
}),
|
|
176
|
+
rule: fakeRule({
|
|
177
|
+
monitorLabels: [LABEL_PRODUCTION_ID, LABEL_TIER1_ID],
|
|
178
|
+
}),
|
|
179
|
+
}),
|
|
180
|
+
).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("rejects a monitor carrying none of them", () => {
|
|
184
|
+
expect(
|
|
185
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
186
|
+
monitor: fakeMonitor(MONITOR_A_ID, { labels: [LABEL_STAGING_ID] }),
|
|
187
|
+
rule: fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
188
|
+
}),
|
|
189
|
+
).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("rejects an unlabelled monitor against a label rule", () => {
|
|
193
|
+
expect(
|
|
194
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
195
|
+
monitor: fakeMonitor(MONITOR_A_ID, { labels: [] }),
|
|
196
|
+
rule: fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
197
|
+
}),
|
|
198
|
+
).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("matches the name pattern case-insensitively", () => {
|
|
202
|
+
expect(
|
|
203
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
204
|
+
monitor: fakeMonitor(MONITOR_A_ID, { name: "API Gateway" }),
|
|
205
|
+
rule: fakeRule({ monitorNamePattern: "^api" }),
|
|
206
|
+
}),
|
|
207
|
+
).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("treats the name pattern as a regex, not a substring", () => {
|
|
211
|
+
expect(
|
|
212
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
213
|
+
monitor: fakeMonitor(MONITOR_A_ID, { name: "internal-api" }),
|
|
214
|
+
rule: fakeRule({ monitorNamePattern: "^api" }),
|
|
215
|
+
}),
|
|
216
|
+
).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("matches the description pattern", () => {
|
|
220
|
+
expect(
|
|
221
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
222
|
+
monitor: fakeMonitor(MONITOR_A_ID, {
|
|
223
|
+
description: "Customer facing checkout flow",
|
|
224
|
+
}),
|
|
225
|
+
rule: fakeRule({ monitorDescriptionPattern: "customer facing" }),
|
|
226
|
+
}),
|
|
227
|
+
).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("rejects a monitor with no description against a description rule", () => {
|
|
231
|
+
expect(
|
|
232
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
233
|
+
monitor: fakeMonitor(MONITOR_A_ID, { description: undefined }),
|
|
234
|
+
rule: fakeRule({ monitorDescriptionPattern: "tier-1" }),
|
|
235
|
+
}),
|
|
236
|
+
).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("ANDs the criteria - passing the labels is not enough if the name fails", () => {
|
|
240
|
+
expect(
|
|
241
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
242
|
+
monitor: fakeMonitor(MONITOR_A_ID, {
|
|
243
|
+
name: "worker-queue",
|
|
244
|
+
labels: [LABEL_PRODUCTION_ID],
|
|
245
|
+
}),
|
|
246
|
+
rule: fakeRule({
|
|
247
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
248
|
+
monitorNamePattern: "^api",
|
|
249
|
+
}),
|
|
250
|
+
}),
|
|
251
|
+
).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("matches when every specified criterion passes", () => {
|
|
255
|
+
expect(
|
|
256
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
257
|
+
monitor: fakeMonitor(MONITOR_A_ID, {
|
|
258
|
+
name: "api-checkout",
|
|
259
|
+
description: "tier-1 service",
|
|
260
|
+
labels: [LABEL_PRODUCTION_ID],
|
|
261
|
+
}),
|
|
262
|
+
rule: fakeRule({
|
|
263
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
264
|
+
monitorNamePattern: "^api",
|
|
265
|
+
monitorDescriptionPattern: "tier-1",
|
|
266
|
+
}),
|
|
267
|
+
}),
|
|
268
|
+
).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("matches everything when the name pattern is .* - the documented escape hatch", () => {
|
|
272
|
+
expect(
|
|
273
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
274
|
+
monitor: fakeMonitor(MONITOR_A_ID, { name: "anything at all" }),
|
|
275
|
+
rule: fakeRule({ monitorNamePattern: ".*" }),
|
|
276
|
+
}),
|
|
277
|
+
).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
/*
|
|
281
|
+
* The safety property: an empty form must not put every monitor in the
|
|
282
|
+
* project on a public status page.
|
|
283
|
+
*/
|
|
284
|
+
it("matches nothing when the rule has no criteria at all", () => {
|
|
285
|
+
expect(
|
|
286
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
287
|
+
monitor: fakeMonitor(MONITOR_A_ID, {
|
|
288
|
+
name: "api",
|
|
289
|
+
labels: [LABEL_PRODUCTION_ID],
|
|
290
|
+
}),
|
|
291
|
+
rule: fakeRule({}),
|
|
292
|
+
}),
|
|
293
|
+
).toBe(false);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("matches nothing rather than throwing when the pattern is not a valid regex", () => {
|
|
297
|
+
expect(
|
|
298
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
299
|
+
monitor: fakeMonitor(MONITOR_A_ID, { name: "api" }),
|
|
300
|
+
rule: fakeRule({ monitorNamePattern: "([unclosed" }),
|
|
301
|
+
}),
|
|
302
|
+
).toBe(false);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
/*
|
|
306
|
+
* #2940. `*api*` throws "Nothing to repeat" as a regex, so a private regex
|
|
307
|
+
* test here would give the user a rule that silently matches nothing. The
|
|
308
|
+
* shared RulePatternMatchUtil falls back to a wildcard glob, which is what
|
|
309
|
+
* the sibling network device rules do and what people actually type.
|
|
310
|
+
*/
|
|
311
|
+
it("matches a `*` wildcard glob, not just a regex", () => {
|
|
312
|
+
expect(
|
|
313
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
314
|
+
monitor: fakeMonitor(MONITOR_A_ID, { name: "prod-checkout-api" }),
|
|
315
|
+
rule: fakeRule({ monitorNamePattern: "*checkout*" }),
|
|
316
|
+
}),
|
|
317
|
+
).toBe(true);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("does not match a glob that the name fails", () => {
|
|
321
|
+
expect(
|
|
322
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
323
|
+
monitor: fakeMonitor(MONITOR_A_ID, { name: "prod-worker" }),
|
|
324
|
+
rule: fakeRule({ monitorNamePattern: "*checkout*" }),
|
|
325
|
+
}),
|
|
326
|
+
).toBe(false);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("matches a glob case-insensitively", () => {
|
|
330
|
+
expect(
|
|
331
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
332
|
+
monitor: fakeMonitor(MONITOR_A_ID, { name: "Prod-Checkout-API" }),
|
|
333
|
+
rule: fakeRule({ monitorNamePattern: "*checkout*" }),
|
|
334
|
+
}),
|
|
335
|
+
).toBe(true);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
/*
|
|
339
|
+
* The glob is anchored to the whole name, unlike the (unanchored) regex
|
|
340
|
+
* attempt - `*gateway` means "ends with gateway", not "contains it".
|
|
341
|
+
*/
|
|
342
|
+
it("anchors a glob to the whole name", () => {
|
|
343
|
+
expect(
|
|
344
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
345
|
+
monitor: fakeMonitor(MONITOR_A_ID, { name: "prod-api-gateway" }),
|
|
346
|
+
rule: fakeRule({ monitorNamePattern: "*gateway" }),
|
|
347
|
+
}),
|
|
348
|
+
).toBe(true);
|
|
349
|
+
|
|
350
|
+
expect(
|
|
351
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
352
|
+
monitor: fakeMonitor(MONITOR_A_ID, { name: "gateway-prod" }),
|
|
353
|
+
rule: fakeRule({ monitorNamePattern: "*gateway" }),
|
|
354
|
+
}),
|
|
355
|
+
).toBe(false);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("applies the glob fallback to the description pattern too", () => {
|
|
359
|
+
expect(
|
|
360
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
361
|
+
monitor: fakeMonitor(MONITOR_A_ID, {
|
|
362
|
+
description: "customer facing tier-1 service",
|
|
363
|
+
}),
|
|
364
|
+
rule: fakeRule({ monitorDescriptionPattern: "*tier-1*" }),
|
|
365
|
+
}),
|
|
366
|
+
).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
/*
|
|
370
|
+
* A pattern is a superset: anything that matched as a regex before still
|
|
371
|
+
* matches, and the glob only ever adds. `.*` must not start behaving like a
|
|
372
|
+
* literal glob.
|
|
373
|
+
*/
|
|
374
|
+
it("still prefers regex semantics for a pattern that is valid as both", () => {
|
|
375
|
+
expect(
|
|
376
|
+
StatusPageMonitorRuleEngineService.doesMonitorMatchRule({
|
|
377
|
+
monitor: fakeMonitor(MONITOR_A_ID, { name: "anything" }),
|
|
378
|
+
rule: fakeRule({ monitorNamePattern: ".*" }),
|
|
379
|
+
}),
|
|
380
|
+
).toBe(true);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe("StatusPageMonitorRuleEngineService.syncResourcesForRule", () => {
|
|
385
|
+
let ruleFindOneByIdSpy: jest.SpyInstance;
|
|
386
|
+
let monitorFindBySpy: jest.SpyInstance;
|
|
387
|
+
let resourceFindBySpy: jest.SpyInstance;
|
|
388
|
+
let resourceCreateSpy: jest.SpyInstance;
|
|
389
|
+
let resourceDeleteSpy: jest.SpyInstance;
|
|
390
|
+
let resourceUpdateSpy: jest.SpyInstance;
|
|
391
|
+
|
|
392
|
+
beforeEach(() => {
|
|
393
|
+
ruleFindOneByIdSpy = jest.spyOn(
|
|
394
|
+
StatusPageMonitorRuleService,
|
|
395
|
+
"findOneById",
|
|
396
|
+
);
|
|
397
|
+
monitorFindBySpy = jest.spyOn(MonitorService, "findBy");
|
|
398
|
+
resourceFindBySpy = jest
|
|
399
|
+
.spyOn(StatusPageResourceService, "findBy")
|
|
400
|
+
.mockResolvedValue([]);
|
|
401
|
+
resourceCreateSpy = jest
|
|
402
|
+
.spyOn(StatusPageResourceService, "create")
|
|
403
|
+
.mockResolvedValue(new StatusPageResource());
|
|
404
|
+
resourceDeleteSpy = jest
|
|
405
|
+
.spyOn(StatusPageResourceService, "deleteOneById")
|
|
406
|
+
.mockResolvedValue(1);
|
|
407
|
+
resourceUpdateSpy = jest
|
|
408
|
+
.spyOn(StatusPageResourceService, "updateOneById")
|
|
409
|
+
.mockResolvedValue(1);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
afterEach(() => {
|
|
413
|
+
jest.restoreAllMocks();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("adds every monitor in the project that the rule matches", async () => {
|
|
417
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
418
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
419
|
+
);
|
|
420
|
+
monitorFindBySpy.mockResolvedValue([
|
|
421
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
422
|
+
fakeMonitor(MONITOR_B_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
423
|
+
]);
|
|
424
|
+
|
|
425
|
+
const result: StatusPageMonitorRuleSyncResult =
|
|
426
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
427
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
expect(resourceCreateSpy).toHaveBeenCalledTimes(2);
|
|
431
|
+
expect(result.monitorIdsAdded.sort()).toEqual(
|
|
432
|
+
[MONITOR_A_ID.toString(), MONITOR_B_ID.toString()].sort(),
|
|
433
|
+
);
|
|
434
|
+
expect(result.statusPageResourceIdsRemoved).toEqual([]);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it("puts them in the group the rule names - the whole point of asking", async () => {
|
|
438
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
439
|
+
fakeRule({
|
|
440
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
441
|
+
statusPageGroupId: GROUP_ID,
|
|
442
|
+
}),
|
|
443
|
+
);
|
|
444
|
+
monitorFindBySpy.mockResolvedValue([
|
|
445
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
446
|
+
]);
|
|
447
|
+
|
|
448
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
449
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
expect(createdResource(resourceCreateSpy).statusPageGroupId).toEqual(
|
|
453
|
+
GROUP_ID,
|
|
454
|
+
);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("leaves the group empty when the rule names none, so the resource lands ungrouped", async () => {
|
|
458
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
459
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
460
|
+
);
|
|
461
|
+
monitorFindBySpy.mockResolvedValue([
|
|
462
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
463
|
+
]);
|
|
464
|
+
|
|
465
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
466
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
expect(
|
|
470
|
+
createdResource(resourceCreateSpy).statusPageGroupId,
|
|
471
|
+
).toBeUndefined();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("stamps the rule on what it creates so it can undo it later", async () => {
|
|
475
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
476
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
477
|
+
);
|
|
478
|
+
monitorFindBySpy.mockResolvedValue([
|
|
479
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
480
|
+
]);
|
|
481
|
+
|
|
482
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
483
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
expect(createdResource(resourceCreateSpy).statusPageMonitorRuleId).toEqual(
|
|
487
|
+
RULE_ID,
|
|
488
|
+
);
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
it("names the resource after the monitor, so nobody has to type one", async () => {
|
|
492
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
493
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
494
|
+
);
|
|
495
|
+
monitorFindBySpy.mockResolvedValue([
|
|
496
|
+
fakeMonitor(MONITOR_A_ID, {
|
|
497
|
+
name: "Checkout API",
|
|
498
|
+
labels: [LABEL_PRODUCTION_ID],
|
|
499
|
+
}),
|
|
500
|
+
]);
|
|
501
|
+
|
|
502
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
503
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
expect(createdResource(resourceCreateSpy).displayName).toBe("Checkout API");
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("copies the rule's display options onto every resource it creates", async () => {
|
|
510
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
511
|
+
fakeRule({
|
|
512
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
513
|
+
showCurrentStatus: false,
|
|
514
|
+
showUptimePercent: true,
|
|
515
|
+
showStatusHistoryChart: false,
|
|
516
|
+
uptimePercentPrecision: UptimePrecision.THREE_DECIMAL,
|
|
517
|
+
}),
|
|
518
|
+
);
|
|
519
|
+
monitorFindBySpy.mockResolvedValue([
|
|
520
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
521
|
+
]);
|
|
522
|
+
|
|
523
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
524
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const resource: StatusPageResource = createdResource(resourceCreateSpy);
|
|
528
|
+
|
|
529
|
+
expect(resource.showCurrentStatus).toBe(false);
|
|
530
|
+
expect(resource.showUptimePercent).toBe(true);
|
|
531
|
+
expect(resource.showStatusHistoryChart).toBe(false);
|
|
532
|
+
expect(resource.uptimePercentPrecision).toBe(UptimePrecision.THREE_DECIMAL);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it("writes as root - the rule is server-side and must not need a caller's permissions", async () => {
|
|
536
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
537
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
538
|
+
);
|
|
539
|
+
monitorFindBySpy.mockResolvedValue([
|
|
540
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
541
|
+
]);
|
|
542
|
+
|
|
543
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
544
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const call: { props: { isRoot: boolean } } = resourceCreateSpy.mock
|
|
548
|
+
.calls[0]![0] as { props: { isRoot: boolean } };
|
|
549
|
+
|
|
550
|
+
expect(call.props.isRoot).toBe(true);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it("applies the regex in memory, so a Postgres-only pattern cannot leak a monitor onto the page", async () => {
|
|
554
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
555
|
+
fakeRule({
|
|
556
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
557
|
+
monitorNamePattern: "^api",
|
|
558
|
+
}),
|
|
559
|
+
);
|
|
560
|
+
// The label query is what the database narrows; the name still has to hold.
|
|
561
|
+
monitorFindBySpy.mockResolvedValue([
|
|
562
|
+
fakeMonitor(MONITOR_A_ID, {
|
|
563
|
+
name: "api-checkout",
|
|
564
|
+
labels: [LABEL_PRODUCTION_ID],
|
|
565
|
+
}),
|
|
566
|
+
fakeMonitor(MONITOR_B_ID, {
|
|
567
|
+
name: "worker-queue",
|
|
568
|
+
labels: [LABEL_PRODUCTION_ID],
|
|
569
|
+
}),
|
|
570
|
+
]);
|
|
571
|
+
|
|
572
|
+
const result: StatusPageMonitorRuleSyncResult =
|
|
573
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
574
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
expect(result.monitorIdsAdded).toEqual([MONITOR_A_ID.toString()]);
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it("does not add a monitor that is already on the page - no duplicates for customers to see", async () => {
|
|
581
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
582
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
583
|
+
);
|
|
584
|
+
monitorFindBySpy.mockResolvedValue([
|
|
585
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
586
|
+
]);
|
|
587
|
+
resourceFindBySpy.mockResolvedValue([
|
|
588
|
+
fakeResource({ id: RESOURCE_A_ID, monitorId: MONITOR_A_ID }),
|
|
589
|
+
]);
|
|
590
|
+
|
|
591
|
+
const result: StatusPageMonitorRuleSyncResult =
|
|
592
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
593
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
expect(resourceCreateSpy).not.toHaveBeenCalled();
|
|
597
|
+
expect(result.monitorIdsAdded).toEqual([]);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it("removes a resource it owns once the monitor stops matching", async () => {
|
|
601
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
602
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
603
|
+
);
|
|
604
|
+
monitorFindBySpy.mockResolvedValue([]);
|
|
605
|
+
resourceFindBySpy.mockResolvedValue([
|
|
606
|
+
fakeResource({
|
|
607
|
+
id: RESOURCE_A_ID,
|
|
608
|
+
monitorId: MONITOR_A_ID,
|
|
609
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
610
|
+
}),
|
|
611
|
+
]);
|
|
612
|
+
|
|
613
|
+
const result: StatusPageMonitorRuleSyncResult =
|
|
614
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
615
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
expect(resourceDeleteSpy).toHaveBeenCalledTimes(1);
|
|
619
|
+
expect(result.statusPageResourceIdsRemoved).toEqual([
|
|
620
|
+
RESOURCE_A_ID.toString(),
|
|
621
|
+
]);
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
/*
|
|
625
|
+
* The configuration-losing case. A resource with no rule stamped on it was
|
|
626
|
+
* added by a person, and no rule evaluation may ever remove it.
|
|
627
|
+
*/
|
|
628
|
+
it("never removes a resource a human added, even when it no longer matches", async () => {
|
|
629
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
630
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
631
|
+
);
|
|
632
|
+
monitorFindBySpy.mockResolvedValue([]);
|
|
633
|
+
resourceFindBySpy.mockResolvedValue([
|
|
634
|
+
fakeResource({ id: RESOURCE_A_ID, monitorId: MONITOR_A_ID }),
|
|
635
|
+
]);
|
|
636
|
+
|
|
637
|
+
const result: StatusPageMonitorRuleSyncResult =
|
|
638
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
639
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
expect(resourceDeleteSpy).not.toHaveBeenCalled();
|
|
643
|
+
expect(result.statusPageResourceIdsRemoved).toEqual([]);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it("never removes a resource another rule owns", async () => {
|
|
647
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
648
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
649
|
+
);
|
|
650
|
+
monitorFindBySpy.mockResolvedValue([]);
|
|
651
|
+
resourceFindBySpy.mockResolvedValue([
|
|
652
|
+
fakeResource({
|
|
653
|
+
id: RESOURCE_B_ID,
|
|
654
|
+
monitorId: MONITOR_B_ID,
|
|
655
|
+
statusPageMonitorRuleId: OTHER_RULE_ID,
|
|
656
|
+
}),
|
|
657
|
+
]);
|
|
658
|
+
|
|
659
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
660
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
expect(resourceDeleteSpy).not.toHaveBeenCalled();
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it("does not adopt a manual resource it happens to match, so a later label change leaves it alone", async () => {
|
|
667
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
668
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
669
|
+
);
|
|
670
|
+
monitorFindBySpy.mockResolvedValue([
|
|
671
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
672
|
+
]);
|
|
673
|
+
resourceFindBySpy.mockResolvedValue([
|
|
674
|
+
fakeResource({ id: RESOURCE_A_ID, monitorId: MONITOR_A_ID }),
|
|
675
|
+
]);
|
|
676
|
+
|
|
677
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
678
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
// Nothing created and nothing stamped: the resource is still manual.
|
|
682
|
+
expect(resourceCreateSpy).not.toHaveBeenCalled();
|
|
683
|
+
expect(resourceDeleteSpy).not.toHaveBeenCalled();
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
/*
|
|
687
|
+
* Editing "Add Monitors To Group" has to move the monitors the rule already
|
|
688
|
+
* added. A rule that only applied its new group to monitors it adds in
|
|
689
|
+
* FUTURE would make the edit look like it silently did nothing — the user
|
|
690
|
+
* changed the one field the whole feature is about and the page did not
|
|
691
|
+
* move.
|
|
692
|
+
*/
|
|
693
|
+
it("re-homes the resources it owns when the rule's group changes", async () => {
|
|
694
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
695
|
+
fakeRule({
|
|
696
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
697
|
+
statusPageGroupId: OTHER_GROUP_ID,
|
|
698
|
+
}),
|
|
699
|
+
);
|
|
700
|
+
monitorFindBySpy.mockResolvedValue([
|
|
701
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
702
|
+
]);
|
|
703
|
+
resourceFindBySpy.mockResolvedValue([
|
|
704
|
+
fakeResource({
|
|
705
|
+
id: RESOURCE_A_ID,
|
|
706
|
+
monitorId: MONITOR_A_ID,
|
|
707
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
708
|
+
statusPageGroupId: GROUP_ID,
|
|
709
|
+
}),
|
|
710
|
+
]);
|
|
711
|
+
|
|
712
|
+
const result: StatusPageMonitorRuleSyncResult =
|
|
713
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
714
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
expect(resourceUpdateSpy).toHaveBeenCalledTimes(1);
|
|
718
|
+
const call: { id: ObjectID; data: { statusPageGroupId: ObjectID } } =
|
|
719
|
+
resourceUpdateSpy.mock.calls[0]![0] as {
|
|
720
|
+
id: ObjectID;
|
|
721
|
+
data: { statusPageGroupId: ObjectID };
|
|
722
|
+
};
|
|
723
|
+
expect(call.id).toEqual(RESOURCE_A_ID);
|
|
724
|
+
expect(call.data.statusPageGroupId).toEqual(OTHER_GROUP_ID);
|
|
725
|
+
expect(result.statusPageResourceIdsUpdated).toEqual([
|
|
726
|
+
RESOURCE_A_ID.toString(),
|
|
727
|
+
]);
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it("moves them back to ungrouped when the rule's group is cleared", async () => {
|
|
731
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
732
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
733
|
+
);
|
|
734
|
+
monitorFindBySpy.mockResolvedValue([
|
|
735
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
736
|
+
]);
|
|
737
|
+
resourceFindBySpy.mockResolvedValue([
|
|
738
|
+
fakeResource({
|
|
739
|
+
id: RESOURCE_A_ID,
|
|
740
|
+
monitorId: MONITOR_A_ID,
|
|
741
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
742
|
+
statusPageGroupId: GROUP_ID,
|
|
743
|
+
}),
|
|
744
|
+
]);
|
|
745
|
+
|
|
746
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
747
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
const call: { data: { statusPageGroupId: ObjectID | null } } =
|
|
751
|
+
resourceUpdateSpy.mock.calls[0]![0] as {
|
|
752
|
+
data: { statusPageGroupId: ObjectID | null };
|
|
753
|
+
};
|
|
754
|
+
expect(call.data.statusPageGroupId).toBeNull();
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it("re-applies the rule's display options to the resources it owns", async () => {
|
|
758
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
759
|
+
fakeRule({
|
|
760
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
761
|
+
showUptimePercent: false,
|
|
762
|
+
uptimePercentPrecision: UptimePrecision.TWO_DECIMAL,
|
|
763
|
+
}),
|
|
764
|
+
);
|
|
765
|
+
monitorFindBySpy.mockResolvedValue([
|
|
766
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
767
|
+
]);
|
|
768
|
+
resourceFindBySpy.mockResolvedValue([
|
|
769
|
+
fakeResource({
|
|
770
|
+
id: RESOURCE_A_ID,
|
|
771
|
+
monitorId: MONITOR_A_ID,
|
|
772
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
773
|
+
showUptimePercent: true,
|
|
774
|
+
uptimePercentPrecision: UptimePrecision.ONE_DECIMAL,
|
|
775
|
+
}),
|
|
776
|
+
]);
|
|
777
|
+
|
|
778
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
779
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
const call: {
|
|
783
|
+
data: {
|
|
784
|
+
showUptimePercent: boolean;
|
|
785
|
+
uptimePercentPrecision: UptimePrecision;
|
|
786
|
+
};
|
|
787
|
+
} = resourceUpdateSpy.mock.calls[0]![0] as {
|
|
788
|
+
data: {
|
|
789
|
+
showUptimePercent: boolean;
|
|
790
|
+
uptimePercentPrecision: UptimePrecision;
|
|
791
|
+
};
|
|
792
|
+
};
|
|
793
|
+
expect(call.data.showUptimePercent).toBe(false);
|
|
794
|
+
expect(call.data.uptimePercentPrecision).toBe(UptimePrecision.TWO_DECIMAL);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
/*
|
|
798
|
+
* Re-running an unchanged rule is the common case (any monitor edit can
|
|
799
|
+
* trigger it), so it must not write.
|
|
800
|
+
*/
|
|
801
|
+
it("writes nothing when the resources it owns already agree with the rule", async () => {
|
|
802
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
803
|
+
fakeRule({
|
|
804
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
805
|
+
statusPageGroupId: GROUP_ID,
|
|
806
|
+
}),
|
|
807
|
+
);
|
|
808
|
+
monitorFindBySpy.mockResolvedValue([
|
|
809
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
810
|
+
]);
|
|
811
|
+
resourceFindBySpy.mockResolvedValue([
|
|
812
|
+
fakeResource({
|
|
813
|
+
id: RESOURCE_A_ID,
|
|
814
|
+
monitorId: MONITOR_A_ID,
|
|
815
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
816
|
+
statusPageGroupId: GROUP_ID,
|
|
817
|
+
showCurrentStatus: true,
|
|
818
|
+
showUptimePercent: true,
|
|
819
|
+
showStatusHistoryChart: true,
|
|
820
|
+
}),
|
|
821
|
+
]);
|
|
822
|
+
|
|
823
|
+
const result: StatusPageMonitorRuleSyncResult =
|
|
824
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
825
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
expect(resourceUpdateSpy).not.toHaveBeenCalled();
|
|
829
|
+
expect(result.statusPageResourceIdsUpdated).toEqual([]);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
it("never re-homes a resource a human added, however far it has drifted", async () => {
|
|
833
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
834
|
+
fakeRule({
|
|
835
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
836
|
+
statusPageGroupId: OTHER_GROUP_ID,
|
|
837
|
+
}),
|
|
838
|
+
);
|
|
839
|
+
monitorFindBySpy.mockResolvedValue([
|
|
840
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
841
|
+
]);
|
|
842
|
+
resourceFindBySpy.mockResolvedValue([
|
|
843
|
+
fakeResource({
|
|
844
|
+
id: RESOURCE_A_ID,
|
|
845
|
+
monitorId: MONITOR_A_ID,
|
|
846
|
+
statusPageGroupId: GROUP_ID,
|
|
847
|
+
}),
|
|
848
|
+
]);
|
|
849
|
+
|
|
850
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
851
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
expect(resourceUpdateSpy).not.toHaveBeenCalled();
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
it("never re-homes a resource another rule owns", async () => {
|
|
858
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
859
|
+
fakeRule({
|
|
860
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
861
|
+
statusPageGroupId: OTHER_GROUP_ID,
|
|
862
|
+
}),
|
|
863
|
+
);
|
|
864
|
+
monitorFindBySpy.mockResolvedValue([
|
|
865
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
866
|
+
]);
|
|
867
|
+
resourceFindBySpy.mockResolvedValue([
|
|
868
|
+
fakeResource({
|
|
869
|
+
id: RESOURCE_A_ID,
|
|
870
|
+
monitorId: MONITOR_A_ID,
|
|
871
|
+
statusPageMonitorRuleId: OTHER_RULE_ID,
|
|
872
|
+
statusPageGroupId: GROUP_ID,
|
|
873
|
+
}),
|
|
874
|
+
]);
|
|
875
|
+
|
|
876
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
877
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
expect(resourceUpdateSpy).not.toHaveBeenCalled();
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
it("does not reconcile a resource whose monitor the rule has stopped claiming - it deletes it", async () => {
|
|
884
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
885
|
+
fakeRule({
|
|
886
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
887
|
+
statusPageGroupId: OTHER_GROUP_ID,
|
|
888
|
+
}),
|
|
889
|
+
);
|
|
890
|
+
monitorFindBySpy.mockResolvedValue([]);
|
|
891
|
+
resourceFindBySpy.mockResolvedValue([
|
|
892
|
+
fakeResource({
|
|
893
|
+
id: RESOURCE_A_ID,
|
|
894
|
+
monitorId: MONITOR_A_ID,
|
|
895
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
896
|
+
statusPageGroupId: GROUP_ID,
|
|
897
|
+
}),
|
|
898
|
+
]);
|
|
899
|
+
|
|
900
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
901
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
expect(resourceUpdateSpy).not.toHaveBeenCalled();
|
|
905
|
+
expect(resourceDeleteSpy).toHaveBeenCalledTimes(1);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
/*
|
|
909
|
+
* Narrowing one rule must not take a monitor off the page that a different
|
|
910
|
+
* rule on the same page still claims. The rule-side pass only knows its own
|
|
911
|
+
* rule, so it hands each released monitor to the monitor-side pass, which
|
|
912
|
+
* consults every rule.
|
|
913
|
+
*/
|
|
914
|
+
it("hands a released monitor to another rule that still claims it", async () => {
|
|
915
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
916
|
+
fakeRule({ monitorLabels: [LABEL_TIER1_ID] }),
|
|
917
|
+
);
|
|
918
|
+
monitorFindBySpy.mockResolvedValue([]);
|
|
919
|
+
resourceFindBySpy.mockResolvedValue([
|
|
920
|
+
fakeResource({
|
|
921
|
+
id: RESOURCE_A_ID,
|
|
922
|
+
monitorId: MONITOR_A_ID,
|
|
923
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
924
|
+
}),
|
|
925
|
+
]);
|
|
926
|
+
|
|
927
|
+
const syncRulesForMonitorSpy: jest.SpyInstance = jest
|
|
928
|
+
.spyOn(StatusPageMonitorRuleEngineService, "syncRulesForMonitor")
|
|
929
|
+
.mockResolvedValue([]);
|
|
930
|
+
|
|
931
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
932
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
expect(syncRulesForMonitorSpy).toHaveBeenCalledTimes(1);
|
|
936
|
+
const call: { monitorId: ObjectID } = syncRulesForMonitorSpy.mock
|
|
937
|
+
.calls[0]![0] as { monitorId: ObjectID };
|
|
938
|
+
expect(call.monitorId.toString()).toBe(MONITOR_A_ID.toString());
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
it("does not re-home monitors it did not release", async () => {
|
|
942
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
943
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
944
|
+
);
|
|
945
|
+
monitorFindBySpy.mockResolvedValue([
|
|
946
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
947
|
+
]);
|
|
948
|
+
|
|
949
|
+
const syncRulesForMonitorSpy: jest.SpyInstance = jest
|
|
950
|
+
.spyOn(StatusPageMonitorRuleEngineService, "syncRulesForMonitor")
|
|
951
|
+
.mockResolvedValue([]);
|
|
952
|
+
|
|
953
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
954
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
expect(syncRulesForMonitorSpy).not.toHaveBeenCalled();
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
it("still reports its own removal when the hand-off fails", async () => {
|
|
961
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
962
|
+
fakeRule({ monitorLabels: [LABEL_TIER1_ID] }),
|
|
963
|
+
);
|
|
964
|
+
monitorFindBySpy.mockResolvedValue([]);
|
|
965
|
+
resourceFindBySpy.mockResolvedValue([
|
|
966
|
+
fakeResource({
|
|
967
|
+
id: RESOURCE_A_ID,
|
|
968
|
+
monitorId: MONITOR_A_ID,
|
|
969
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
970
|
+
}),
|
|
971
|
+
]);
|
|
972
|
+
jest
|
|
973
|
+
.spyOn(StatusPageMonitorRuleEngineService, "syncRulesForMonitor")
|
|
974
|
+
.mockRejectedValue(new Error("db down"));
|
|
975
|
+
|
|
976
|
+
const result: StatusPageMonitorRuleSyncResult =
|
|
977
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
978
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
expect(result.statusPageResourceIdsRemoved).toEqual([
|
|
982
|
+
RESOURCE_A_ID.toString(),
|
|
983
|
+
]);
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
it("adds and removes in the same pass, so a re-pointed rule fully re-derives the page", async () => {
|
|
987
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
988
|
+
fakeRule({ monitorLabels: [LABEL_TIER1_ID] }),
|
|
989
|
+
);
|
|
990
|
+
monitorFindBySpy.mockResolvedValue([
|
|
991
|
+
fakeMonitor(MONITOR_B_ID, { labels: [LABEL_TIER1_ID] }),
|
|
992
|
+
]);
|
|
993
|
+
resourceFindBySpy.mockResolvedValue([
|
|
994
|
+
fakeResource({
|
|
995
|
+
id: RESOURCE_A_ID,
|
|
996
|
+
monitorId: MONITOR_A_ID,
|
|
997
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
998
|
+
}),
|
|
999
|
+
]);
|
|
1000
|
+
|
|
1001
|
+
const result: StatusPageMonitorRuleSyncResult =
|
|
1002
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
1003
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
expect(result.monitorIdsAdded).toEqual([MONITOR_B_ID.toString()]);
|
|
1007
|
+
expect(result.statusPageResourceIdsRemoved).toEqual([
|
|
1008
|
+
RESOURCE_A_ID.toString(),
|
|
1009
|
+
]);
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
/*
|
|
1013
|
+
* Disabling is the reversible half of deleting. If it left the resources
|
|
1014
|
+
* behind, the toggle would not mean anything.
|
|
1015
|
+
*/
|
|
1016
|
+
it("removes everything it added when the rule is disabled", async () => {
|
|
1017
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
1018
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID], isEnabled: false }),
|
|
1019
|
+
);
|
|
1020
|
+
resourceFindBySpy.mockResolvedValue([
|
|
1021
|
+
fakeResource({
|
|
1022
|
+
id: RESOURCE_A_ID,
|
|
1023
|
+
monitorId: MONITOR_A_ID,
|
|
1024
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1025
|
+
}),
|
|
1026
|
+
]);
|
|
1027
|
+
|
|
1028
|
+
const result: StatusPageMonitorRuleSyncResult =
|
|
1029
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
1030
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
expect(monitorFindBySpy).not.toHaveBeenCalled();
|
|
1034
|
+
expect(result.statusPageResourceIdsRemoved).toEqual([
|
|
1035
|
+
RESOURCE_A_ID.toString(),
|
|
1036
|
+
]);
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
it("still leaves manual resources alone when the rule is disabled", async () => {
|
|
1040
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
1041
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID], isEnabled: false }),
|
|
1042
|
+
);
|
|
1043
|
+
resourceFindBySpy.mockResolvedValue([
|
|
1044
|
+
fakeResource({ id: RESOURCE_A_ID, monitorId: MONITOR_A_ID }),
|
|
1045
|
+
]);
|
|
1046
|
+
|
|
1047
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
1048
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
expect(resourceDeleteSpy).not.toHaveBeenCalled();
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
it("scopes the monitor search to the rule's own project", async () => {
|
|
1055
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
1056
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
1057
|
+
);
|
|
1058
|
+
monitorFindBySpy.mockResolvedValue([]);
|
|
1059
|
+
|
|
1060
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
1061
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
const call: { query: { projectId: ObjectID } } = monitorFindBySpy.mock
|
|
1065
|
+
.calls[0]![0] as { query: { projectId: ObjectID } };
|
|
1066
|
+
|
|
1067
|
+
expect(call.query.projectId).toEqual(PROJECT_ID);
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
it("reads only this status page's resources, not the whole project's", async () => {
|
|
1071
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
1072
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
1073
|
+
);
|
|
1074
|
+
monitorFindBySpy.mockResolvedValue([]);
|
|
1075
|
+
|
|
1076
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
1077
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
const call: { query: { statusPageId: ObjectID } } = resourceFindBySpy.mock
|
|
1081
|
+
.calls[0]![0] as { query: { statusPageId: ObjectID } };
|
|
1082
|
+
|
|
1083
|
+
expect(call.query.statusPageId).toEqual(STATUS_PAGE_ID);
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
it("does nothing for a rule that no longer exists", async () => {
|
|
1087
|
+
ruleFindOneByIdSpy.mockResolvedValue(null);
|
|
1088
|
+
|
|
1089
|
+
const result: StatusPageMonitorRuleSyncResult =
|
|
1090
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
1091
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
expect(result).toEqual({
|
|
1095
|
+
monitorIdsAdded: [],
|
|
1096
|
+
statusPageResourceIdsRemoved: [],
|
|
1097
|
+
statusPageResourceIdsUpdated: [],
|
|
1098
|
+
monitorIdsReleased: [],
|
|
1099
|
+
});
|
|
1100
|
+
expect(resourceCreateSpy).not.toHaveBeenCalled();
|
|
1101
|
+
expect(resourceDeleteSpy).not.toHaveBeenCalled();
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it("does nothing for a rule with no criteria, rather than adding the whole project", async () => {
|
|
1105
|
+
ruleFindOneByIdSpy.mockResolvedValue(fakeRule({}));
|
|
1106
|
+
|
|
1107
|
+
const result: StatusPageMonitorRuleSyncResult =
|
|
1108
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
1109
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
expect(monitorFindBySpy).not.toHaveBeenCalled();
|
|
1113
|
+
expect(result.monitorIdsAdded).toEqual([]);
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
/*
|
|
1117
|
+
* A rule can lose its last criterion without anyone editing it: its only
|
|
1118
|
+
* criterion was a label, and deleting that label cascaded the join row away.
|
|
1119
|
+
* The rule then claims nothing, and what it had added has to go — otherwise
|
|
1120
|
+
* the page keeps monitors that no surviving configuration explains.
|
|
1121
|
+
*/
|
|
1122
|
+
it("removes what it owns once it has been left with no criteria at all", async () => {
|
|
1123
|
+
ruleFindOneByIdSpy.mockResolvedValue(fakeRule({}));
|
|
1124
|
+
resourceFindBySpy.mockResolvedValue([
|
|
1125
|
+
fakeResource({
|
|
1126
|
+
id: RESOURCE_A_ID,
|
|
1127
|
+
monitorId: MONITOR_A_ID,
|
|
1128
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1129
|
+
}),
|
|
1130
|
+
]);
|
|
1131
|
+
|
|
1132
|
+
const result: StatusPageMonitorRuleSyncResult =
|
|
1133
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
1134
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
expect(result.statusPageResourceIdsRemoved).toEqual([
|
|
1138
|
+
RESOURCE_A_ID.toString(),
|
|
1139
|
+
]);
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
it("still spares manual resources when it has been left with no criteria", async () => {
|
|
1143
|
+
ruleFindOneByIdSpy.mockResolvedValue(fakeRule({}));
|
|
1144
|
+
resourceFindBySpy.mockResolvedValue([
|
|
1145
|
+
fakeResource({ id: RESOURCE_A_ID, monitorId: MONITOR_A_ID }),
|
|
1146
|
+
]);
|
|
1147
|
+
|
|
1148
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
1149
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
expect(resourceDeleteSpy).not.toHaveBeenCalled();
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
it("stamps the project and the status page on what it creates", async () => {
|
|
1156
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
1157
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
1158
|
+
);
|
|
1159
|
+
monitorFindBySpy.mockResolvedValue([
|
|
1160
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
1161
|
+
]);
|
|
1162
|
+
|
|
1163
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
1164
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1165
|
+
});
|
|
1166
|
+
|
|
1167
|
+
const resource: StatusPageResource = createdResource(resourceCreateSpy);
|
|
1168
|
+
|
|
1169
|
+
expect(resource.projectId).toEqual(PROJECT_ID);
|
|
1170
|
+
expect(resource.statusPageId).toEqual(STATUS_PAGE_ID);
|
|
1171
|
+
expect(resource.monitorId).toEqual(MONITOR_A_ID);
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
it("defaults the display options to on when the rule sets none", async () => {
|
|
1175
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
1176
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
1177
|
+
);
|
|
1178
|
+
monitorFindBySpy.mockResolvedValue([
|
|
1179
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
1180
|
+
]);
|
|
1181
|
+
|
|
1182
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
1183
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
const resource: StatusPageResource = createdResource(resourceCreateSpy);
|
|
1187
|
+
|
|
1188
|
+
expect(resource.showCurrentStatus).toBe(true);
|
|
1189
|
+
expect(resource.showUptimePercent).toBe(true);
|
|
1190
|
+
expect(resource.showStatusHistoryChart).toBe(true);
|
|
1191
|
+
// Left unset rather than written as null, so the column default applies.
|
|
1192
|
+
expect(resource.uptimePercentPrecision).toBeUndefined();
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
/*
|
|
1196
|
+
* The label filter is the one criterion the database can evaluate, so it is
|
|
1197
|
+
* pushed into the query. A pattern-only rule must not send an empty `labels`
|
|
1198
|
+
* key, which would ask for monitors carrying no labels at all.
|
|
1199
|
+
*/
|
|
1200
|
+
it("pushes the label filter into the monitor query when the rule names labels", async () => {
|
|
1201
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
1202
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID, LABEL_TIER1_ID] }),
|
|
1203
|
+
);
|
|
1204
|
+
monitorFindBySpy.mockResolvedValue([]);
|
|
1205
|
+
|
|
1206
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
1207
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1208
|
+
});
|
|
1209
|
+
|
|
1210
|
+
const call: { query: { labels?: Array<ObjectID> } } = monitorFindBySpy.mock
|
|
1211
|
+
.calls[0]![0] as { query: { labels?: Array<ObjectID> } };
|
|
1212
|
+
|
|
1213
|
+
expect(
|
|
1214
|
+
(call.query.labels || []).map((id: ObjectID) => {
|
|
1215
|
+
return id.toString();
|
|
1216
|
+
}),
|
|
1217
|
+
).toEqual([LABEL_PRODUCTION_ID.toString(), LABEL_TIER1_ID.toString()]);
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
it("sends no labels key at all for a pattern-only rule", async () => {
|
|
1221
|
+
ruleFindOneByIdSpy.mockResolvedValue(
|
|
1222
|
+
fakeRule({ monitorNamePattern: "^api" }),
|
|
1223
|
+
);
|
|
1224
|
+
monitorFindBySpy.mockResolvedValue([]);
|
|
1225
|
+
|
|
1226
|
+
await StatusPageMonitorRuleEngineService.syncResourcesForRule({
|
|
1227
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
const call: { query: Record<string, unknown> } = monitorFindBySpy.mock
|
|
1231
|
+
.calls[0]![0] as { query: Record<string, unknown> };
|
|
1232
|
+
|
|
1233
|
+
expect("labels" in call.query).toBe(false);
|
|
1234
|
+
});
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
describe("StatusPageMonitorRuleEngineService.syncRulesForMonitor", () => {
|
|
1238
|
+
let monitorFindOneByIdSpy: jest.SpyInstance;
|
|
1239
|
+
let ruleFindBySpy: jest.SpyInstance;
|
|
1240
|
+
let resourceFindBySpy: jest.SpyInstance;
|
|
1241
|
+
let resourceCreateSpy: jest.SpyInstance;
|
|
1242
|
+
let resourceDeleteSpy: jest.SpyInstance;
|
|
1243
|
+
let resourceUpdateSpy: jest.SpyInstance;
|
|
1244
|
+
|
|
1245
|
+
beforeEach(() => {
|
|
1246
|
+
monitorFindOneByIdSpy = jest.spyOn(MonitorService, "findOneById");
|
|
1247
|
+
ruleFindBySpy = jest.spyOn(StatusPageMonitorRuleService, "findBy");
|
|
1248
|
+
resourceFindBySpy = jest
|
|
1249
|
+
.spyOn(StatusPageResourceService, "findBy")
|
|
1250
|
+
.mockResolvedValue([]);
|
|
1251
|
+
resourceCreateSpy = jest
|
|
1252
|
+
.spyOn(StatusPageResourceService, "create")
|
|
1253
|
+
.mockResolvedValue(new StatusPageResource());
|
|
1254
|
+
resourceDeleteSpy = jest
|
|
1255
|
+
.spyOn(StatusPageResourceService, "deleteOneById")
|
|
1256
|
+
.mockResolvedValue(1);
|
|
1257
|
+
resourceUpdateSpy = jest
|
|
1258
|
+
.spyOn(StatusPageResourceService, "updateOneById")
|
|
1259
|
+
.mockResolvedValue(1);
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
afterEach(() => {
|
|
1263
|
+
jest.restoreAllMocks();
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
/*
|
|
1267
|
+
* Reconciling belongs to the rule-side path only. Nothing about the rule
|
|
1268
|
+
* moved here, so quietly reverting a per-resource tweak because somebody
|
|
1269
|
+
* relabelled an unrelated monitor would be a surprise with no cause the
|
|
1270
|
+
* user could see.
|
|
1271
|
+
*/
|
|
1272
|
+
it("does not re-home drifted resources - only a rule edit does that", async () => {
|
|
1273
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
1274
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
1275
|
+
);
|
|
1276
|
+
ruleFindBySpy.mockResolvedValue([
|
|
1277
|
+
fakeRule({
|
|
1278
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
1279
|
+
statusPageGroupId: OTHER_GROUP_ID,
|
|
1280
|
+
}),
|
|
1281
|
+
]);
|
|
1282
|
+
resourceFindBySpy.mockResolvedValue([
|
|
1283
|
+
fakeResource({
|
|
1284
|
+
id: RESOURCE_A_ID,
|
|
1285
|
+
monitorId: MONITOR_A_ID,
|
|
1286
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1287
|
+
statusPageGroupId: GROUP_ID,
|
|
1288
|
+
}),
|
|
1289
|
+
]);
|
|
1290
|
+
|
|
1291
|
+
await StatusPageMonitorRuleEngineService.syncRulesForMonitor({
|
|
1292
|
+
monitorId: MONITOR_A_ID,
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
expect(resourceUpdateSpy).not.toHaveBeenCalled();
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
/*
|
|
1299
|
+
* Two rules on one page can match the same monitor; only the first creates
|
|
1300
|
+
* the resource, so the page shows it once. When the monitor stops matching
|
|
1301
|
+
* the OWNING rule but still matches the other, the monitor must stay on the
|
|
1302
|
+
* page — and must do so whichever order the rules come back in, because
|
|
1303
|
+
* findBy gives no ordering guarantee.
|
|
1304
|
+
*
|
|
1305
|
+
* Interleaving adds and removes made this order-dependent: evaluate the
|
|
1306
|
+
* still-matching rule first and it skips the add (the monitor is still on
|
|
1307
|
+
* the page), then the owning rule deletes and the monitor silently
|
|
1308
|
+
* disappears from a public status page.
|
|
1309
|
+
*/
|
|
1310
|
+
it("keeps a monitor on the page when one rule releases it and another still claims it", async () => {
|
|
1311
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
1312
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_TIER1_ID] }),
|
|
1313
|
+
);
|
|
1314
|
+
|
|
1315
|
+
// RULE_ID owns the resource but no longer matches. OTHER_RULE_ID matches.
|
|
1316
|
+
ruleFindBySpy.mockResolvedValue([
|
|
1317
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
1318
|
+
fakeRule({ id: OTHER_RULE_ID, monitorLabels: [LABEL_TIER1_ID] }),
|
|
1319
|
+
]);
|
|
1320
|
+
|
|
1321
|
+
/*
|
|
1322
|
+
* The page as each pass reads it: the resource is there until the removal
|
|
1323
|
+
* pass deletes it, and gone afterwards.
|
|
1324
|
+
*/
|
|
1325
|
+
let isOnPage: boolean = true;
|
|
1326
|
+
resourceFindBySpy.mockImplementation(() => {
|
|
1327
|
+
return Promise.resolve(
|
|
1328
|
+
isOnPage
|
|
1329
|
+
? [
|
|
1330
|
+
fakeResource({
|
|
1331
|
+
id: RESOURCE_A_ID,
|
|
1332
|
+
monitorId: MONITOR_A_ID,
|
|
1333
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1334
|
+
}),
|
|
1335
|
+
]
|
|
1336
|
+
: [],
|
|
1337
|
+
);
|
|
1338
|
+
});
|
|
1339
|
+
resourceDeleteSpy.mockImplementation(() => {
|
|
1340
|
+
isOnPage = false;
|
|
1341
|
+
return Promise.resolve(1);
|
|
1342
|
+
});
|
|
1343
|
+
|
|
1344
|
+
await StatusPageMonitorRuleEngineService.syncRulesForMonitor({
|
|
1345
|
+
monitorId: MONITOR_A_ID,
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
expect(resourceDeleteSpy).toHaveBeenCalledTimes(1);
|
|
1349
|
+
expect(resourceCreateSpy).toHaveBeenCalledTimes(1);
|
|
1350
|
+
expect(createdResource(resourceCreateSpy).statusPageMonitorRuleId).toEqual(
|
|
1351
|
+
OTHER_RULE_ID,
|
|
1352
|
+
);
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
it("holds that guarantee whichever order the rules come back in", async () => {
|
|
1356
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
1357
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_TIER1_ID] }),
|
|
1358
|
+
);
|
|
1359
|
+
|
|
1360
|
+
// Same as above, with the still-matching rule evaluated FIRST.
|
|
1361
|
+
ruleFindBySpy.mockResolvedValue([
|
|
1362
|
+
fakeRule({ id: OTHER_RULE_ID, monitorLabels: [LABEL_TIER1_ID] }),
|
|
1363
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
1364
|
+
]);
|
|
1365
|
+
|
|
1366
|
+
let isOnPage: boolean = true;
|
|
1367
|
+
resourceFindBySpy.mockImplementation(() => {
|
|
1368
|
+
return Promise.resolve(
|
|
1369
|
+
isOnPage
|
|
1370
|
+
? [
|
|
1371
|
+
fakeResource({
|
|
1372
|
+
id: RESOURCE_A_ID,
|
|
1373
|
+
monitorId: MONITOR_A_ID,
|
|
1374
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1375
|
+
}),
|
|
1376
|
+
]
|
|
1377
|
+
: [],
|
|
1378
|
+
);
|
|
1379
|
+
});
|
|
1380
|
+
resourceDeleteSpy.mockImplementation(() => {
|
|
1381
|
+
isOnPage = false;
|
|
1382
|
+
return Promise.resolve(1);
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
await StatusPageMonitorRuleEngineService.syncRulesForMonitor({
|
|
1386
|
+
monitorId: MONITOR_A_ID,
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
expect(resourceCreateSpy).toHaveBeenCalledTimes(1);
|
|
1390
|
+
expect(createdResource(resourceCreateSpy).statusPageMonitorRuleId).toEqual(
|
|
1391
|
+
OTHER_RULE_ID,
|
|
1392
|
+
);
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
it("adds the monitor to every status page whose rule it now matches", async () => {
|
|
1396
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
1397
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
1398
|
+
);
|
|
1399
|
+
ruleFindBySpy.mockResolvedValue([
|
|
1400
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
1401
|
+
fakeRule({
|
|
1402
|
+
id: OTHER_RULE_ID,
|
|
1403
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
1404
|
+
statusPageGroupId: GROUP_ID,
|
|
1405
|
+
}),
|
|
1406
|
+
]);
|
|
1407
|
+
|
|
1408
|
+
const results: Array<StatusPageMonitorRuleSyncResult> =
|
|
1409
|
+
await StatusPageMonitorRuleEngineService.syncRulesForMonitor({
|
|
1410
|
+
monitorId: MONITOR_A_ID,
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
expect(resourceCreateSpy).toHaveBeenCalledTimes(2);
|
|
1414
|
+
expect(results).toHaveLength(2);
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
it("only considers enabled rules", async () => {
|
|
1418
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
1419
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
1420
|
+
);
|
|
1421
|
+
ruleFindBySpy.mockResolvedValue([]);
|
|
1422
|
+
|
|
1423
|
+
await StatusPageMonitorRuleEngineService.syncRulesForMonitor({
|
|
1424
|
+
monitorId: MONITOR_A_ID,
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
const call: { query: { isEnabled: boolean } } = ruleFindBySpy.mock
|
|
1428
|
+
.calls[0]![0] as { query: { isEnabled: boolean } };
|
|
1429
|
+
|
|
1430
|
+
expect(call.query.isEnabled).toBe(true);
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
it("removes the monitor from a page once it stops matching that page's rule", async () => {
|
|
1434
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
1435
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_STAGING_ID] }),
|
|
1436
|
+
);
|
|
1437
|
+
ruleFindBySpy.mockResolvedValue([
|
|
1438
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
1439
|
+
]);
|
|
1440
|
+
resourceFindBySpy.mockResolvedValue([
|
|
1441
|
+
fakeResource({
|
|
1442
|
+
id: RESOURCE_A_ID,
|
|
1443
|
+
monitorId: MONITOR_A_ID,
|
|
1444
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1445
|
+
}),
|
|
1446
|
+
]);
|
|
1447
|
+
|
|
1448
|
+
const results: Array<StatusPageMonitorRuleSyncResult> =
|
|
1449
|
+
await StatusPageMonitorRuleEngineService.syncRulesForMonitor({
|
|
1450
|
+
monitorId: MONITOR_A_ID,
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
expect(results[0]!.statusPageResourceIdsRemoved).toEqual([
|
|
1454
|
+
RESOURCE_A_ID.toString(),
|
|
1455
|
+
]);
|
|
1456
|
+
});
|
|
1457
|
+
|
|
1458
|
+
/*
|
|
1459
|
+
* The monitor-side path knows about one monitor. Every other monitor the
|
|
1460
|
+
* rule owns is somebody else's business and must survive untouched.
|
|
1461
|
+
*/
|
|
1462
|
+
it("leaves the rule's other monitors alone", async () => {
|
|
1463
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
1464
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_STAGING_ID] }),
|
|
1465
|
+
);
|
|
1466
|
+
ruleFindBySpy.mockResolvedValue([
|
|
1467
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
1468
|
+
]);
|
|
1469
|
+
resourceFindBySpy.mockResolvedValue([
|
|
1470
|
+
fakeResource({
|
|
1471
|
+
id: RESOURCE_A_ID,
|
|
1472
|
+
monitorId: MONITOR_A_ID,
|
|
1473
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1474
|
+
}),
|
|
1475
|
+
fakeResource({
|
|
1476
|
+
id: RESOURCE_B_ID,
|
|
1477
|
+
monitorId: MONITOR_B_ID,
|
|
1478
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1479
|
+
}),
|
|
1480
|
+
]);
|
|
1481
|
+
|
|
1482
|
+
await StatusPageMonitorRuleEngineService.syncRulesForMonitor({
|
|
1483
|
+
monitorId: MONITOR_A_ID,
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
expect(resourceDeleteSpy).toHaveBeenCalledTimes(1);
|
|
1487
|
+
const call: { id: ObjectID } = resourceDeleteSpy.mock.calls[0]![0] as {
|
|
1488
|
+
id: ObjectID;
|
|
1489
|
+
};
|
|
1490
|
+
expect(call.id).toEqual(RESOURCE_A_ID);
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
it("still refuses to remove the monitor's manual resource", async () => {
|
|
1494
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
1495
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_STAGING_ID] }),
|
|
1496
|
+
);
|
|
1497
|
+
ruleFindBySpy.mockResolvedValue([
|
|
1498
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
1499
|
+
]);
|
|
1500
|
+
resourceFindBySpy.mockResolvedValue([
|
|
1501
|
+
fakeResource({ id: RESOURCE_A_ID, monitorId: MONITOR_A_ID }),
|
|
1502
|
+
]);
|
|
1503
|
+
|
|
1504
|
+
await StatusPageMonitorRuleEngineService.syncRulesForMonitor({
|
|
1505
|
+
monitorId: MONITOR_A_ID,
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
expect(resourceDeleteSpy).not.toHaveBeenCalled();
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
it("reports nothing for rules that were already in the right state", async () => {
|
|
1512
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
1513
|
+
fakeMonitor(MONITOR_C_ID, { labels: [LABEL_STAGING_ID] }),
|
|
1514
|
+
);
|
|
1515
|
+
ruleFindBySpy.mockResolvedValue([
|
|
1516
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
1517
|
+
]);
|
|
1518
|
+
|
|
1519
|
+
const results: Array<StatusPageMonitorRuleSyncResult> =
|
|
1520
|
+
await StatusPageMonitorRuleEngineService.syncRulesForMonitor({
|
|
1521
|
+
monitorId: MONITOR_C_ID,
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
expect(results).toEqual([]);
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
it("keeps going after one rule fails - the monitor has already changed", async () => {
|
|
1528
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
1529
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
1530
|
+
);
|
|
1531
|
+
ruleFindBySpy.mockResolvedValue([
|
|
1532
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
1533
|
+
fakeRule({ id: OTHER_RULE_ID, monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
1534
|
+
]);
|
|
1535
|
+
resourceCreateSpy
|
|
1536
|
+
.mockRejectedValueOnce(new Error("db down"))
|
|
1537
|
+
.mockResolvedValueOnce(new StatusPageResource());
|
|
1538
|
+
|
|
1539
|
+
const results: Array<StatusPageMonitorRuleSyncResult> =
|
|
1540
|
+
await StatusPageMonitorRuleEngineService.syncRulesForMonitor({
|
|
1541
|
+
monitorId: MONITOR_A_ID,
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
expect(results).toHaveLength(1);
|
|
1545
|
+
expect(results[0]!.monitorIdsAdded).toEqual([MONITOR_A_ID.toString()]);
|
|
1546
|
+
});
|
|
1547
|
+
|
|
1548
|
+
it("does nothing for a monitor that no longer exists", async () => {
|
|
1549
|
+
monitorFindOneByIdSpy.mockResolvedValue(null);
|
|
1550
|
+
|
|
1551
|
+
const results: Array<StatusPageMonitorRuleSyncResult> =
|
|
1552
|
+
await StatusPageMonitorRuleEngineService.syncRulesForMonitor({
|
|
1553
|
+
monitorId: MONITOR_A_ID,
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
expect(results).toEqual([]);
|
|
1557
|
+
expect(ruleFindBySpy).not.toHaveBeenCalled();
|
|
1558
|
+
});
|
|
1559
|
+
|
|
1560
|
+
it("falls back to the caller's project id when the monitor row does not carry one", async () => {
|
|
1561
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
1562
|
+
fakeMonitor(MONITOR_A_ID, {
|
|
1563
|
+
projectId: null,
|
|
1564
|
+
labels: [LABEL_PRODUCTION_ID],
|
|
1565
|
+
}),
|
|
1566
|
+
);
|
|
1567
|
+
ruleFindBySpy.mockResolvedValue([]);
|
|
1568
|
+
|
|
1569
|
+
await StatusPageMonitorRuleEngineService.syncRulesForMonitor({
|
|
1570
|
+
monitorId: MONITOR_A_ID,
|
|
1571
|
+
projectId: PROJECT_ID,
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
const call: { query: { projectId: ObjectID } } = ruleFindBySpy.mock
|
|
1575
|
+
.calls[0]![0] as { query: { projectId: ObjectID } };
|
|
1576
|
+
|
|
1577
|
+
expect(call.query.projectId).toEqual(PROJECT_ID);
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
/*
|
|
1581
|
+
* Two rules on one page both wanting the same monitor is ordinary
|
|
1582
|
+
* configuration (a broad rule and a narrow one). The page must list it once.
|
|
1583
|
+
*/
|
|
1584
|
+
it("gives a monitor exactly one resource when two rules on the same page match it", async () => {
|
|
1585
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
1586
|
+
fakeMonitor(MONITOR_A_ID, {
|
|
1587
|
+
labels: [LABEL_PRODUCTION_ID, LABEL_TIER1_ID],
|
|
1588
|
+
}),
|
|
1589
|
+
);
|
|
1590
|
+
ruleFindBySpy.mockResolvedValue([
|
|
1591
|
+
fakeRule({ monitorLabels: [LABEL_PRODUCTION_ID] }),
|
|
1592
|
+
fakeRule({ id: OTHER_RULE_ID, monitorLabels: [LABEL_TIER1_ID] }),
|
|
1593
|
+
]);
|
|
1594
|
+
|
|
1595
|
+
// Once the first rule creates the resource, the page has it.
|
|
1596
|
+
let created: Array<StatusPageResource> = [];
|
|
1597
|
+
resourceFindBySpy.mockImplementation(() => {
|
|
1598
|
+
return Promise.resolve(created);
|
|
1599
|
+
});
|
|
1600
|
+
resourceCreateSpy.mockImplementation(() => {
|
|
1601
|
+
created = [
|
|
1602
|
+
fakeResource({
|
|
1603
|
+
id: RESOURCE_A_ID,
|
|
1604
|
+
monitorId: MONITOR_A_ID,
|
|
1605
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1606
|
+
}),
|
|
1607
|
+
];
|
|
1608
|
+
return Promise.resolve(new StatusPageResource());
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
await StatusPageMonitorRuleEngineService.syncRulesForMonitor({
|
|
1612
|
+
monitorId: MONITOR_A_ID,
|
|
1613
|
+
});
|
|
1614
|
+
|
|
1615
|
+
expect(resourceCreateSpy).toHaveBeenCalledTimes(1);
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
it("skips a rule row that came back without a status page", async () => {
|
|
1619
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
1620
|
+
fakeMonitor(MONITOR_A_ID, { labels: [LABEL_PRODUCTION_ID] }),
|
|
1621
|
+
);
|
|
1622
|
+
ruleFindBySpy.mockResolvedValue([
|
|
1623
|
+
fakeRule({
|
|
1624
|
+
statusPageId: null,
|
|
1625
|
+
monitorLabels: [LABEL_PRODUCTION_ID],
|
|
1626
|
+
}),
|
|
1627
|
+
]);
|
|
1628
|
+
|
|
1629
|
+
const results: Array<StatusPageMonitorRuleSyncResult> =
|
|
1630
|
+
await StatusPageMonitorRuleEngineService.syncRulesForMonitor({
|
|
1631
|
+
monitorId: MONITOR_A_ID,
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
expect(results).toEqual([]);
|
|
1635
|
+
expect(resourceCreateSpy).not.toHaveBeenCalled();
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
it("does nothing when no project id can be resolved at all", async () => {
|
|
1639
|
+
monitorFindOneByIdSpy.mockResolvedValue(
|
|
1640
|
+
fakeMonitor(MONITOR_A_ID, { projectId: null }),
|
|
1641
|
+
);
|
|
1642
|
+
|
|
1643
|
+
const results: Array<StatusPageMonitorRuleSyncResult> =
|
|
1644
|
+
await StatusPageMonitorRuleEngineService.syncRulesForMonitor({
|
|
1645
|
+
monitorId: MONITOR_A_ID,
|
|
1646
|
+
});
|
|
1647
|
+
|
|
1648
|
+
expect(results).toEqual([]);
|
|
1649
|
+
expect(ruleFindBySpy).not.toHaveBeenCalled();
|
|
1650
|
+
});
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
describe("StatusPageMonitorRuleEngineService.removeResourcesAddedByRule", () => {
|
|
1654
|
+
let resourceFindBySpy: jest.SpyInstance;
|
|
1655
|
+
let resourceDeleteSpy: jest.SpyInstance;
|
|
1656
|
+
|
|
1657
|
+
beforeEach(() => {
|
|
1658
|
+
resourceFindBySpy = jest.spyOn(StatusPageResourceService, "findBy");
|
|
1659
|
+
resourceDeleteSpy = jest
|
|
1660
|
+
.spyOn(StatusPageResourceService, "deleteOneById")
|
|
1661
|
+
.mockResolvedValue(1);
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
afterEach(() => {
|
|
1665
|
+
jest.restoreAllMocks();
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1668
|
+
it("deletes every resource the rule added", async () => {
|
|
1669
|
+
resourceFindBySpy.mockResolvedValue([
|
|
1670
|
+
fakeResource({
|
|
1671
|
+
id: RESOURCE_A_ID,
|
|
1672
|
+
monitorId: MONITOR_A_ID,
|
|
1673
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1674
|
+
}),
|
|
1675
|
+
fakeResource({
|
|
1676
|
+
id: RESOURCE_B_ID,
|
|
1677
|
+
monitorId: MONITOR_B_ID,
|
|
1678
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1679
|
+
}),
|
|
1680
|
+
]);
|
|
1681
|
+
|
|
1682
|
+
const removed: Array<string> =
|
|
1683
|
+
await StatusPageMonitorRuleEngineService.removeResourcesAddedByRule({
|
|
1684
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
expect(resourceDeleteSpy).toHaveBeenCalledTimes(2);
|
|
1688
|
+
expect(removed.sort()).toEqual(
|
|
1689
|
+
[RESOURCE_A_ID.toString(), RESOURCE_B_ID.toString()].sort(),
|
|
1690
|
+
);
|
|
1691
|
+
});
|
|
1692
|
+
|
|
1693
|
+
it("asks only for resources stamped with this rule, so manual ones are never in range", async () => {
|
|
1694
|
+
resourceFindBySpy.mockResolvedValue([]);
|
|
1695
|
+
|
|
1696
|
+
await StatusPageMonitorRuleEngineService.removeResourcesAddedByRule({
|
|
1697
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
const call: { query: { statusPageMonitorRuleId: ObjectID } } =
|
|
1701
|
+
resourceFindBySpy.mock.calls[0]![0] as {
|
|
1702
|
+
query: { statusPageMonitorRuleId: ObjectID };
|
|
1703
|
+
};
|
|
1704
|
+
|
|
1705
|
+
expect(call.query.statusPageMonitorRuleId).toEqual(RULE_ID);
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
it("returns an empty list when the rule added nothing", async () => {
|
|
1709
|
+
resourceFindBySpy.mockResolvedValue([]);
|
|
1710
|
+
|
|
1711
|
+
const removed: Array<string> =
|
|
1712
|
+
await StatusPageMonitorRuleEngineService.removeResourcesAddedByRule({
|
|
1713
|
+
statusPageMonitorRuleId: RULE_ID,
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1716
|
+
expect(removed).toEqual([]);
|
|
1717
|
+
expect(resourceDeleteSpy).not.toHaveBeenCalled();
|
|
1718
|
+
});
|
|
1719
|
+
});
|