@oneuptime/common 11.6.0 → 11.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.ts +41 -24
- package/Models/AnalyticsModels/Index.ts +4 -0
- package/Models/AnalyticsModels/KubernetesCostAllocation.ts +842 -0
- package/Models/AnalyticsModels/MetricBaselineHourly.ts +2 -0
- package/Models/AnalyticsModels/MetricItemAggMV1m.ts +5 -2
- package/Models/AnalyticsModels/MetricItemAggMV1mByContainer.ts +5 -2
- package/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.ts +5 -2
- package/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +5 -2
- package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +5 -2
- package/Models/AnalyticsModels/NetworkFlow.ts +54 -0
- package/Models/AnalyticsModels/SloHistory.ts +240 -0
- package/Models/DatabaseModels/Index.ts +20 -0
- package/Models/DatabaseModels/NetworkDevice.ts +453 -6
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +7 -6
- package/Models/DatabaseModels/NetworkEndpoint.ts +753 -0
- package/Models/DatabaseModels/NetworkSite.ts +1155 -0
- package/Models/DatabaseModels/NetworkSiteAssignmentRule.ts +517 -0
- package/Models/DatabaseModels/NetworkSiteLink.ts +611 -0
- package/Models/DatabaseModels/NetworkSiteStatusTimeline.ts +520 -0
- package/Models/DatabaseModels/NetworkSiteType.ts +499 -0
- package/Models/DatabaseModels/ServiceLevelObjective.ts +1106 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveBurnRateRule.ts +762 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveOwnerTeam.ts +444 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveOwnerUser.ts +443 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784757142154-AddNetworkSiteHierarchyAndEndpoints.ts +359 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784897317860-AddNetworkSiteAlerting.ts +48 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784970388777-AddNetworkDevicePollingColumns.ts +65 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784986826214-AddNetworkSiteTypeTable.ts +88 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784987015619-AddServiceLevelObjective.ts +391 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +10 -0
- package/Server/Services/AnalyticsDatabaseService.ts +38 -10
- package/Server/Services/Index.ts +24 -0
- package/Server/Services/KubernetesCostAllocationService.ts +14 -0
- package/Server/Services/MetricService.ts +21 -2
- package/Server/Services/MonitorService.ts +22 -0
- package/Server/Services/NetworkDeviceService.ts +438 -2
- package/Server/Services/NetworkEndpointService.ts +414 -0
- package/Server/Services/NetworkSiteAssignmentRuleService.ts +98 -0
- package/Server/Services/NetworkSiteLinkService.ts +10 -0
- package/Server/Services/NetworkSiteService.ts +1254 -0
- package/Server/Services/NetworkSiteStatusTimelineService.ts +10 -0
- package/Server/Services/NetworkSiteTypeService.ts +10 -0
- package/Server/Services/ProjectService.ts +122 -2
- package/Server/Services/ServiceLevelObjectiveBurnRateRuleService.ts +457 -0
- package/Server/Services/ServiceLevelObjectiveOwnerTeamService.ts +10 -0
- package/Server/Services/ServiceLevelObjectiveOwnerUserService.ts +10 -0
- package/Server/Services/ServiceLevelObjectiveService.ts +677 -0
- package/Server/Services/SloHistoryService.ts +101 -0
- package/Server/Services/UserNotificationSettingService.ts +37 -0
- package/Server/Types/AnalyticsDatabase/AggregateBy.ts +9 -0
- package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +15 -0
- package/Server/Utils/AnalyticsDatabase/Statement.ts +39 -3
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +128 -52
- package/Server/Utils/Memory.ts +9 -1
- package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +190 -12
- package/Server/Utils/Monitor/Data/OuiVendors.json +6920 -0
- package/Server/Utils/Monitor/DnsResolutionCache.ts +129 -0
- package/Server/Utils/Monitor/MonitorResource.ts +4 -30
- package/Server/Utils/Monitor/NetworkDeviceHydrationUtil.ts +214 -39
- package/Server/Utils/Monitor/NetworkDeviceMetricUtil.ts +343 -0
- package/Server/Utils/Monitor/NetworkDeviceWalkUtil.ts +251 -0
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +176 -142
- package/Server/Utils/Monitor/OuiLookupUtil.ts +49 -0
- package/Server/Utils/Monitor/SnmpInterfaceRateUtil.ts +17 -13
- package/Server/Utils/WhatsAppTemplateUtil.ts +7 -3
- package/Tests/Models/AnalyticsModels/AnalyticsBaseModel.test.ts +122 -0
- package/Tests/Models/AnalyticsModels/KubernetesCostAllocation.test.ts +135 -0
- package/Tests/Models/NetworkSiteHierarchy.test.ts +134 -0
- package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +239 -0
- package/Tests/Server/Services/KubernetesCostAllocationAggregate.test.ts +338 -0
- package/Tests/Server/Services/MetricServiceAggregate.test.ts +142 -0
- package/Tests/Server/Services/NetworkDeviceSiteAssignment.test.ts +484 -0
- package/Tests/Server/Services/NetworkEndpointServiceUpsert.test.ts +686 -0
- package/Tests/Server/Services/NetworkSiteAssignmentRuleService.test.ts +185 -0
- package/Tests/Server/Services/NetworkSiteService.test.ts +1354 -0
- package/Tests/Server/Services/ServiceLevelObjectiveBurnRateRuleService.test.ts +1313 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +1988 -0
- package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +164 -3
- package/Tests/Server/Utils/AnalyticsDatabase/Statement.test.ts +22 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +503 -0
- package/Tests/Server/Utils/Encryption.test.ts +70 -0
- package/Tests/Server/Utils/FileAttachmentMarkdownUtil.test.ts +225 -0
- package/Tests/Server/Utils/JsonWebToken.test.ts +175 -0
- package/Tests/Server/Utils/LocalFile.test.ts +258 -0
- package/Tests/Server/Utils/Memory.test.ts +213 -0
- package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +30 -0
- package/Tests/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.test.ts +385 -0
- package/Tests/Server/Utils/Monitor/DnsResolutionCache.test.ts +194 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceHydrationUtil.test.ts +155 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +600 -0
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +151 -124
- package/Tests/Server/Utils/Monitor/SnmpInterfaceRateUtil.test.ts +496 -0
- package/Tests/Server/Utils/MonitorCriteriaDataExtractor.test.ts +175 -0
- package/Tests/Server/Utils/MonitorCriteriaMessageFormatter.test.ts +282 -0
- package/Tests/Server/Utils/OuiLookupUtil.test.ts +59 -0
- package/Tests/Server/Utils/SeriesResourceLabels.test.ts +117 -0
- package/Tests/Server/Utils/StackTraceParser.test.ts +370 -0
- package/Tests/Server/Utils/Stream.test.ts +87 -0
- package/Tests/Server/Utils/ValidateGlobalProviderProjectTeams.test.ts +221 -0
- package/Tests/Server/Utils/WhatsAppTemplateUtil.test.ts +199 -0
- package/Tests/Types/Database/CompareOperatorWireSerialization.test.ts +33 -0
- package/Tests/Types/Email.test.ts +198 -0
- package/Tests/Types/HashCode.test.ts +41 -0
- package/Tests/Types/Kubernetes/KubernetesObjectParser.test.ts +177 -0
- package/Tests/Types/Log/LogQueryParser.test.ts +186 -0
- package/Tests/Types/Log/LogQueryToFilter.test.ts +111 -0
- package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +141 -1
- package/Tests/Types/Monitor/MonitorType.test.ts +59 -1
- package/Tests/Types/Monitor/NetworkDeviceAlertPack.test.ts +97 -0
- package/Tests/Types/Monitor/SnmpEndpointDiscovery.test.ts +151 -0
- package/Tests/Types/Monitor/SnmpVendorTemplate.test.ts +110 -6
- package/Tests/Types/NetworkSite/DefaultNetworkSiteType.test.ts +129 -0
- package/Tests/Types/SerializableObjectDictionary.test.ts +166 -0
- package/Tests/Types/Time/RangeStartAndEndDateTime.test.ts +98 -0
- package/Tests/Types/WebsiteRequest.test.ts +230 -0
- package/Tests/UI/Components/EntityFilterRoundTrip.test.tsx +103 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableUrlState.test.tsx +612 -0
- package/Tests/UI/Components/ModelTable/FilterDataToQuery.test.ts +275 -0
- package/Tests/UI/Utils/LogExport.test.ts +159 -0
- package/Tests/UI/Utils/Navigation.test.ts +183 -0
- package/Tests/UI/Utils/TableFilterUrlState.test.ts +539 -0
- package/Tests/UI/Utils/TableViewUrlState.test.ts +272 -0
- package/Tests/Utils/Base64.test.ts +51 -0
- package/Tests/Utils/Boolean.test.ts +76 -0
- package/Tests/Utils/Crypto.test.ts +48 -0
- package/Tests/Utils/Dashboard/DashboardSloComponent.test.ts +259 -0
- package/Tests/Utils/Dashboard/ModelQueryVariableInterpolation.test.ts +162 -0
- package/Tests/Utils/Dashboard/VariableInterpolation.test.ts +197 -0
- package/Tests/Utils/Enum.test.ts +54 -0
- package/Tests/Utils/IpCanonicalUtil.test.ts +85 -0
- package/Tests/Utils/Metrics/MetricSeriesFingerprint.test.ts +160 -0
- package/Tests/Utils/Monitor/EndpointAttachmentUtil.test.ts +836 -0
- package/Tests/Utils/Monitor/MonitorMetricType.test.ts +218 -0
- package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +447 -0
- package/Tests/Utils/NetworkSite/CidrMatchUtil.test.ts +440 -0
- package/Tests/Utils/NetworkSite/MaterializedPathUtil.test.ts +205 -0
- package/Tests/Utils/NetworkSite/SiteStatusRollupUtil.test.ts +221 -0
- package/Tests/Utils/NetworkSite/SiteUptimeUtil.test.ts +283 -0
- package/Tests/Utils/ObjectUtil.test.ts +30 -0
- package/Tests/Utils/Slo/SloStatusColor.test.ts +89 -0
- package/Tests/Utils/Slo/SloUtil.test.ts +1980 -0
- package/Tests/Utils/Slo/SloWidgetFormat.test.ts +605 -0
- package/Tests/Utils/TechStack.test.ts +50 -0
- package/Tests/Utils/Traces/CriticalPath.test.ts +178 -0
- package/Tests/Utils/ValueFormatter.test.ts +305 -0
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +10 -0
- package/Types/AnalyticsDatabase/TableColumn.ts +23 -0
- package/Types/BaseDatabase/EqualTo.ts +9 -1
- package/Types/BaseDatabase/EqualToOrNull.ts +9 -1
- package/Types/BaseDatabase/NotEqual.ts +9 -1
- package/Types/Dashboard/DashboardComponentType.ts +1 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardSloComponent.ts +42 -0
- package/Types/Dashboard/DashboardTemplates.ts +214 -0
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/Types/Kubernetes/KubernetesCostIngest.ts +79 -0
- package/Types/Monitor/CriteriaFilter.ts +9 -0
- package/Types/Monitor/MonitorStep.ts +5 -8
- package/Types/Monitor/MonitorStepNetworkDeviceMonitor.ts +27 -5
- package/Types/Monitor/MonitorType.ts +15 -5
- package/Types/Monitor/SnmpMonitor/ArpEntry.ts +14 -0
- package/Types/Monitor/SnmpMonitor/FdbEntry.ts +18 -0
- package/Types/Monitor/SnmpMonitor/NetworkDeviceAlertPack.ts +10 -3
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +31 -5
- package/Types/Monitor/SnmpMonitor/SnmpMonitorResponse.ts +12 -0
- package/Types/Monitor/SnmpMonitor/SnmpVendorTemplate.ts +231 -3
- package/Types/NetworkSite/DefaultNetworkSiteType.ts +20 -0
- package/Types/NotificationSetting/NotificationSettingEventType.ts +4 -0
- package/Types/Permission.ts +449 -0
- package/Types/ServiceLevelObjective/SliType.ts +6 -0
- package/Types/ServiceLevelObjective/SloMultiMonitorMode.ts +6 -0
- package/Types/ServiceLevelObjective/SloStatus.ts +9 -0
- package/Types/ServiceLevelObjective/SloWindowType.ts +6 -0
- package/Types/Telemetry/ServiceType.ts +1 -0
- package/UI/Components/Filters/EntityFilter.tsx +16 -0
- package/UI/Components/Filters/FilterViewer.tsx +22 -5
- package/UI/Components/List/List.tsx +8 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +309 -106
- package/UI/Components/ModelTable/FilterDataToQuery.ts +172 -0
- package/UI/Components/Navbar/NavBar.tsx +16 -2
- package/UI/Components/Navbar/NavBarMenuModal.tsx +3 -3
- package/UI/Utils/Navigation.ts +10 -0
- package/UI/Utils/TableFilterUrlState.ts +204 -17
- package/UI/Utils/TableViewUrlState.ts +165 -0
- package/Utils/Dashboard/Components/DashboardSloComponent.ts +114 -0
- package/Utils/Dashboard/Components/Index.ts +7 -0
- package/Utils/IpCanonicalUtil.ts +134 -0
- package/Utils/Monitor/EndpointAttachmentUtil.ts +508 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +157 -5
- package/Utils/NetworkSite/CidrMatchUtil.ts +296 -0
- package/Utils/NetworkSite/MaterializedPathUtil.ts +115 -0
- package/Utils/NetworkSite/SiteStatusRollupUtil.ts +97 -0
- package/Utils/NetworkSite/SiteUptimeUtil.ts +111 -0
- package/Utils/Slo/SloStatusColor.ts +54 -0
- package/Utils/Slo/SloUtil.ts +637 -0
- package/Utils/Slo/SloWidgetFormat.ts +383 -0
- package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js +25 -20
- package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Index.js +4 -0
- package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js +713 -0
- package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js +2 -0
- package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/NetworkFlow.js +44 -0
- package/build/dist/Models/AnalyticsModels/NetworkFlow.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/SloHistory.js +215 -0
- package/build/dist/Models/AnalyticsModels/SloHistory.js.map +1 -0
- package/build/dist/Models/DatabaseModels/Index.js +20 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +458 -6
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +0 -6
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkEndpoint.js +779 -0
- package/build/dist/Models/DatabaseModels/NetworkEndpoint.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkSite.js +1180 -0
- package/build/dist/Models/DatabaseModels/NetworkSite.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkSiteAssignmentRule.js +534 -0
- package/build/dist/Models/DatabaseModels/NetworkSiteAssignmentRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkSiteLink.js +629 -0
- package/build/dist/Models/DatabaseModels/NetworkSiteLink.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js +538 -0
- package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkSiteType.js +522 -0
- package/build/dist/Models/DatabaseModels/NetworkSiteType.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +1124 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveBurnRateRule.js +769 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveBurnRateRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerTeam.js +460 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerTeam.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerUser.js +459 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerUser.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784757142154-AddNetworkSiteHierarchyAndEndpoints.js +130 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784757142154-AddNetworkSiteHierarchyAndEndpoints.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784897317860-AddNetworkSiteAlerting.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784897317860-AddNetworkSiteAlerting.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784970388777-AddNetworkDevicePollingColumns.js +36 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784970388777-AddNetworkDevicePollingColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784986826214-AddNetworkSiteTypeTable.js +49 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784986826214-AddNetworkSiteTypeTable.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784987015619-AddServiceLevelObjective.js +140 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784987015619-AddServiceLevelObjective.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +10 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +32 -7
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +24 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/KubernetesCostAllocationService.js +12 -0
- package/build/dist/Server/Services/KubernetesCostAllocationService.js.map +1 -0
- package/build/dist/Server/Services/MetricService.js +21 -3
- package/build/dist/Server/Services/MetricService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +19 -0
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +351 -1
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/NetworkEndpointService.js +319 -0
- package/build/dist/Server/Services/NetworkEndpointService.js.map +1 -0
- package/build/dist/Server/Services/NetworkSiteAssignmentRuleService.js +90 -0
- package/build/dist/Server/Services/NetworkSiteAssignmentRuleService.js.map +1 -0
- package/build/dist/Server/Services/NetworkSiteLinkService.js +9 -0
- package/build/dist/Server/Services/NetworkSiteLinkService.js.map +1 -0
- package/build/dist/Server/Services/NetworkSiteService.js +1038 -0
- package/build/dist/Server/Services/NetworkSiteService.js.map +1 -0
- package/build/dist/Server/Services/NetworkSiteStatusTimelineService.js +9 -0
- package/build/dist/Server/Services/NetworkSiteStatusTimelineService.js.map +1 -0
- package/build/dist/Server/Services/NetworkSiteTypeService.js +9 -0
- package/build/dist/Server/Services/NetworkSiteTypeService.js.map +1 -0
- package/build/dist/Server/Services/ProjectService.js +98 -2
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveBurnRateRuleService.js +358 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveBurnRateRuleService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerTeamService.js +9 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerTeamService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerUserService.js +9 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerUserService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +582 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -0
- package/build/dist/Server/Services/SloHistoryService.js +77 -0
- package/build/dist/Server/Services/SloHistoryService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationSettingService.js +20 -0
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/AggregateBy.js +9 -0
- package/build/dist/Server/Types/AnalyticsDatabase/AggregateBy.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +15 -0
- package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +31 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +105 -52
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Memory.js +9 -1
- package/build/dist/Server/Utils/Memory.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +132 -10
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Data/OuiVendors.json +6920 -0
- package/build/dist/Server/Utils/Monitor/DnsResolutionCache.js +77 -0
- package/build/dist/Server/Utils/Monitor/DnsResolutionCache.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +21 -41
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js +152 -40
- package/build/dist/Server/Utils/Monitor/NetworkDeviceHydrationUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMetricUtil.js +262 -0
- package/build/dist/Server/Utils/Monitor/NetworkDeviceMetricUtil.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/NetworkDeviceWalkUtil.js +201 -0
- package/build/dist/Server/Utils/Monitor/NetworkDeviceWalkUtil.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +152 -120
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/OuiLookupUtil.js +44 -0
- package/build/dist/Server/Utils/Monitor/OuiLookupUtil.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/SnmpInterfaceRateUtil.js +10 -10
- package/build/dist/Server/Utils/Monitor/SnmpInterfaceRateUtil.js.map +1 -1
- package/build/dist/Server/Utils/WhatsAppTemplateUtil.js +5 -0
- package/build/dist/Server/Utils/WhatsAppTemplateUtil.js.map +1 -1
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +10 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
- package/build/dist/Types/AnalyticsDatabase/TableColumn.js +7 -0
- package/build/dist/Types/AnalyticsDatabase/TableColumn.js.map +1 -1
- package/build/dist/Types/BaseDatabase/EqualTo.js +9 -1
- package/build/dist/Types/BaseDatabase/EqualTo.js.map +1 -1
- package/build/dist/Types/BaseDatabase/EqualToOrNull.js +9 -1
- package/build/dist/Types/BaseDatabase/EqualToOrNull.js.map +1 -1
- package/build/dist/Types/BaseDatabase/NotEqual.js +9 -1
- package/build/dist/Types/BaseDatabase/NotEqual.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/DashboardSloComponent.js +23 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardSloComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js +205 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesCostIngest.js +21 -0
- package/build/dist/Types/Kubernetes/KubernetesCostIngest.js.map +1 -0
- package/build/dist/Types/Monitor/CriteriaFilter.js +1 -0
- package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStep.js +5 -5
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepNetworkDeviceMonitor.js +8 -0
- package/build/dist/Types/Monitor/MonitorStepNetworkDeviceMonitor.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +15 -5
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/Monitor/SnmpMonitor/ArpEntry.js +2 -0
- package/build/dist/Types/Monitor/SnmpMonitor/ArpEntry.js.map +1 -0
- package/build/dist/Types/Monitor/SnmpMonitor/FdbEntry.js +2 -0
- package/build/dist/Types/Monitor/SnmpMonitor/FdbEntry.js.map +1 -0
- package/build/dist/Types/Monitor/SnmpMonitor/NetworkDeviceAlertPack.js +9 -3
- package/build/dist/Types/Monitor/SnmpMonitor/NetworkDeviceAlertPack.js.map +1 -1
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpVendorTemplate.js +217 -3
- package/build/dist/Types/Monitor/SnmpMonitor/SnmpVendorTemplate.js.map +1 -1
- package/build/dist/Types/NetworkSite/DefaultNetworkSiteType.js +21 -0
- package/build/dist/Types/NetworkSite/DefaultNetworkSiteType.js.map +1 -0
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
- package/build/dist/Types/Permission.js +409 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/ServiceLevelObjective/SliType.js +7 -0
- package/build/dist/Types/ServiceLevelObjective/SliType.js.map +1 -0
- package/build/dist/Types/ServiceLevelObjective/SloMultiMonitorMode.js +7 -0
- package/build/dist/Types/ServiceLevelObjective/SloMultiMonitorMode.js.map +1 -0
- package/build/dist/Types/ServiceLevelObjective/SloStatus.js +10 -0
- package/build/dist/Types/ServiceLevelObjective/SloStatus.js.map +1 -0
- package/build/dist/Types/ServiceLevelObjective/SloWindowType.js +7 -0
- package/build/dist/Types/ServiceLevelObjective/SloWindowType.js.map +1 -0
- package/build/dist/Types/Telemetry/ServiceType.js +1 -0
- package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
- package/build/dist/UI/Components/Filters/EntityFilter.js +15 -0
- package/build/dist/UI/Components/Filters/EntityFilter.js.map +1 -1
- package/build/dist/UI/Components/Filters/FilterViewer.js +19 -4
- package/build/dist/UI/Components/Filters/FilterViewer.js.map +1 -1
- package/build/dist/UI/Components/List/List.js +1 -1
- package/build/dist/UI/Components/List/List.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +235 -89
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/FilterDataToQuery.js +96 -0
- package/build/dist/UI/Components/ModelTable/FilterDataToQuery.js.map +1 -0
- package/build/dist/UI/Components/Navbar/NavBar.js +14 -2
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +3 -2
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +9 -0
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/UI/Utils/TableFilterUrlState.js +159 -15
- package/build/dist/UI/Utils/TableFilterUrlState.js.map +1 -1
- package/build/dist/UI/Utils/TableViewUrlState.js +103 -0
- package/build/dist/UI/Utils/TableViewUrlState.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardSloComponent.js +93 -0
- package/build/dist/Utils/Dashboard/Components/DashboardSloComponent.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/IpCanonicalUtil.js +110 -0
- package/build/dist/Utils/IpCanonicalUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/EndpointAttachmentUtil.js +311 -0
- package/build/dist/Utils/Monitor/EndpointAttachmentUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +98 -5
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkSite/CidrMatchUtil.js +222 -0
- package/build/dist/Utils/NetworkSite/CidrMatchUtil.js.map +1 -0
- package/build/dist/Utils/NetworkSite/MaterializedPathUtil.js +89 -0
- package/build/dist/Utils/NetworkSite/MaterializedPathUtil.js.map +1 -0
- package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js +60 -0
- package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js.map +1 -0
- package/build/dist/Utils/NetworkSite/SiteUptimeUtil.js +76 -0
- package/build/dist/Utils/NetworkSite/SiteUptimeUtil.js.map +1 -0
- package/build/dist/Utils/Slo/SloStatusColor.js +18 -0
- package/build/dist/Utils/Slo/SloStatusColor.js.map +1 -0
- package/build/dist/Utils/Slo/SloUtil.js +393 -0
- package/build/dist/Utils/Slo/SloUtil.js.map +1 -0
- package/build/dist/Utils/Slo/SloWidgetFormat.js +174 -0
- package/build/dist/Utils/Slo/SloWidgetFormat.js.map +1 -0
- package/package.json +2 -2
- package/Types/Dashboard/DashboardComponents/Index.ts +0 -7
- package/build/dist/Types/Dashboard/DashboardComponents/Index.js +0 -8
- package/build/dist/Types/Dashboard/DashboardComponents/Index.js.map +0 -1
|
@@ -0,0 +1,1354 @@
|
|
|
1
|
+
import NetworkSiteService from "../../../Server/Services/NetworkSiteService";
|
|
2
|
+
import NetworkSiteStatusTimelineService from "../../../Server/Services/NetworkSiteStatusTimelineService";
|
|
3
|
+
import NetworkDeviceService from "../../../Server/Services/NetworkDeviceService";
|
|
4
|
+
import MonitorService from "../../../Server/Services/MonitorService";
|
|
5
|
+
import MonitorStatusService from "../../../Server/Services/MonitorStatusService";
|
|
6
|
+
import NetworkSite from "../../../Models/DatabaseModels/NetworkSite";
|
|
7
|
+
import NetworkDevice from "../../../Models/DatabaseModels/NetworkDevice";
|
|
8
|
+
import Monitor from "../../../Models/DatabaseModels/Monitor";
|
|
9
|
+
import MonitorStatus from "../../../Models/DatabaseModels/MonitorStatus";
|
|
10
|
+
import NetworkSiteStatusTimeline from "../../../Models/DatabaseModels/NetworkSiteStatusTimeline";
|
|
11
|
+
import MonitorType from "../../../Types/Monitor/MonitorType";
|
|
12
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
13
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
14
|
+
import UpdateBy from "../../../Server/Types/Database/UpdateBy";
|
|
15
|
+
import DeleteBy from "../../../Server/Types/Database/DeleteBy";
|
|
16
|
+
import { OnDelete, OnUpdate } from "../../../Server/Types/Database/Hooks";
|
|
17
|
+
import { FindOperator } from "typeorm";
|
|
18
|
+
import { describe, expect, it, afterEach } from "@jest/globals";
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* Contract under test - the persisted rollup engine and the hierarchy
|
|
22
|
+
* maintenance around it:
|
|
23
|
+
*
|
|
24
|
+
* - recomputeRollupForSite persists worst-of over the subtree's devices,
|
|
25
|
+
* closes the open NetworkSiteStatusTimeline row and opens a new one on a
|
|
26
|
+
* change, and only stamps lastRollupAt when nothing changed (or no
|
|
27
|
+
* device contributes),
|
|
28
|
+
* - onMonitorStatusChanged stamps the referenced devices and recomputes
|
|
29
|
+
* each affected site chain exactly once, and NEVER throws,
|
|
30
|
+
* - onBeforeUpdate rejects cycles (a site under itself or one of its own
|
|
31
|
+
* descendants) with BadDataException,
|
|
32
|
+
* - onUpdateSuccess rebases the entire subtree's materialized paths on a
|
|
33
|
+
* parent change.
|
|
34
|
+
*
|
|
35
|
+
* Everything below the service boundary is spied - no database.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
39
|
+
"22222222-2222-4222-8222-222222222222",
|
|
40
|
+
);
|
|
41
|
+
const SITE_ID: ObjectID = new ObjectID("11111111-1111-4111-8111-111111111111");
|
|
42
|
+
const PARENT_SITE_ID: ObjectID = new ObjectID(
|
|
43
|
+
"33333333-3333-4333-8333-333333333333",
|
|
44
|
+
);
|
|
45
|
+
const OPERATIONAL_STATUS_ID: ObjectID = new ObjectID(
|
|
46
|
+
"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
47
|
+
);
|
|
48
|
+
const OFFLINE_STATUS_ID: ObjectID = new ObjectID(
|
|
49
|
+
"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
|
|
50
|
+
);
|
|
51
|
+
const DEVICE_ID: ObjectID = new ObjectID(
|
|
52
|
+
"cccccccc-cccc-4ccc-8ccc-cccccccccccc",
|
|
53
|
+
);
|
|
54
|
+
const OTHER_PROJECT_ID: ObjectID = new ObjectID(
|
|
55
|
+
"44444444-4444-4444-8444-444444444444",
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
function fakeSite(overrides: Record<string, unknown>): NetworkSite {
|
|
59
|
+
return {
|
|
60
|
+
id: SITE_ID,
|
|
61
|
+
_id: SITE_ID.toString(),
|
|
62
|
+
projectId: PROJECT_ID,
|
|
63
|
+
...overrides,
|
|
64
|
+
} as unknown as NetworkSite;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fakeStatuses(): Array<MonitorStatus> {
|
|
68
|
+
return [
|
|
69
|
+
{
|
|
70
|
+
id: OPERATIONAL_STATUS_ID,
|
|
71
|
+
priority: 1,
|
|
72
|
+
isOperationalState: true,
|
|
73
|
+
isOfflineState: false,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: OFFLINE_STATUS_ID,
|
|
77
|
+
priority: 3,
|
|
78
|
+
isOperationalState: false,
|
|
79
|
+
isOfflineState: true,
|
|
80
|
+
},
|
|
81
|
+
] as unknown as Array<MonitorStatus>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe("NetworkSiteService.recomputeRollupForSite", () => {
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
jest.restoreAllMocks();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
interface RollupSpies {
|
|
90
|
+
updateColumns: jest.SpyInstance;
|
|
91
|
+
timelineUpdateBy: jest.SpyInstance;
|
|
92
|
+
timelineCreate: jest.SpyInstance;
|
|
93
|
+
deviceFindBy: jest.SpyInstance;
|
|
94
|
+
descendantSiteIds: jest.SpyInstance;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function setupRollup(data: {
|
|
98
|
+
site: NetworkSite | null;
|
|
99
|
+
devices: Array<NetworkDevice>;
|
|
100
|
+
}): RollupSpies {
|
|
101
|
+
jest.spyOn(NetworkSiteService, "findOneById").mockResolvedValue(data.site);
|
|
102
|
+
const descendantSiteIds: jest.SpyInstance = jest
|
|
103
|
+
.spyOn(NetworkSiteService, "getDescendantSiteIds")
|
|
104
|
+
.mockResolvedValue([]);
|
|
105
|
+
const deviceFindBy: jest.SpyInstance = jest
|
|
106
|
+
.spyOn(NetworkDeviceService, "findBy")
|
|
107
|
+
.mockResolvedValue(data.devices);
|
|
108
|
+
jest
|
|
109
|
+
.spyOn(MonitorStatusService, "findBy")
|
|
110
|
+
.mockResolvedValue(fakeStatuses());
|
|
111
|
+
|
|
112
|
+
const updateColumns: jest.SpyInstance = jest
|
|
113
|
+
.spyOn(NetworkSiteService, "updateColumnsByIdWithoutHooks")
|
|
114
|
+
.mockResolvedValue(undefined as never);
|
|
115
|
+
const timelineUpdateBy: jest.SpyInstance = jest
|
|
116
|
+
.spyOn(NetworkSiteStatusTimelineService, "updateBy")
|
|
117
|
+
.mockResolvedValue(0 as never);
|
|
118
|
+
const timelineCreate: jest.SpyInstance = jest
|
|
119
|
+
.spyOn(NetworkSiteStatusTimelineService, "create")
|
|
120
|
+
.mockResolvedValue({} as never);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
updateColumns,
|
|
124
|
+
timelineUpdateBy,
|
|
125
|
+
timelineCreate,
|
|
126
|
+
deviceFindBy,
|
|
127
|
+
descendantSiteIds,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
it("persists a changed status and rolls the timeline (close open row, open new)", async () => {
|
|
132
|
+
const spies: RollupSpies = setupRollup({
|
|
133
|
+
site: fakeSite({ currentMonitorStatusId: OPERATIONAL_STATUS_ID }),
|
|
134
|
+
devices: [
|
|
135
|
+
{
|
|
136
|
+
id: DEVICE_ID,
|
|
137
|
+
currentMonitorStatusId: OFFLINE_STATUS_ID,
|
|
138
|
+
},
|
|
139
|
+
] as unknown as Array<NetworkDevice>,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await NetworkSiteService.recomputeRollupForSite(SITE_ID);
|
|
143
|
+
|
|
144
|
+
expect(spies.updateColumns).toHaveBeenCalledTimes(1);
|
|
145
|
+
const updateArgs: any = spies.updateColumns.mock.calls[0]![0];
|
|
146
|
+
expect(updateArgs.id.toString()).toBe(SITE_ID.toString());
|
|
147
|
+
expect(updateArgs.data.currentMonitorStatusId.toString()).toBe(
|
|
148
|
+
OFFLINE_STATUS_ID.toString(),
|
|
149
|
+
);
|
|
150
|
+
expect(updateArgs.data.lastRollupAt).toBeInstanceOf(Date);
|
|
151
|
+
|
|
152
|
+
// The open row is closed...
|
|
153
|
+
expect(spies.timelineUpdateBy).toHaveBeenCalledTimes(1);
|
|
154
|
+
const closeArgs: any = spies.timelineUpdateBy.mock.calls[0]![0];
|
|
155
|
+
expect(closeArgs.query.siteId.toString()).toBe(SITE_ID.toString());
|
|
156
|
+
expect(closeArgs.data.endsAt).toBeInstanceOf(Date);
|
|
157
|
+
|
|
158
|
+
// ...and a new one opened with the new status.
|
|
159
|
+
expect(spies.timelineCreate).toHaveBeenCalledTimes(1);
|
|
160
|
+
const created: NetworkSiteStatusTimeline =
|
|
161
|
+
spies.timelineCreate.mock.calls[0]![0].data;
|
|
162
|
+
expect(created.siteId?.toString()).toBe(SITE_ID.toString());
|
|
163
|
+
expect(created.projectId?.toString()).toBe(PROJECT_ID.toString());
|
|
164
|
+
expect(created.monitorStatusId?.toString()).toBe(
|
|
165
|
+
OFFLINE_STATUS_ID.toString(),
|
|
166
|
+
);
|
|
167
|
+
expect(created.startsAt).toBeInstanceOf(Date);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("only stamps lastRollupAt when the status is unchanged", async () => {
|
|
171
|
+
const spies: RollupSpies = setupRollup({
|
|
172
|
+
site: fakeSite({ currentMonitorStatusId: OFFLINE_STATUS_ID }),
|
|
173
|
+
devices: [
|
|
174
|
+
{
|
|
175
|
+
id: DEVICE_ID,
|
|
176
|
+
currentMonitorStatusId: OFFLINE_STATUS_ID,
|
|
177
|
+
},
|
|
178
|
+
] as unknown as Array<NetworkDevice>,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await NetworkSiteService.recomputeRollupForSite(SITE_ID);
|
|
182
|
+
|
|
183
|
+
expect(spies.updateColumns).toHaveBeenCalledTimes(1);
|
|
184
|
+
const updateArgs: any = spies.updateColumns.mock.calls[0]![0];
|
|
185
|
+
expect(Object.keys(updateArgs.data)).toEqual(["lastRollupAt"]);
|
|
186
|
+
expect(spies.timelineUpdateBy).not.toHaveBeenCalled();
|
|
187
|
+
expect(spies.timelineCreate).not.toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("an empty device subtree is a no-op on status (lastRollupAt only)", async () => {
|
|
191
|
+
const spies: RollupSpies = setupRollup({
|
|
192
|
+
site: fakeSite({ currentMonitorStatusId: OPERATIONAL_STATUS_ID }),
|
|
193
|
+
devices: [],
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await NetworkSiteService.recomputeRollupForSite(SITE_ID);
|
|
197
|
+
|
|
198
|
+
const updateArgs: any = spies.updateColumns.mock.calls[0]![0];
|
|
199
|
+
expect(Object.keys(updateArgs.data)).toEqual(["lastRollupAt"]);
|
|
200
|
+
expect(spies.timelineUpdateBy).not.toHaveBeenCalled();
|
|
201
|
+
expect(spies.timelineCreate).not.toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("uses the freshness fallback for devices without a stamped status", async () => {
|
|
205
|
+
const spies: RollupSpies = setupRollup({
|
|
206
|
+
site: fakeSite({ currentMonitorStatusId: OPERATIONAL_STATUS_ID }),
|
|
207
|
+
devices: [
|
|
208
|
+
{
|
|
209
|
+
id: DEVICE_ID,
|
|
210
|
+
// Unmonitored and last seen an hour ago -> offline equivalent.
|
|
211
|
+
lastSeenAt: new Date(Date.now() - 60 * 60 * 1000),
|
|
212
|
+
},
|
|
213
|
+
] as unknown as Array<NetworkDevice>,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
await NetworkSiteService.recomputeRollupForSite(SITE_ID);
|
|
217
|
+
|
|
218
|
+
const updateArgs: any = spies.updateColumns.mock.calls[0]![0];
|
|
219
|
+
expect(updateArgs.data.currentMonitorStatusId.toString()).toBe(
|
|
220
|
+
OFFLINE_STATUS_ID.toString(),
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("does nothing when the site does not exist", async () => {
|
|
225
|
+
const spies: RollupSpies = setupRollup({ site: null, devices: [] });
|
|
226
|
+
|
|
227
|
+
await NetworkSiteService.recomputeRollupForSite(SITE_ID);
|
|
228
|
+
|
|
229
|
+
expect(spies.updateColumns).not.toHaveBeenCalled();
|
|
230
|
+
expect(spies.timelineUpdateBy).not.toHaveBeenCalled();
|
|
231
|
+
expect(spies.timelineCreate).not.toHaveBeenCalled();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
/*
|
|
235
|
+
* An archived device is decommissioned: it keeps its siteId but must not
|
|
236
|
+
* vote. Without this filter an archived, never-monitored device hits the
|
|
237
|
+
* freshness fallback and pins its whole ancestor chain Offline forever.
|
|
238
|
+
*/
|
|
239
|
+
it("excludes archived devices from the subtree scan", async () => {
|
|
240
|
+
const spies: RollupSpies = setupRollup({
|
|
241
|
+
site: fakeSite({ currentMonitorStatusId: OPERATIONAL_STATUS_ID }),
|
|
242
|
+
devices: [],
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await NetworkSiteService.recomputeRollupForSite(SITE_ID);
|
|
246
|
+
|
|
247
|
+
expect(spies.deviceFindBy).toHaveBeenCalledTimes(1);
|
|
248
|
+
const deviceQuery: any = spies.deviceFindBy.mock.calls[0]![0].query;
|
|
249
|
+
expect(deviceQuery.isArchived).toBe(false);
|
|
250
|
+
expect(deviceQuery.projectId.toString()).toBe(PROJECT_ID.toString());
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("scopes the descendant lookup to the site's own project", async () => {
|
|
254
|
+
const spies: RollupSpies = setupRollup({
|
|
255
|
+
site: fakeSite({ currentMonitorStatusId: OPERATIONAL_STATUS_ID }),
|
|
256
|
+
devices: [],
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await NetworkSiteService.recomputeRollupForSite(SITE_ID);
|
|
260
|
+
|
|
261
|
+
expect(spies.descendantSiteIds).toHaveBeenCalledTimes(1);
|
|
262
|
+
const [calledSiteId, calledProjectId]: Array<any> =
|
|
263
|
+
spies.descendantSiteIds.mock.calls[0]!;
|
|
264
|
+
expect(calledSiteId.toString()).toBe(SITE_ID.toString());
|
|
265
|
+
expect(calledProjectId.toString()).toBe(PROJECT_ID.toString());
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe("NetworkSiteService.getDescendantSiteIds", () => {
|
|
270
|
+
afterEach(() => {
|
|
271
|
+
jest.restoreAllMocks();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
/*
|
|
275
|
+
* The prefix query must carry projectId (otherwise it reads every tenant's
|
|
276
|
+
* rows) and must not go through QueryHelper.startsWith, whose
|
|
277
|
+
* `CAST(alias AS TEXT) ILIKE` form makes the materializedPath btree index
|
|
278
|
+
* unusable and forces a sequential scan on the hot rollup path.
|
|
279
|
+
*/
|
|
280
|
+
it("scopes the prefix query to the project and emits an indexable LIKE", async () => {
|
|
281
|
+
const childId: ObjectID = new ObjectID(
|
|
282
|
+
"eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
|
|
283
|
+
);
|
|
284
|
+
const path: string = `/${SITE_ID.toString()}/`;
|
|
285
|
+
|
|
286
|
+
jest
|
|
287
|
+
.spyOn(NetworkSiteService, "getMaterializedPathForSite")
|
|
288
|
+
.mockResolvedValue(path);
|
|
289
|
+
const findBySpy: jest.SpyInstance = jest
|
|
290
|
+
.spyOn(NetworkSiteService, "findBy")
|
|
291
|
+
.mockResolvedValue([
|
|
292
|
+
fakeSite({ materializedPath: path }),
|
|
293
|
+
fakeSite({
|
|
294
|
+
id: childId,
|
|
295
|
+
_id: childId.toString(),
|
|
296
|
+
materializedPath: `${path}${childId.toString()}/`,
|
|
297
|
+
}),
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
const descendants: Array<ObjectID> =
|
|
301
|
+
await NetworkSiteService.getDescendantSiteIds(SITE_ID, PROJECT_ID);
|
|
302
|
+
|
|
303
|
+
// The site itself is excluded.
|
|
304
|
+
expect(
|
|
305
|
+
descendants.map((id: ObjectID) => {
|
|
306
|
+
return id.toString();
|
|
307
|
+
}),
|
|
308
|
+
).toEqual([childId.toString()]);
|
|
309
|
+
|
|
310
|
+
const query: any = findBySpy.mock.calls[0]![0].query;
|
|
311
|
+
expect(query.projectId.toString()).toBe(PROJECT_ID.toString());
|
|
312
|
+
|
|
313
|
+
const pathPredicate: FindOperator<any> = query.materializedPath;
|
|
314
|
+
const boundParameters: Record<string, unknown> =
|
|
315
|
+
pathPredicate.objectLiteralParameters as Record<string, unknown>;
|
|
316
|
+
expect(pathPredicate.getSql!("site.materializedPath")).toBe(
|
|
317
|
+
`(site.materializedPath LIKE :${Object.keys(boundParameters)[0]})`,
|
|
318
|
+
);
|
|
319
|
+
expect(Object.values(boundParameters)[0]).toBe(`${path}%`);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("returns [] when the site has no path", async () => {
|
|
323
|
+
jest
|
|
324
|
+
.spyOn(NetworkSiteService, "getMaterializedPathForSite")
|
|
325
|
+
.mockResolvedValue(null);
|
|
326
|
+
const findBySpy: jest.SpyInstance = jest.spyOn(
|
|
327
|
+
NetworkSiteService,
|
|
328
|
+
"findBy",
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
expect(
|
|
332
|
+
await NetworkSiteService.getDescendantSiteIds(SITE_ID, PROJECT_ID),
|
|
333
|
+
).toEqual([]);
|
|
334
|
+
expect(findBySpy).not.toHaveBeenCalled();
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe("NetworkSiteService.recomputeRollupForSiteAndAncestors", () => {
|
|
339
|
+
afterEach(() => {
|
|
340
|
+
jest.restoreAllMocks();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("recomputes the site first, then each ancestor nearest-first", async () => {
|
|
344
|
+
const rootId: ObjectID = new ObjectID(
|
|
345
|
+
"dddddddd-dddd-4ddd-8ddd-dddddddddddd",
|
|
346
|
+
);
|
|
347
|
+
const recomputeSpy: jest.SpyInstance = jest
|
|
348
|
+
.spyOn(NetworkSiteService, "recomputeRollupForSite")
|
|
349
|
+
.mockResolvedValue(undefined as never);
|
|
350
|
+
jest
|
|
351
|
+
.spyOn(NetworkSiteService, "getAncestorIds")
|
|
352
|
+
.mockResolvedValue([rootId, PARENT_SITE_ID]); // root-first
|
|
353
|
+
|
|
354
|
+
await NetworkSiteService.recomputeRollupForSiteAndAncestors(SITE_ID);
|
|
355
|
+
|
|
356
|
+
const calledWith: Array<string> = recomputeSpy.mock.calls.map(
|
|
357
|
+
(call: Array<any>) => {
|
|
358
|
+
return call[0].toString();
|
|
359
|
+
},
|
|
360
|
+
);
|
|
361
|
+
expect(calledWith).toEqual([
|
|
362
|
+
SITE_ID.toString(),
|
|
363
|
+
PARENT_SITE_ID.toString(),
|
|
364
|
+
rootId.toString(),
|
|
365
|
+
]);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
describe("NetworkSiteService.getAncestorIds", () => {
|
|
370
|
+
afterEach(() => {
|
|
371
|
+
jest.restoreAllMocks();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("parses ancestors (excluding the site itself) from the materialized path", async () => {
|
|
375
|
+
jest
|
|
376
|
+
.spyOn(NetworkSiteService, "getMaterializedPathForSite")
|
|
377
|
+
.mockResolvedValue(
|
|
378
|
+
`/${PARENT_SITE_ID.toString()}/${SITE_ID.toString()}/`,
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const ancestors: Array<ObjectID> =
|
|
382
|
+
await NetworkSiteService.getAncestorIds(SITE_ID);
|
|
383
|
+
|
|
384
|
+
expect(
|
|
385
|
+
ancestors.map((id: ObjectID) => {
|
|
386
|
+
return id.toString();
|
|
387
|
+
}),
|
|
388
|
+
).toEqual([PARENT_SITE_ID.toString()]);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("returns [] when the site has no path", async () => {
|
|
392
|
+
jest
|
|
393
|
+
.spyOn(NetworkSiteService, "getMaterializedPathForSite")
|
|
394
|
+
.mockResolvedValue(null);
|
|
395
|
+
|
|
396
|
+
expect(await NetworkSiteService.getAncestorIds(SITE_ID)).toEqual([]);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe("NetworkSiteService.onBeforeUpdate (cycle rejection)", () => {
|
|
401
|
+
afterEach(() => {
|
|
402
|
+
jest.restoreAllMocks();
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
function makeUpdateBy(parentSiteId: ObjectID | null): UpdateBy<NetworkSite> {
|
|
406
|
+
return {
|
|
407
|
+
query: { _id: SITE_ID.toString() },
|
|
408
|
+
data: { parentSiteId: parentSiteId },
|
|
409
|
+
props: { isRoot: true },
|
|
410
|
+
} as unknown as UpdateBy<NetworkSite>;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
it("rejects moving a site under one of its own descendants", async () => {
|
|
414
|
+
const childId: ObjectID = new ObjectID(
|
|
415
|
+
"eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
jest
|
|
419
|
+
.spyOn(NetworkSiteService, "findBy")
|
|
420
|
+
.mockResolvedValue([
|
|
421
|
+
fakeSite({ materializedPath: `/${SITE_ID.toString()}/` }),
|
|
422
|
+
]);
|
|
423
|
+
jest.spyOn(NetworkSiteService, "findOneById").mockResolvedValue(
|
|
424
|
+
fakeSite({
|
|
425
|
+
id: childId,
|
|
426
|
+
_id: childId.toString(),
|
|
427
|
+
}),
|
|
428
|
+
);
|
|
429
|
+
// The proposed parent's path contains the site being moved -> cycle.
|
|
430
|
+
jest
|
|
431
|
+
.spyOn(NetworkSiteService, "getMaterializedPathForSite")
|
|
432
|
+
.mockResolvedValue(`/${SITE_ID.toString()}/${childId.toString()}/`);
|
|
433
|
+
|
|
434
|
+
await expect(
|
|
435
|
+
(NetworkSiteService as any).onBeforeUpdate(makeUpdateBy(childId)),
|
|
436
|
+
).rejects.toThrow(BadDataException);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it("rejects a site becoming its own parent", async () => {
|
|
440
|
+
jest
|
|
441
|
+
.spyOn(NetworkSiteService, "findBy")
|
|
442
|
+
.mockResolvedValue([
|
|
443
|
+
fakeSite({ materializedPath: `/${SITE_ID.toString()}/` }),
|
|
444
|
+
]);
|
|
445
|
+
jest
|
|
446
|
+
.spyOn(NetworkSiteService, "findOneById")
|
|
447
|
+
.mockResolvedValue(fakeSite({}));
|
|
448
|
+
|
|
449
|
+
await expect(
|
|
450
|
+
(NetworkSiteService as any).onBeforeUpdate(makeUpdateBy(SITE_ID)),
|
|
451
|
+
).rejects.toThrow(BadDataException);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("rejects a missing parent", async () => {
|
|
455
|
+
jest.spyOn(NetworkSiteService, "findBy").mockResolvedValue([fakeSite({})]);
|
|
456
|
+
jest.spyOn(NetworkSiteService, "findOneById").mockResolvedValue(null);
|
|
457
|
+
|
|
458
|
+
await expect(
|
|
459
|
+
(NetworkSiteService as any).onBeforeUpdate(makeUpdateBy(PARENT_SITE_ID)),
|
|
460
|
+
).rejects.toThrow(BadDataException);
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it("allows a legal move and carries the previous state forward", async () => {
|
|
464
|
+
jest
|
|
465
|
+
.spyOn(NetworkSiteService, "findBy")
|
|
466
|
+
.mockResolvedValue([
|
|
467
|
+
fakeSite({ materializedPath: `/${SITE_ID.toString()}/` }),
|
|
468
|
+
]);
|
|
469
|
+
jest.spyOn(NetworkSiteService, "findOneById").mockResolvedValue(
|
|
470
|
+
fakeSite({
|
|
471
|
+
id: PARENT_SITE_ID,
|
|
472
|
+
_id: PARENT_SITE_ID.toString(),
|
|
473
|
+
}),
|
|
474
|
+
);
|
|
475
|
+
jest
|
|
476
|
+
.spyOn(NetworkSiteService, "getMaterializedPathForSite")
|
|
477
|
+
.mockResolvedValue(`/${PARENT_SITE_ID.toString()}/`);
|
|
478
|
+
|
|
479
|
+
const result: OnUpdate<NetworkSite> = await (
|
|
480
|
+
NetworkSiteService as any
|
|
481
|
+
).onBeforeUpdate(makeUpdateBy(PARENT_SITE_ID));
|
|
482
|
+
|
|
483
|
+
expect(result.carryForward.newParentPath).toBe(
|
|
484
|
+
`/${PARENT_SITE_ID.toString()}/`,
|
|
485
|
+
);
|
|
486
|
+
expect(result.carryForward.previousItems).toHaveLength(1);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it("moving to root (parentSiteId null) skips parent validation", async () => {
|
|
490
|
+
jest
|
|
491
|
+
.spyOn(NetworkSiteService, "findBy")
|
|
492
|
+
.mockResolvedValue([
|
|
493
|
+
fakeSite({ materializedPath: `/x/${SITE_ID.toString()}/` }),
|
|
494
|
+
]);
|
|
495
|
+
const findOneByIdSpy: jest.SpyInstance = jest.spyOn(
|
|
496
|
+
NetworkSiteService,
|
|
497
|
+
"findOneById",
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
const result: OnUpdate<NetworkSite> = await (
|
|
501
|
+
NetworkSiteService as any
|
|
502
|
+
).onBeforeUpdate(makeUpdateBy(null));
|
|
503
|
+
|
|
504
|
+
expect(result.carryForward.newParentPath).toBeNull();
|
|
505
|
+
expect(findOneByIdSpy).not.toHaveBeenCalled();
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("does nothing when the update does not touch parentSiteId", async () => {
|
|
509
|
+
const findBySpy: jest.SpyInstance = jest.spyOn(
|
|
510
|
+
NetworkSiteService,
|
|
511
|
+
"findBy",
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
const result: OnUpdate<NetworkSite> = await (
|
|
515
|
+
NetworkSiteService as any
|
|
516
|
+
).onBeforeUpdate({
|
|
517
|
+
query: { _id: SITE_ID.toString() },
|
|
518
|
+
data: { name: "renamed" },
|
|
519
|
+
props: { isRoot: true },
|
|
520
|
+
} as unknown as UpdateBy<NetworkSite>);
|
|
521
|
+
|
|
522
|
+
expect(result.carryForward).toBeNull();
|
|
523
|
+
expect(findBySpy).not.toHaveBeenCalled();
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
/*
|
|
528
|
+
* onBeforeUpdate runs BEFORE DatabaseService applies tenant scoping to the
|
|
529
|
+
* query, so an unscoped root read here would hand the hook another project's
|
|
530
|
+
* row - which onUpdateSuccess would then rewrite even though the scoped
|
|
531
|
+
* UPDATE matched nothing.
|
|
532
|
+
*/
|
|
533
|
+
describe("NetworkSiteService.onBeforeUpdate (tenant scoping)", () => {
|
|
534
|
+
afterEach(() => {
|
|
535
|
+
jest.restoreAllMocks();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
function makeTenantUpdateBy(
|
|
539
|
+
parentSiteId: ObjectID | null,
|
|
540
|
+
): UpdateBy<NetworkSite> {
|
|
541
|
+
return {
|
|
542
|
+
query: { _id: SITE_ID.toString() },
|
|
543
|
+
data: { parentSiteId: parentSiteId },
|
|
544
|
+
props: { tenantId: PROJECT_ID },
|
|
545
|
+
} as unknown as UpdateBy<NetworkSite>;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
it("adds the caller's project to the previous-item read", async () => {
|
|
549
|
+
const findBySpy: jest.SpyInstance = jest
|
|
550
|
+
.spyOn(NetworkSiteService, "findBy")
|
|
551
|
+
.mockResolvedValue([]);
|
|
552
|
+
|
|
553
|
+
await (NetworkSiteService as any).onBeforeUpdate(makeTenantUpdateBy(null));
|
|
554
|
+
|
|
555
|
+
expect(findBySpy).toHaveBeenCalledTimes(1);
|
|
556
|
+
const query: any = findBySpy.mock.calls[0]![0].query;
|
|
557
|
+
expect(query._id).toBe(SITE_ID.toString());
|
|
558
|
+
expect(query.projectId.toString()).toBe(PROJECT_ID.toString());
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
/*
|
|
562
|
+
* The detach case has no parent to compare against, so it used to skip
|
|
563
|
+
* every project check while still carrying the victim row forward.
|
|
564
|
+
*/
|
|
565
|
+
it("rejects a parentSiteId:null detach of a site in another project", async () => {
|
|
566
|
+
jest.spyOn(NetworkSiteService, "findBy").mockResolvedValue([
|
|
567
|
+
fakeSite({
|
|
568
|
+
projectId: OTHER_PROJECT_ID,
|
|
569
|
+
materializedPath: `/${PARENT_SITE_ID.toString()}/${SITE_ID.toString()}/`,
|
|
570
|
+
}),
|
|
571
|
+
]);
|
|
572
|
+
|
|
573
|
+
await expect(
|
|
574
|
+
(NetworkSiteService as any).onBeforeUpdate(makeTenantUpdateBy(null)),
|
|
575
|
+
).rejects.toThrow(BadDataException);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it("rejects a re-parent of a site in another project", async () => {
|
|
579
|
+
jest
|
|
580
|
+
.spyOn(NetworkSiteService, "findBy")
|
|
581
|
+
.mockResolvedValue([
|
|
582
|
+
fakeSite({ projectId: OTHER_PROJECT_ID, materializedPath: null }),
|
|
583
|
+
]);
|
|
584
|
+
const findOneByIdSpy: jest.SpyInstance = jest.spyOn(
|
|
585
|
+
NetworkSiteService,
|
|
586
|
+
"findOneById",
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
await expect(
|
|
590
|
+
(NetworkSiteService as any).onBeforeUpdate(
|
|
591
|
+
makeTenantUpdateBy(PARENT_SITE_ID),
|
|
592
|
+
),
|
|
593
|
+
).rejects.toThrow(BadDataException);
|
|
594
|
+
|
|
595
|
+
// Rejected before the parent is even resolved.
|
|
596
|
+
expect(findOneByIdSpy).not.toHaveBeenCalled();
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it("allows a detach of a site inside the caller's project", async () => {
|
|
600
|
+
jest.spyOn(NetworkSiteService, "findBy").mockResolvedValue([
|
|
601
|
+
fakeSite({
|
|
602
|
+
materializedPath: `/${PARENT_SITE_ID.toString()}/${SITE_ID.toString()}/`,
|
|
603
|
+
}),
|
|
604
|
+
]);
|
|
605
|
+
|
|
606
|
+
const result: OnUpdate<NetworkSite> = await (
|
|
607
|
+
NetworkSiteService as any
|
|
608
|
+
).onBeforeUpdate(makeTenantUpdateBy(null));
|
|
609
|
+
|
|
610
|
+
expect(result.carryForward.previousItems).toHaveLength(1);
|
|
611
|
+
expect(result.carryForward.newParentPath).toBeNull();
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
describe("NetworkSiteService.onBeforeCreate (cross-project parent guard)", () => {
|
|
616
|
+
afterEach(() => {
|
|
617
|
+
jest.restoreAllMocks();
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it("rejects a parent that belongs to another project", async () => {
|
|
621
|
+
jest.spyOn(NetworkSiteService, "findOneById").mockResolvedValue(
|
|
622
|
+
fakeSite({
|
|
623
|
+
id: PARENT_SITE_ID,
|
|
624
|
+
_id: PARENT_SITE_ID.toString(),
|
|
625
|
+
projectId: OTHER_PROJECT_ID,
|
|
626
|
+
}),
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
await expect(
|
|
630
|
+
(NetworkSiteService as any).onBeforeCreate({
|
|
631
|
+
data: {
|
|
632
|
+
projectId: PROJECT_ID,
|
|
633
|
+
parentSiteId: PARENT_SITE_ID,
|
|
634
|
+
},
|
|
635
|
+
props: { tenantId: PROJECT_ID },
|
|
636
|
+
}),
|
|
637
|
+
).rejects.toThrow(BadDataException);
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("carries the parent path forward for a same-project parent", async () => {
|
|
641
|
+
jest.spyOn(NetworkSiteService, "findOneById").mockResolvedValue(
|
|
642
|
+
fakeSite({
|
|
643
|
+
id: PARENT_SITE_ID,
|
|
644
|
+
_id: PARENT_SITE_ID.toString(),
|
|
645
|
+
}),
|
|
646
|
+
);
|
|
647
|
+
jest
|
|
648
|
+
.spyOn(NetworkSiteService, "getMaterializedPathForSite")
|
|
649
|
+
.mockResolvedValue(`/${PARENT_SITE_ID.toString()}/`);
|
|
650
|
+
|
|
651
|
+
const result: any = await (NetworkSiteService as any).onBeforeCreate({
|
|
652
|
+
data: {
|
|
653
|
+
projectId: PROJECT_ID,
|
|
654
|
+
parentSiteId: PARENT_SITE_ID,
|
|
655
|
+
},
|
|
656
|
+
props: { tenantId: PROJECT_ID },
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
expect(result.carryForward.parentPath).toBe(
|
|
660
|
+
`/${PARENT_SITE_ID.toString()}/`,
|
|
661
|
+
);
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
describe("NetworkSiteService.onUpdateSuccess (subtree rebase)", () => {
|
|
666
|
+
afterEach(() => {
|
|
667
|
+
jest.restoreAllMocks();
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
it("rebases the moved site and every descendant, then refreshes both chains", async () => {
|
|
671
|
+
const childId: ObjectID = new ObjectID(
|
|
672
|
+
"eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
|
|
673
|
+
);
|
|
674
|
+
const oldParentId: ObjectID = PARENT_SITE_ID;
|
|
675
|
+
const newParentId: ObjectID = new ObjectID(
|
|
676
|
+
"ffffffff-ffff-4fff-8fff-ffffffffffff",
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
const oldPath: string = `/${oldParentId.toString()}/${SITE_ID.toString()}/`;
|
|
680
|
+
const oldChildPath: string = `${oldPath}${childId.toString()}/`;
|
|
681
|
+
|
|
682
|
+
const updateColumnsSpy: jest.SpyInstance = jest
|
|
683
|
+
.spyOn(NetworkSiteService, "updateColumnsByIdWithoutHooks")
|
|
684
|
+
.mockResolvedValue(undefined as never);
|
|
685
|
+
jest.spyOn(NetworkSiteService, "findBy").mockResolvedValue([
|
|
686
|
+
// The prefix query matches the moved site itself and its child.
|
|
687
|
+
fakeSite({ materializedPath: oldPath }),
|
|
688
|
+
fakeSite({
|
|
689
|
+
id: childId,
|
|
690
|
+
_id: childId.toString(),
|
|
691
|
+
materializedPath: oldChildPath,
|
|
692
|
+
}),
|
|
693
|
+
]);
|
|
694
|
+
const rollupSpy: jest.SpyInstance = jest
|
|
695
|
+
.spyOn(NetworkSiteService, "recomputeRollupForSiteAndAncestors")
|
|
696
|
+
.mockResolvedValue(undefined as never);
|
|
697
|
+
|
|
698
|
+
const onUpdate: OnUpdate<NetworkSite> = {
|
|
699
|
+
updateBy: {
|
|
700
|
+
query: { _id: SITE_ID.toString() },
|
|
701
|
+
data: { parentSiteId: newParentId },
|
|
702
|
+
props: { isRoot: true },
|
|
703
|
+
} as unknown as UpdateBy<NetworkSite>,
|
|
704
|
+
carryForward: {
|
|
705
|
+
previousItems: [
|
|
706
|
+
fakeSite({
|
|
707
|
+
materializedPath: oldPath,
|
|
708
|
+
parentSiteId: oldParentId,
|
|
709
|
+
}),
|
|
710
|
+
],
|
|
711
|
+
newParentId: newParentId,
|
|
712
|
+
newParentPath: `/${newParentId.toString()}/`,
|
|
713
|
+
},
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
await (NetworkSiteService as any).onUpdateSuccess(onUpdate, [SITE_ID]);
|
|
717
|
+
|
|
718
|
+
const newPath: string = `/${newParentId.toString()}/${SITE_ID.toString()}/`;
|
|
719
|
+
|
|
720
|
+
// Moved site rewritten...
|
|
721
|
+
const selfUpdate: any = updateColumnsSpy.mock.calls.find((call: any) => {
|
|
722
|
+
return call[0].id.toString() === SITE_ID.toString();
|
|
723
|
+
});
|
|
724
|
+
expect(selfUpdate[0].data.materializedPath).toBe(newPath);
|
|
725
|
+
expect(selfUpdate[0].data.depth).toBe(1);
|
|
726
|
+
|
|
727
|
+
// ...and the descendant rebased under the new prefix.
|
|
728
|
+
const childUpdate: any = updateColumnsSpy.mock.calls.find((call: any) => {
|
|
729
|
+
return call[0].id.toString() === childId.toString();
|
|
730
|
+
});
|
|
731
|
+
expect(childUpdate[0].data.materializedPath).toBe(
|
|
732
|
+
`${newPath}${childId.toString()}/`,
|
|
733
|
+
);
|
|
734
|
+
expect(childUpdate[0].data.depth).toBe(2);
|
|
735
|
+
|
|
736
|
+
// Rollups: the moved site's new chain + the old parent's chain.
|
|
737
|
+
const rollupIds: Array<string> = rollupSpy.mock.calls.map(
|
|
738
|
+
(call: Array<any>) => {
|
|
739
|
+
return call[0].toString();
|
|
740
|
+
},
|
|
741
|
+
);
|
|
742
|
+
expect(rollupIds).toContain(SITE_ID.toString());
|
|
743
|
+
expect(rollupIds).toContain(oldParentId.toString());
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
/*
|
|
747
|
+
* DatabaseService calls onUpdateSuccess even when the tenant-scoped UPDATE
|
|
748
|
+
* matched zero rows. Nothing may be written for a row the UPDATE did not
|
|
749
|
+
* touch - otherwise a cross-project id in the query rewrites the victim's
|
|
750
|
+
* path and rollup while its parentSiteId column stays untouched.
|
|
751
|
+
*/
|
|
752
|
+
it("writes nothing for sites the scoped update did not match", async () => {
|
|
753
|
+
const updateColumnsSpy: jest.SpyInstance = jest.spyOn(
|
|
754
|
+
NetworkSiteService,
|
|
755
|
+
"updateColumnsByIdWithoutHooks",
|
|
756
|
+
);
|
|
757
|
+
const findBySpy: jest.SpyInstance = jest.spyOn(
|
|
758
|
+
NetworkSiteService,
|
|
759
|
+
"findBy",
|
|
760
|
+
);
|
|
761
|
+
const rollupSpy: jest.SpyInstance = jest.spyOn(
|
|
762
|
+
NetworkSiteService,
|
|
763
|
+
"recomputeRollupForSiteAndAncestors",
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
await (NetworkSiteService as any).onUpdateSuccess(
|
|
767
|
+
{
|
|
768
|
+
updateBy: {
|
|
769
|
+
query: { _id: SITE_ID.toString() },
|
|
770
|
+
data: { parentSiteId: null },
|
|
771
|
+
props: { tenantId: PROJECT_ID },
|
|
772
|
+
} as unknown as UpdateBy<NetworkSite>,
|
|
773
|
+
carryForward: {
|
|
774
|
+
previousItems: [
|
|
775
|
+
fakeSite({
|
|
776
|
+
projectId: OTHER_PROJECT_ID,
|
|
777
|
+
parentSiteId: PARENT_SITE_ID,
|
|
778
|
+
materializedPath: `/${PARENT_SITE_ID.toString()}/${SITE_ID.toString()}/`,
|
|
779
|
+
}),
|
|
780
|
+
],
|
|
781
|
+
newParentId: null,
|
|
782
|
+
newParentPath: null,
|
|
783
|
+
},
|
|
784
|
+
},
|
|
785
|
+
[], // the scoped UPDATE matched nothing
|
|
786
|
+
);
|
|
787
|
+
|
|
788
|
+
expect(updateColumnsSpy).not.toHaveBeenCalled();
|
|
789
|
+
expect(findBySpy).not.toHaveBeenCalled();
|
|
790
|
+
expect(rollupSpy).not.toHaveBeenCalled();
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
it("is a no-op without a parent-change carryForward", async () => {
|
|
794
|
+
const updateColumnsSpy: jest.SpyInstance = jest.spyOn(
|
|
795
|
+
NetworkSiteService,
|
|
796
|
+
"updateColumnsByIdWithoutHooks",
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
await (NetworkSiteService as any).onUpdateSuccess(
|
|
800
|
+
{
|
|
801
|
+
updateBy: {
|
|
802
|
+
query: {},
|
|
803
|
+
data: { name: "renamed" },
|
|
804
|
+
props: { isRoot: true },
|
|
805
|
+
},
|
|
806
|
+
carryForward: null,
|
|
807
|
+
},
|
|
808
|
+
[SITE_ID],
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
expect(updateColumnsSpy).not.toHaveBeenCalled();
|
|
812
|
+
});
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
describe("NetworkSiteService.onMonitorStatusChanged", () => {
|
|
816
|
+
afterEach(() => {
|
|
817
|
+
jest.restoreAllMocks();
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
const MONITOR_ID: ObjectID = new ObjectID(
|
|
821
|
+
"99999999-9999-4999-8999-999999999999",
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
function fakeNetworkDeviceMonitor(deviceIds: Array<string>): Monitor {
|
|
825
|
+
return {
|
|
826
|
+
id: MONITOR_ID,
|
|
827
|
+
monitorType: MonitorType.NetworkDevice,
|
|
828
|
+
monitorSteps: {
|
|
829
|
+
data: {
|
|
830
|
+
monitorStepsInstanceArray: deviceIds.map((deviceId: string) => {
|
|
831
|
+
return {
|
|
832
|
+
data: {
|
|
833
|
+
networkDeviceMonitor: { networkDeviceId: deviceId },
|
|
834
|
+
},
|
|
835
|
+
};
|
|
836
|
+
}),
|
|
837
|
+
},
|
|
838
|
+
},
|
|
839
|
+
} as unknown as Monitor;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
it("stamps referenced devices and recomputes each distinct site chain once", async () => {
|
|
843
|
+
const secondDeviceId: ObjectID = new ObjectID(
|
|
844
|
+
"dddddddd-dddd-4ddd-8ddd-dddddddddddd",
|
|
845
|
+
);
|
|
846
|
+
const otherSiteId: ObjectID = new ObjectID(
|
|
847
|
+
"eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
jest
|
|
851
|
+
.spyOn(MonitorService, "findBy")
|
|
852
|
+
.mockResolvedValue([
|
|
853
|
+
fakeNetworkDeviceMonitor([
|
|
854
|
+
DEVICE_ID.toString(),
|
|
855
|
+
secondDeviceId.toString(),
|
|
856
|
+
]),
|
|
857
|
+
]);
|
|
858
|
+
jest.spyOn(NetworkDeviceService, "findBy").mockResolvedValue([
|
|
859
|
+
{ id: DEVICE_ID, siteId: SITE_ID },
|
|
860
|
+
{ id: secondDeviceId, siteId: otherSiteId },
|
|
861
|
+
] as unknown as Array<NetworkDevice>);
|
|
862
|
+
|
|
863
|
+
const stampSpy: jest.SpyInstance = jest
|
|
864
|
+
.spyOn(NetworkDeviceService, "updateColumnsByIdWithoutHooks")
|
|
865
|
+
.mockResolvedValue(undefined as never);
|
|
866
|
+
const rollupSpy: jest.SpyInstance = jest
|
|
867
|
+
.spyOn(NetworkSiteService, "recomputeRollupForSiteAndAncestors")
|
|
868
|
+
.mockResolvedValue(undefined as never);
|
|
869
|
+
|
|
870
|
+
await NetworkSiteService.onMonitorStatusChanged({
|
|
871
|
+
projectId: PROJECT_ID,
|
|
872
|
+
monitorIds: [MONITOR_ID],
|
|
873
|
+
monitorStatusId: OFFLINE_STATUS_ID,
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
expect(stampSpy).toHaveBeenCalledTimes(2);
|
|
877
|
+
for (const call of stampSpy.mock.calls) {
|
|
878
|
+
expect((call[0] as any).data.currentMonitorStatusId.toString()).toBe(
|
|
879
|
+
OFFLINE_STATUS_ID.toString(),
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const rollupIds: Array<string> = rollupSpy.mock.calls.map(
|
|
884
|
+
(call: Array<any>) => {
|
|
885
|
+
return call[0].toString();
|
|
886
|
+
},
|
|
887
|
+
);
|
|
888
|
+
expect(rollupIds.sort()).toEqual(
|
|
889
|
+
[SITE_ID.toString(), otherSiteId.toString()].sort(),
|
|
890
|
+
);
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
it("deduplicates devices that share a site", async () => {
|
|
894
|
+
const secondDeviceId: ObjectID = new ObjectID(
|
|
895
|
+
"dddddddd-dddd-4ddd-8ddd-dddddddddddd",
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
jest
|
|
899
|
+
.spyOn(MonitorService, "findBy")
|
|
900
|
+
.mockResolvedValue([
|
|
901
|
+
fakeNetworkDeviceMonitor([
|
|
902
|
+
DEVICE_ID.toString(),
|
|
903
|
+
secondDeviceId.toString(),
|
|
904
|
+
]),
|
|
905
|
+
]);
|
|
906
|
+
jest.spyOn(NetworkDeviceService, "findBy").mockResolvedValue([
|
|
907
|
+
{ id: DEVICE_ID, siteId: SITE_ID },
|
|
908
|
+
{ id: secondDeviceId, siteId: SITE_ID },
|
|
909
|
+
] as unknown as Array<NetworkDevice>);
|
|
910
|
+
jest
|
|
911
|
+
.spyOn(NetworkDeviceService, "updateColumnsByIdWithoutHooks")
|
|
912
|
+
.mockResolvedValue(undefined as never);
|
|
913
|
+
const rollupSpy: jest.SpyInstance = jest
|
|
914
|
+
.spyOn(NetworkSiteService, "recomputeRollupForSiteAndAncestors")
|
|
915
|
+
.mockResolvedValue(undefined as never);
|
|
916
|
+
|
|
917
|
+
await NetworkSiteService.onMonitorStatusChanged({
|
|
918
|
+
projectId: PROJECT_ID,
|
|
919
|
+
monitorIds: [MONITOR_ID],
|
|
920
|
+
monitorStatusId: OFFLINE_STATUS_ID,
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
expect(rollupSpy).toHaveBeenCalledTimes(1);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it("still stamps devices that have no site (rollup skipped)", async () => {
|
|
927
|
+
jest
|
|
928
|
+
.spyOn(MonitorService, "findBy")
|
|
929
|
+
.mockResolvedValue([fakeNetworkDeviceMonitor([DEVICE_ID.toString()])]);
|
|
930
|
+
jest
|
|
931
|
+
.spyOn(NetworkDeviceService, "findBy")
|
|
932
|
+
.mockResolvedValue([
|
|
933
|
+
{ id: DEVICE_ID },
|
|
934
|
+
] as unknown as Array<NetworkDevice>);
|
|
935
|
+
const stampSpy: jest.SpyInstance = jest
|
|
936
|
+
.spyOn(NetworkDeviceService, "updateColumnsByIdWithoutHooks")
|
|
937
|
+
.mockResolvedValue(undefined as never);
|
|
938
|
+
const rollupSpy: jest.SpyInstance = jest.spyOn(
|
|
939
|
+
NetworkSiteService,
|
|
940
|
+
"recomputeRollupForSiteAndAncestors",
|
|
941
|
+
);
|
|
942
|
+
|
|
943
|
+
await NetworkSiteService.onMonitorStatusChanged({
|
|
944
|
+
projectId: PROJECT_ID,
|
|
945
|
+
monitorIds: [MONITOR_ID],
|
|
946
|
+
monitorStatusId: OFFLINE_STATUS_ID,
|
|
947
|
+
});
|
|
948
|
+
|
|
949
|
+
expect(stampSpy).toHaveBeenCalledTimes(1);
|
|
950
|
+
expect(rollupSpy).not.toHaveBeenCalled();
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
it("does nothing when the monitors are not NetworkDevice monitors", async () => {
|
|
954
|
+
jest.spyOn(MonitorService, "findBy").mockResolvedValue([]);
|
|
955
|
+
const deviceFindBySpy: jest.SpyInstance = jest.spyOn(
|
|
956
|
+
NetworkDeviceService,
|
|
957
|
+
"findBy",
|
|
958
|
+
);
|
|
959
|
+
|
|
960
|
+
await NetworkSiteService.onMonitorStatusChanged({
|
|
961
|
+
projectId: PROJECT_ID,
|
|
962
|
+
monitorIds: [MONITOR_ID],
|
|
963
|
+
monitorStatusId: OFFLINE_STATUS_ID,
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
expect(deviceFindBySpy).not.toHaveBeenCalled();
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
it("NEVER throws - a failing lookup is logged, not propagated", async () => {
|
|
970
|
+
jest
|
|
971
|
+
.spyOn(MonitorService, "findBy")
|
|
972
|
+
.mockRejectedValue(new Error("database is down"));
|
|
973
|
+
|
|
974
|
+
await expect(
|
|
975
|
+
NetworkSiteService.onMonitorStatusChanged({
|
|
976
|
+
projectId: PROJECT_ID,
|
|
977
|
+
monitorIds: [MONITOR_ID],
|
|
978
|
+
monitorStatusId: OFFLINE_STATUS_ID,
|
|
979
|
+
}),
|
|
980
|
+
).resolves.toBeUndefined();
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
/*
|
|
985
|
+
* The REAL path builder (not the spy the rest of this file installs). It is
|
|
986
|
+
* the single source of truth for getAncestorIds, getDescendantSiteIds and the
|
|
987
|
+
* cycle guard, and the only thing that repairs a row whose path write failed
|
|
988
|
+
* or whose ancestor was deleted out from under it.
|
|
989
|
+
*/
|
|
990
|
+
describe("NetworkSiteService.getMaterializedPathForSite", () => {
|
|
991
|
+
afterEach(() => {
|
|
992
|
+
jest.restoreAllMocks();
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
function stubRows(rows: Array<NetworkSite>): jest.SpyInstance {
|
|
996
|
+
const byId: Map<string, NetworkSite> = new Map(
|
|
997
|
+
rows.map((row: NetworkSite) => {
|
|
998
|
+
return [row.id!.toString(), row];
|
|
999
|
+
}),
|
|
1000
|
+
);
|
|
1001
|
+
|
|
1002
|
+
jest
|
|
1003
|
+
.spyOn(NetworkSiteService, "findOneById")
|
|
1004
|
+
.mockImplementation((input: any) => {
|
|
1005
|
+
return Promise.resolve(byId.get(input.id.toString()) || null);
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
return jest
|
|
1009
|
+
.spyOn(NetworkSiteService, "updateColumnsByIdWithoutHooks")
|
|
1010
|
+
.mockResolvedValue(undefined as never);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
it("returns a stored path that agrees with parentSiteId, writing nothing", async () => {
|
|
1014
|
+
const storedPath: string = `/${PARENT_SITE_ID.toString()}/${SITE_ID.toString()}/`;
|
|
1015
|
+
const updateColumns: jest.SpyInstance = stubRows([
|
|
1016
|
+
fakeSite({
|
|
1017
|
+
parentSiteId: PARENT_SITE_ID,
|
|
1018
|
+
materializedPath: storedPath,
|
|
1019
|
+
}),
|
|
1020
|
+
]);
|
|
1021
|
+
|
|
1022
|
+
expect(await NetworkSiteService.getMaterializedPathForSite(SITE_ID)).toBe(
|
|
1023
|
+
storedPath,
|
|
1024
|
+
);
|
|
1025
|
+
expect(updateColumns).not.toHaveBeenCalled();
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
it("rebuilds and persists a null path from the parent's stored path", async () => {
|
|
1029
|
+
const updateColumns: jest.SpyInstance = stubRows([
|
|
1030
|
+
fakeSite({
|
|
1031
|
+
parentSiteId: PARENT_SITE_ID,
|
|
1032
|
+
materializedPath: undefined,
|
|
1033
|
+
}),
|
|
1034
|
+
fakeSite({
|
|
1035
|
+
id: PARENT_SITE_ID,
|
|
1036
|
+
_id: PARENT_SITE_ID.toString(),
|
|
1037
|
+
materializedPath: `/${PARENT_SITE_ID.toString()}/`,
|
|
1038
|
+
}),
|
|
1039
|
+
]);
|
|
1040
|
+
|
|
1041
|
+
const expectedPath: string = `/${PARENT_SITE_ID.toString()}/${SITE_ID.toString()}/`;
|
|
1042
|
+
|
|
1043
|
+
expect(await NetworkSiteService.getMaterializedPathForSite(SITE_ID)).toBe(
|
|
1044
|
+
expectedPath,
|
|
1045
|
+
);
|
|
1046
|
+
|
|
1047
|
+
expect(updateColumns).toHaveBeenCalledTimes(1);
|
|
1048
|
+
const args: any = updateColumns.mock.calls[0]![0];
|
|
1049
|
+
expect(args.id.toString()).toBe(SITE_ID.toString());
|
|
1050
|
+
expect(args.data.materializedPath).toBe(expectedPath);
|
|
1051
|
+
expect(args.data.depth).toBe(1);
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
it("heals a whole null-path chain top-down", async () => {
|
|
1055
|
+
const rootId: ObjectID = new ObjectID(
|
|
1056
|
+
"dddddddd-dddd-4ddd-8ddd-dddddddddddd",
|
|
1057
|
+
);
|
|
1058
|
+
const updateColumns: jest.SpyInstance = stubRows([
|
|
1059
|
+
fakeSite({
|
|
1060
|
+
parentSiteId: PARENT_SITE_ID,
|
|
1061
|
+
materializedPath: undefined,
|
|
1062
|
+
}),
|
|
1063
|
+
fakeSite({
|
|
1064
|
+
id: PARENT_SITE_ID,
|
|
1065
|
+
_id: PARENT_SITE_ID.toString(),
|
|
1066
|
+
parentSiteId: rootId,
|
|
1067
|
+
materializedPath: undefined,
|
|
1068
|
+
}),
|
|
1069
|
+
fakeSite({
|
|
1070
|
+
id: rootId,
|
|
1071
|
+
_id: rootId.toString(),
|
|
1072
|
+
materializedPath: `/${rootId.toString()}/`,
|
|
1073
|
+
}),
|
|
1074
|
+
]);
|
|
1075
|
+
|
|
1076
|
+
const expectedPath: string = `/${rootId.toString()}/${PARENT_SITE_ID.toString()}/${SITE_ID.toString()}/`;
|
|
1077
|
+
|
|
1078
|
+
expect(await NetworkSiteService.getMaterializedPathForSite(SITE_ID)).toBe(
|
|
1079
|
+
expectedPath,
|
|
1080
|
+
);
|
|
1081
|
+
|
|
1082
|
+
// Persisted root-ward first, so each child builds on a healed parent.
|
|
1083
|
+
const writes: Array<Array<unknown>> = updateColumns.mock.calls.map(
|
|
1084
|
+
(call: Array<any>) => {
|
|
1085
|
+
return [
|
|
1086
|
+
call[0].id.toString(),
|
|
1087
|
+
call[0].data.materializedPath,
|
|
1088
|
+
call[0].data.depth,
|
|
1089
|
+
];
|
|
1090
|
+
},
|
|
1091
|
+
);
|
|
1092
|
+
expect(writes).toEqual([
|
|
1093
|
+
[
|
|
1094
|
+
PARENT_SITE_ID.toString(),
|
|
1095
|
+
`/${rootId.toString()}/${PARENT_SITE_ID.toString()}/`,
|
|
1096
|
+
1,
|
|
1097
|
+
],
|
|
1098
|
+
[SITE_ID.toString(), expectedPath, 2],
|
|
1099
|
+
]);
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
/*
|
|
1103
|
+
* A deleted mid-tree ancestor leaves parentSiteId NULL (FK SET NULL) while
|
|
1104
|
+
* the stored path still routes through the dead row. That desync must not
|
|
1105
|
+
* survive - a stale path is as untrustworthy as a missing one.
|
|
1106
|
+
*/
|
|
1107
|
+
it("self-heals a stale path stranded by a deleted ancestor", async () => {
|
|
1108
|
+
const deletedId: ObjectID = new ObjectID(
|
|
1109
|
+
"ffffffff-ffff-4fff-8fff-ffffffffffff",
|
|
1110
|
+
);
|
|
1111
|
+
const updateColumns: jest.SpyInstance = stubRows([
|
|
1112
|
+
fakeSite({
|
|
1113
|
+
parentSiteId: undefined,
|
|
1114
|
+
materializedPath: `/${deletedId.toString()}/${SITE_ID.toString()}/`,
|
|
1115
|
+
}),
|
|
1116
|
+
]);
|
|
1117
|
+
|
|
1118
|
+
expect(await NetworkSiteService.getMaterializedPathForSite(SITE_ID)).toBe(
|
|
1119
|
+
`/${SITE_ID.toString()}/`,
|
|
1120
|
+
);
|
|
1121
|
+
|
|
1122
|
+
expect(updateColumns).toHaveBeenCalledTimes(1);
|
|
1123
|
+
const args: any = updateColumns.mock.calls[0]![0];
|
|
1124
|
+
expect(args.data.materializedPath).toBe(`/${SITE_ID.toString()}/`);
|
|
1125
|
+
expect(args.data.depth).toBe(0);
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
it("terminates on a corrupted parent cycle instead of looping forever", async () => {
|
|
1129
|
+
stubRows([
|
|
1130
|
+
fakeSite({
|
|
1131
|
+
parentSiteId: PARENT_SITE_ID,
|
|
1132
|
+
materializedPath: undefined,
|
|
1133
|
+
}),
|
|
1134
|
+
fakeSite({
|
|
1135
|
+
id: PARENT_SITE_ID,
|
|
1136
|
+
_id: PARENT_SITE_ID.toString(),
|
|
1137
|
+
parentSiteId: SITE_ID,
|
|
1138
|
+
materializedPath: undefined,
|
|
1139
|
+
}),
|
|
1140
|
+
]);
|
|
1141
|
+
|
|
1142
|
+
// The cycle is broken by treating the topmost visited node as a root.
|
|
1143
|
+
expect(await NetworkSiteService.getMaterializedPathForSite(SITE_ID)).toBe(
|
|
1144
|
+
`/${PARENT_SITE_ID.toString()}/${SITE_ID.toString()}/`,
|
|
1145
|
+
);
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
it("returns null when the site does not exist", async () => {
|
|
1149
|
+
stubRows([]);
|
|
1150
|
+
|
|
1151
|
+
expect(
|
|
1152
|
+
await NetworkSiteService.getMaterializedPathForSite(SITE_ID),
|
|
1153
|
+
).toBeNull();
|
|
1154
|
+
});
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
/*
|
|
1158
|
+
* parentSiteId is onDelete: "SET NULL", so deleting a mid-tree site detaches
|
|
1159
|
+
* its children but strands their materializedPath through the dead row -
|
|
1160
|
+
* leaving the children endpoint (parentSiteId) and the rollup engine
|
|
1161
|
+
* (materializedPath) disagreeing, and double-counting the subtree's outages.
|
|
1162
|
+
*/
|
|
1163
|
+
describe("NetworkSiteService delete hooks (orphan repair)", () => {
|
|
1164
|
+
afterEach(() => {
|
|
1165
|
+
jest.restoreAllMocks();
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
const REGION_ID: ObjectID = new ObjectID(
|
|
1169
|
+
"dddddddd-dddd-4ddd-8ddd-dddddddddddd",
|
|
1170
|
+
);
|
|
1171
|
+
const DISTRICT_ID: ObjectID = SITE_ID;
|
|
1172
|
+
const STORE_ID: ObjectID = new ObjectID(
|
|
1173
|
+
"eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee",
|
|
1174
|
+
);
|
|
1175
|
+
const UNIT_ID: ObjectID = new ObjectID(
|
|
1176
|
+
"ffffffff-ffff-4fff-8fff-ffffffffffff",
|
|
1177
|
+
);
|
|
1178
|
+
|
|
1179
|
+
const DISTRICT_PATH: string = `/${REGION_ID.toString()}/${DISTRICT_ID.toString()}/`;
|
|
1180
|
+
const STORE_PATH: string = `${DISTRICT_PATH}${STORE_ID.toString()}/`;
|
|
1181
|
+
const UNIT_PATH: string = `${STORE_PATH}${UNIT_ID.toString()}/`;
|
|
1182
|
+
|
|
1183
|
+
function deletedDistrict(): NetworkSite {
|
|
1184
|
+
return fakeSite({
|
|
1185
|
+
id: DISTRICT_ID,
|
|
1186
|
+
_id: DISTRICT_ID.toString(),
|
|
1187
|
+
parentSiteId: REGION_ID,
|
|
1188
|
+
materializedPath: DISTRICT_PATH,
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
it("onBeforeDelete scopes the pre-delete read to the caller's project", async () => {
|
|
1193
|
+
const findBySpy: jest.SpyInstance = jest
|
|
1194
|
+
.spyOn(NetworkSiteService, "findBy")
|
|
1195
|
+
.mockResolvedValue([]);
|
|
1196
|
+
|
|
1197
|
+
const result: OnDelete<NetworkSite> = await (
|
|
1198
|
+
NetworkSiteService as any
|
|
1199
|
+
).onBeforeDelete({
|
|
1200
|
+
query: { _id: DISTRICT_ID.toString() },
|
|
1201
|
+
props: { tenantId: PROJECT_ID },
|
|
1202
|
+
} as unknown as DeleteBy<NetworkSite>);
|
|
1203
|
+
|
|
1204
|
+
const query: any = findBySpy.mock.calls[0]![0].query;
|
|
1205
|
+
expect(query.projectId.toString()).toBe(PROJECT_ID.toString());
|
|
1206
|
+
expect(result.carryForward.sitesToDelete).toEqual([]);
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
it("reparents the direct child and rebases the whole subtree", async () => {
|
|
1210
|
+
jest
|
|
1211
|
+
.spyOn(NetworkSiteService, "getMaterializedPathForSite")
|
|
1212
|
+
.mockResolvedValue(`/${REGION_ID.toString()}/`);
|
|
1213
|
+
jest.spyOn(NetworkSiteService, "findBy").mockResolvedValue([
|
|
1214
|
+
fakeSite({
|
|
1215
|
+
id: STORE_ID,
|
|
1216
|
+
_id: STORE_ID.toString(),
|
|
1217
|
+
materializedPath: STORE_PATH,
|
|
1218
|
+
}),
|
|
1219
|
+
fakeSite({
|
|
1220
|
+
id: UNIT_ID,
|
|
1221
|
+
_id: UNIT_ID.toString(),
|
|
1222
|
+
materializedPath: UNIT_PATH,
|
|
1223
|
+
}),
|
|
1224
|
+
]);
|
|
1225
|
+
const updateColumns: jest.SpyInstance = jest
|
|
1226
|
+
.spyOn(NetworkSiteService, "updateColumnsByIdWithoutHooks")
|
|
1227
|
+
.mockResolvedValue(undefined as never);
|
|
1228
|
+
const rollupSpy: jest.SpyInstance = jest
|
|
1229
|
+
.spyOn(NetworkSiteService, "recomputeRollupForSiteAndAncestors")
|
|
1230
|
+
.mockResolvedValue(undefined as never);
|
|
1231
|
+
|
|
1232
|
+
await (NetworkSiteService as any).onDeleteSuccess(
|
|
1233
|
+
{
|
|
1234
|
+
deleteBy: {
|
|
1235
|
+
query: { _id: DISTRICT_ID.toString() },
|
|
1236
|
+
props: { tenantId: PROJECT_ID },
|
|
1237
|
+
},
|
|
1238
|
+
carryForward: { sitesToDelete: [deletedDistrict()] },
|
|
1239
|
+
},
|
|
1240
|
+
[DISTRICT_ID],
|
|
1241
|
+
);
|
|
1242
|
+
|
|
1243
|
+
// The direct child is re-attached to the deleted site's own parent...
|
|
1244
|
+
const storeWrite: any = updateColumns.mock.calls.find((call: any) => {
|
|
1245
|
+
return call[0].id.toString() === STORE_ID.toString();
|
|
1246
|
+
})![0];
|
|
1247
|
+
expect(storeWrite.data.materializedPath).toBe(
|
|
1248
|
+
`/${REGION_ID.toString()}/${STORE_ID.toString()}/`,
|
|
1249
|
+
);
|
|
1250
|
+
expect(storeWrite.data.depth).toBe(1);
|
|
1251
|
+
expect(storeWrite.data.parentSiteId.toString()).toBe(REGION_ID.toString());
|
|
1252
|
+
|
|
1253
|
+
// ...and deeper descendants only lose the deleted segment.
|
|
1254
|
+
const unitWrite: any = updateColumns.mock.calls.find((call: any) => {
|
|
1255
|
+
return call[0].id.toString() === UNIT_ID.toString();
|
|
1256
|
+
})![0];
|
|
1257
|
+
expect(unitWrite.data.materializedPath).toBe(
|
|
1258
|
+
`/${REGION_ID.toString()}/${STORE_ID.toString()}/${UNIT_ID.toString()}/`,
|
|
1259
|
+
);
|
|
1260
|
+
expect(unitWrite.data.depth).toBe(2);
|
|
1261
|
+
expect(unitWrite.data.parentSiteId).toBeUndefined();
|
|
1262
|
+
|
|
1263
|
+
// The surviving ancestor chain's rollup is refreshed.
|
|
1264
|
+
expect(rollupSpy).toHaveBeenCalledTimes(1);
|
|
1265
|
+
expect(rollupSpy.mock.calls[0]![0].toString()).toBe(REGION_ID.toString());
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
it("promotes children to roots when the deleted site was a root", async () => {
|
|
1269
|
+
const rootPath: string = `/${DISTRICT_ID.toString()}/`;
|
|
1270
|
+
jest.spyOn(NetworkSiteService, "findBy").mockResolvedValue([
|
|
1271
|
+
fakeSite({
|
|
1272
|
+
id: STORE_ID,
|
|
1273
|
+
_id: STORE_ID.toString(),
|
|
1274
|
+
materializedPath: `${rootPath}${STORE_ID.toString()}/`,
|
|
1275
|
+
}),
|
|
1276
|
+
]);
|
|
1277
|
+
const updateColumns: jest.SpyInstance = jest
|
|
1278
|
+
.spyOn(NetworkSiteService, "updateColumnsByIdWithoutHooks")
|
|
1279
|
+
.mockResolvedValue(undefined as never);
|
|
1280
|
+
const rollupSpy: jest.SpyInstance = jest.spyOn(
|
|
1281
|
+
NetworkSiteService,
|
|
1282
|
+
"recomputeRollupForSiteAndAncestors",
|
|
1283
|
+
);
|
|
1284
|
+
|
|
1285
|
+
await (NetworkSiteService as any).onDeleteSuccess(
|
|
1286
|
+
{
|
|
1287
|
+
deleteBy: {
|
|
1288
|
+
query: {},
|
|
1289
|
+
props: { tenantId: PROJECT_ID },
|
|
1290
|
+
},
|
|
1291
|
+
carryForward: {
|
|
1292
|
+
sitesToDelete: [
|
|
1293
|
+
fakeSite({
|
|
1294
|
+
id: DISTRICT_ID,
|
|
1295
|
+
_id: DISTRICT_ID.toString(),
|
|
1296
|
+
materializedPath: rootPath,
|
|
1297
|
+
}),
|
|
1298
|
+
],
|
|
1299
|
+
},
|
|
1300
|
+
},
|
|
1301
|
+
[DISTRICT_ID],
|
|
1302
|
+
);
|
|
1303
|
+
|
|
1304
|
+
const storeWrite: any = updateColumns.mock.calls[0]![0];
|
|
1305
|
+
expect(storeWrite.data.materializedPath).toBe(`/${STORE_ID.toString()}/`);
|
|
1306
|
+
expect(storeWrite.data.depth).toBe(0);
|
|
1307
|
+
expect(storeWrite.data.parentSiteId).toBeNull();
|
|
1308
|
+
expect(rollupSpy).not.toHaveBeenCalled();
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
it("repairs nothing for sites the permission-checked delete did not remove", async () => {
|
|
1312
|
+
const findBySpy: jest.SpyInstance = jest.spyOn(
|
|
1313
|
+
NetworkSiteService,
|
|
1314
|
+
"findBy",
|
|
1315
|
+
);
|
|
1316
|
+
const updateColumns: jest.SpyInstance = jest.spyOn(
|
|
1317
|
+
NetworkSiteService,
|
|
1318
|
+
"updateColumnsByIdWithoutHooks",
|
|
1319
|
+
);
|
|
1320
|
+
|
|
1321
|
+
await (NetworkSiteService as any).onDeleteSuccess(
|
|
1322
|
+
{
|
|
1323
|
+
deleteBy: {
|
|
1324
|
+
query: { _id: DISTRICT_ID.toString() },
|
|
1325
|
+
props: { tenantId: PROJECT_ID },
|
|
1326
|
+
},
|
|
1327
|
+
carryForward: { sitesToDelete: [deletedDistrict()] },
|
|
1328
|
+
},
|
|
1329
|
+
[], // nothing was actually deleted
|
|
1330
|
+
);
|
|
1331
|
+
|
|
1332
|
+
expect(findBySpy).not.toHaveBeenCalled();
|
|
1333
|
+
expect(updateColumns).not.toHaveBeenCalled();
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
it("NEVER throws - a repair failure is logged, not propagated", async () => {
|
|
1337
|
+
jest
|
|
1338
|
+
.spyOn(NetworkSiteService, "getMaterializedPathForSite")
|
|
1339
|
+
.mockRejectedValue(new Error("database is down"));
|
|
1340
|
+
|
|
1341
|
+
await expect(
|
|
1342
|
+
(NetworkSiteService as any).onDeleteSuccess(
|
|
1343
|
+
{
|
|
1344
|
+
deleteBy: {
|
|
1345
|
+
query: { _id: DISTRICT_ID.toString() },
|
|
1346
|
+
props: { tenantId: PROJECT_ID },
|
|
1347
|
+
},
|
|
1348
|
+
carryForward: { sitesToDelete: [deletedDistrict()] },
|
|
1349
|
+
},
|
|
1350
|
+
[DISTRICT_ID],
|
|
1351
|
+
),
|
|
1352
|
+
).resolves.toBeDefined();
|
|
1353
|
+
});
|
|
1354
|
+
});
|