@oneuptime/common 12.0.9 → 12.0.11
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/NetworkDevice.ts +44 -0
- package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
- package/Models/DatabaseModels/RunnerJob.ts +17 -2
- package/Server/API/LlmProviderAPI.ts +8 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +240 -0
- package/Server/Services/NetworkDeviceLinkService.ts +338 -2
- package/Server/Services/NetworkDeviceService.ts +265 -12
- package/Server/Services/RunnerJobService.ts +24 -1
- package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
- package/Server/Types/Database/JSONColumnQuery.ts +42 -4
- package/Server/Utils/LLM/LLMService.ts +108 -19
- package/Server/Utils/Monitor/MonitorAlert.ts +22 -115
- package/Server/Utils/Monitor/MonitorIncident.ts +7 -337
- package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +27 -2
- package/Server/Utils/Monitor/SeriesResourceLabels.ts +32 -0
- package/Server/Utils/Monitor/SeriesResourceLinker.ts +469 -0
- package/Server/Utils/SSRFProtection.ts +5 -4
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
- package/Tests/App/AdminDashboard/AdminUsersBulkAddToProject.test.tsx +955 -0
- package/Tests/App/AdminDashboard/BulkAddUsersToProject.test.ts +778 -0
- package/Tests/App/AdminDashboard/BulkAddUsersToProjectModal.test.tsx +1051 -0
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +81 -6
- package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
- package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
- package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
- package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
- package/Tests/App/Dashboard/UserElementEmail.test.tsx +447 -0
- package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +284 -0
- package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
- package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
- package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
- package/Tests/Server/Services/NetworkDeviceLabelRuleRun.test.ts +618 -0
- package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
- package/Tests/Server/Services/NetworkSiteAssignmentRuleRun.test.ts +706 -0
- package/Tests/Server/Services/RunnerJobEnqueue.test.ts +412 -0
- package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
- package/Tests/Server/Utils/AI/LLMServiceRequestPolicy.test.ts +2 -2
- package/Tests/Server/Utils/AI/LLMServiceRetryPolicy.test.ts +450 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +556 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +547 -0
- package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +30 -0
- package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +699 -0
- package/Tests/Server/Utils/SeriesResourceLabels.test.ts +55 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
- package/Tests/Types/API/URLMalformedTolerance.test.ts +301 -0
- package/Tests/Types/API/URLTelScheme.test.ts +261 -0
- package/Tests/Types/API/URLUnsupportedScheme.test.ts +148 -0
- package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
- package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
- package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
- package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +184 -0
- package/Tests/Types/Runbook/RunbookStepType.test.ts +124 -0
- package/Tests/Types/Workflow/BaseModelComponents.test.ts +17 -0
- package/Tests/UI/Components/Button.test.tsx +279 -0
- package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
- package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
- package/Tests/UI/Components/DictionaryValuePlaceholderAndSuggestions.test.tsx +595 -0
- package/Tests/UI/Components/Icon.test.tsx +57 -0
- package/Tests/UI/Components/List.test.tsx +20 -3
- package/Tests/UI/Components/LogsSavedViewsDropdown.test.tsx +304 -0
- package/Tests/UI/Components/LogsViewerClearSavedView.test.tsx +179 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
- package/Tests/UI/Components/Pagination.test.tsx +721 -146
- package/Tests/UI/Components/PaginationIntegration.test.tsx +386 -0
- package/Tests/UI/Components/PaginationUtil.test.ts +417 -0
- package/Tests/UI/Components/TelemetrySavedViewsDropdown.test.tsx +343 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnControl.test.ts +289 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnOperators.test.ts +271 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRow.test.ts +119 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.test.ts +968 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditor.render.test.tsx +766 -0
- package/Tests/UI/Utils/RunNetworkRule.test.ts +320 -0
- package/Tests/Utils/APIRetryPolicy.test.ts +823 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
- package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
- package/Tests/Utils/NetworkAutomation/RuleRunSummary.test.ts +286 -0
- package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
- package/Types/API/Protocol.ts +9 -0
- package/Types/API/URL.ts +286 -20
- package/Types/Billing/PayAsYouGoPricing.ts +47 -0
- package/Types/CustomField/CustomFieldType.ts +28 -0
- package/Types/Monitor/MonitorType.ts +11 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
- package/Types/NetworkAutomation/RuleRunResult.ts +121 -0
- package/Types/Runbook/RunbookStepType.ts +23 -0
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -0
- package/UI/Components/Button/Button.tsx +14 -22
- package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
- package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
- package/UI/Components/Charts/Line/LineChart.tsx +4 -2
- package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
- package/UI/Components/Dictionary/Dictionary.tsx +4 -39
- package/UI/Components/Icon/Icon.tsx +1 -1
- package/UI/Components/LogsViewer/LogsViewer.tsx +5 -1
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +8 -0
- package/UI/Components/LogsViewer/components/LogsPagination.tsx +29 -91
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +2 -0
- package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +66 -1
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
- package/UI/Components/Pagination/Pagination.tsx +373 -328
- package/UI/Components/Pagination/PaginationUtil.ts +263 -0
- package/UI/Components/Pagination/Screenshots/README.md +15 -0
- package/UI/Components/Pagination/Screenshots/pagination-before-after.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-go-to-page-modal.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-long-list.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-mobile.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-telemetry.png +0 -0
- package/UI/Components/TelemetryViewer/components/SavedViewsDropdown.tsx +66 -1
- package/UI/Components/TelemetryViewer/components/TelemetryPagination.tsx +38 -92
- package/UI/Components/TextArea/TextArea.tsx +3 -0
- package/UI/Components/Workflow/ArgumentsForm.tsx +18 -1
- package/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.tsx +177 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.tsx +243 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnControl.ts +255 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.tsx +178 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnOperators.ts +206 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnRow.ts +157 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.ts +517 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnValueInput.tsx +482 -0
- package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +131 -0
- package/UI/Components/Workflow/ColumnEditor/ModelRecordForm.tsx +163 -0
- package/UI/Components/Workflow/ModelColumnEditor.tsx +284 -100
- package/UI/Utils/NetworkAutomation/RunNetworkRule.ts +122 -0
- package/Utils/API.ts +243 -36
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
- package/Utils/NetworkAutomation/RuleRunSummary.ts +147 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RunnerJob.js +17 -2
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
- package/build/dist/Server/API/LlmProviderAPI.js +9 -1
- package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +202 -0
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js +253 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +223 -12
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/RunnerJobService.js +20 -2
- package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
- package/build/dist/Server/Utils/LLM/LLMService.js +78 -22
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -98
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +7 -254
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +14 -2
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +27 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +285 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -0
- package/build/dist/Server/Utils/SSRFProtection.js +5 -4
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- package/build/dist/Types/API/Protocol.js +9 -0
- package/build/dist/Types/API/Protocol.js.map +1 -1
- package/build/dist/Types/API/URL.js +241 -18
- package/build/dist/Types/API/URL.js.map +1 -1
- package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
- package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +10 -0
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +39 -0
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -0
- package/build/dist/Types/Runbook/RunbookStepType.js +21 -0
- package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/Button/Button.js +10 -16
- package/build/dist/UI/Components/Button/Button.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +4 -18
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +1 -1
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +3 -3
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +5 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +14 -44
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +20 -2
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/Pagination/Pagination.js +174 -199
- package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
- package/build/dist/UI/Components/Pagination/PaginationUtil.js +160 -0
- package/build/dist/UI/Components/Pagination/PaginationUtil.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js +20 -2
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js +23 -47
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js.map +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +14 -2
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js +86 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js +119 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js +206 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js +57 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js +150 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js +67 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js +309 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js +204 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +66 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js +83 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +187 -68
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -1
- package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js +75 -0
- package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js.map +1 -0
- package/build/dist/Utils/API.js +175 -34
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js +78 -0
- package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
import NetworkDeviceService from "../../../Server/Services/NetworkDeviceService";
|
|
2
|
+
import NetworkSiteAssignmentRuleService from "../../../Server/Services/NetworkSiteAssignmentRuleService";
|
|
3
|
+
import NetworkDevice from "../../../Models/DatabaseModels/NetworkDevice";
|
|
4
|
+
import NetworkSiteAssignmentRule from "../../../Models/DatabaseModels/NetworkSiteAssignmentRule";
|
|
5
|
+
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
|
6
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
7
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
8
|
+
import { SiteAssignmentRuleRunResult } from "../../../Types/NetworkAutomation/RuleRunResult";
|
|
9
|
+
import { describe, expect, it, afterEach } from "@jest/globals";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* Contract under test - "Run now" for a site assignment rule
|
|
13
|
+
* (OneUptime/oneuptime#3191).
|
|
14
|
+
*
|
|
15
|
+
* Rules only fire on device create, on an identity change, or on the next
|
|
16
|
+
* poll of a device with no site, so a rule written after an estate was
|
|
17
|
+
* imported never reaches it. applySiteAssignmentRuleToExistingDevices is the
|
|
18
|
+
* manual path that does, and it keeps two promises of the automatic one:
|
|
19
|
+
*
|
|
20
|
+
* - priority still decides. A device this rule matches but a
|
|
21
|
+
* higher-priority rule also matches is reported, never moved: one rule's
|
|
22
|
+
* button must not do another rule's work.
|
|
23
|
+
* - a device that already belongs to a site is left alone unless the caller
|
|
24
|
+
* explicitly asked to overwrite, because nothing records whether that
|
|
25
|
+
* site was chosen by a rule or by a person.
|
|
26
|
+
*
|
|
27
|
+
* Everything else here is about being honest with the operator: the counters
|
|
28
|
+
* have to add up, one device's failure must not abandon the rest of the
|
|
29
|
+
* fleet, and a run that stopped at the cap has to say so.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
33
|
+
"22222222-2222-4222-8222-222222222222",
|
|
34
|
+
);
|
|
35
|
+
const OTHER_PROJECT_ID: ObjectID = new ObjectID(
|
|
36
|
+
"99999999-9999-4999-8999-999999999999",
|
|
37
|
+
);
|
|
38
|
+
const SITE_A_ID: ObjectID = new ObjectID(
|
|
39
|
+
"11111111-1111-4111-8111-111111111111",
|
|
40
|
+
);
|
|
41
|
+
const SITE_B_ID: ObjectID = new ObjectID(
|
|
42
|
+
"33333333-3333-4333-8333-333333333333",
|
|
43
|
+
);
|
|
44
|
+
const RULE_ID: ObjectID = new ObjectID("44444444-4444-4444-8444-444444444444");
|
|
45
|
+
const OTHER_RULE_ID: ObjectID = new ObjectID(
|
|
46
|
+
"55555555-5555-4555-8555-555555555555",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
function deviceId(index: number): ObjectID {
|
|
50
|
+
/*
|
|
51
|
+
* Deterministic per-index ids so a paging test can assert exactly which
|
|
52
|
+
* devices were touched without hard-coding ten thousand UUIDs.
|
|
53
|
+
*/
|
|
54
|
+
const suffix: string = index.toString(16).padStart(12, "0");
|
|
55
|
+
return new ObjectID(`00000000-0000-4000-8000-${suffix}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function fakeDevice(data: {
|
|
59
|
+
index?: number | undefined;
|
|
60
|
+
id?: ObjectID | undefined;
|
|
61
|
+
siteId?: ObjectID | undefined;
|
|
62
|
+
hostname?: string | undefined;
|
|
63
|
+
sysName?: string | undefined;
|
|
64
|
+
name?: string | undefined;
|
|
65
|
+
}): NetworkDevice {
|
|
66
|
+
const id: ObjectID = data.id || deviceId(data.index ?? 0);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
id: id,
|
|
70
|
+
_id: id.toString(),
|
|
71
|
+
projectId: PROJECT_ID,
|
|
72
|
+
siteId: data.siteId,
|
|
73
|
+
hostname: data.hostname,
|
|
74
|
+
sysName: data.sysName,
|
|
75
|
+
name: data.name,
|
|
76
|
+
} as unknown as NetworkDevice;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function fakeRule(data: {
|
|
80
|
+
id?: ObjectID | undefined;
|
|
81
|
+
// null means "a rule row with no site", which is a state the run rejects.
|
|
82
|
+
siteId?: ObjectID | null | undefined;
|
|
83
|
+
subnetCidr?: string | undefined;
|
|
84
|
+
hostnamePattern?: string | undefined;
|
|
85
|
+
priority?: number | undefined;
|
|
86
|
+
createdAt?: Date | undefined;
|
|
87
|
+
}): NetworkSiteAssignmentRule {
|
|
88
|
+
const id: ObjectID = data.id || RULE_ID;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
id: id,
|
|
92
|
+
_id: id.toString(),
|
|
93
|
+
projectId: PROJECT_ID,
|
|
94
|
+
siteId: data.siteId === undefined ? SITE_A_ID : data.siteId || undefined,
|
|
95
|
+
subnetCidr: data.subnetCidr,
|
|
96
|
+
hostnamePattern: data.hostnamePattern,
|
|
97
|
+
priority: data.priority ?? 0,
|
|
98
|
+
createdAt: data.createdAt,
|
|
99
|
+
} as unknown as NetworkSiteAssignmentRule;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function mockRules(rules: Array<NetworkSiteAssignmentRule>): jest.SpyInstance {
|
|
103
|
+
return jest
|
|
104
|
+
.spyOn(NetworkSiteAssignmentRuleService, "findBy")
|
|
105
|
+
.mockResolvedValue(rules);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// A findBy that pages over a fixed list, honouring skip and limit.
|
|
109
|
+
function mockDevices(devices: Array<NetworkDevice>): jest.SpyInstance {
|
|
110
|
+
return jest
|
|
111
|
+
.spyOn(NetworkDeviceService, "findBy")
|
|
112
|
+
.mockImplementation(async (data: any) => {
|
|
113
|
+
const skip: number = data.skip || 0;
|
|
114
|
+
const limit: number = data.limit || devices.length;
|
|
115
|
+
return devices.slice(skip, skip + limit) as any;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/*
|
|
120
|
+
* The same, but generated on demand: a truncation test needs more devices
|
|
121
|
+
* than it is worth building arrays for.
|
|
122
|
+
*/
|
|
123
|
+
function mockGeneratedDevices(totalDevices: number): jest.SpyInstance {
|
|
124
|
+
return jest
|
|
125
|
+
.spyOn(NetworkDeviceService, "findBy")
|
|
126
|
+
.mockImplementation(async (data: any) => {
|
|
127
|
+
const skip: number = data.skip || 0;
|
|
128
|
+
const limit: number = data.limit || 0;
|
|
129
|
+
const page: Array<NetworkDevice> = [];
|
|
130
|
+
|
|
131
|
+
for (
|
|
132
|
+
let index: number = skip;
|
|
133
|
+
index < Math.min(skip + limit, totalDevices);
|
|
134
|
+
index++
|
|
135
|
+
) {
|
|
136
|
+
page.push(fakeDevice({ index: index, hostname: "10.0.5.9" }));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return page as any;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function mockUpdate(): jest.SpyInstance {
|
|
144
|
+
return jest
|
|
145
|
+
.spyOn(NetworkDeviceService, "updateOneById")
|
|
146
|
+
.mockResolvedValue(undefined as never);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function run(
|
|
150
|
+
overrides: {
|
|
151
|
+
ruleId?: ObjectID | undefined;
|
|
152
|
+
projectId?: ObjectID | undefined;
|
|
153
|
+
reassignDevicesAlreadyInASite?: boolean | undefined;
|
|
154
|
+
} = {},
|
|
155
|
+
): Promise<SiteAssignmentRuleRunResult> {
|
|
156
|
+
return NetworkDeviceService.applySiteAssignmentRuleToExistingDevices({
|
|
157
|
+
ruleId: overrides.ruleId || RULE_ID,
|
|
158
|
+
projectId: overrides.projectId || PROJECT_ID,
|
|
159
|
+
reassignDevicesAlreadyInASite:
|
|
160
|
+
overrides.reassignDevicesAlreadyInASite ?? false,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
describe("NetworkDeviceService.applySiteAssignmentRuleToExistingDevices", () => {
|
|
165
|
+
afterEach(() => {
|
|
166
|
+
jest.restoreAllMocks();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("rule resolution", () => {
|
|
170
|
+
it("rejects a rule id that is not one of the project's rules", async () => {
|
|
171
|
+
mockRules([fakeRule({ subnetCidr: "10.0.0.0/8" })]);
|
|
172
|
+
mockDevices([]);
|
|
173
|
+
|
|
174
|
+
await expect(run({ ruleId: OTHER_RULE_ID })).rejects.toThrow(
|
|
175
|
+
BadDataException,
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
/*
|
|
180
|
+
* The tenant guard: the rules are loaded by project, so a rule id from
|
|
181
|
+
* another project is simply not in the list and the run refuses. Nothing
|
|
182
|
+
* about another tenant's estate is read or written.
|
|
183
|
+
*/
|
|
184
|
+
it("refuses to run a rule belonging to another project", async () => {
|
|
185
|
+
const rulesSpy: jest.SpyInstance = mockRules([]);
|
|
186
|
+
const devicesSpy: jest.SpyInstance = mockDevices([
|
|
187
|
+
fakeDevice({ hostname: "10.0.5.9" }),
|
|
188
|
+
]);
|
|
189
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
190
|
+
|
|
191
|
+
await expect(run({ projectId: OTHER_PROJECT_ID })).rejects.toThrow(
|
|
192
|
+
"Assignment rule not found.",
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
expect(rulesSpy.mock.calls[0]![0].query.projectId.toString()).toBe(
|
|
196
|
+
OTHER_PROJECT_ID.toString(),
|
|
197
|
+
);
|
|
198
|
+
expect(devicesSpy).not.toHaveBeenCalled();
|
|
199
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("rejects a rule that has no site to assign to", async () => {
|
|
203
|
+
mockRules([fakeRule({ siteId: null, subnetCidr: "10.0.0.0/8" })]);
|
|
204
|
+
mockDevices([]);
|
|
205
|
+
|
|
206
|
+
await expect(run()).rejects.toThrow(
|
|
207
|
+
"This assignment rule has no site to assign devices to.",
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("scopes both the rule and the device query to the project", async () => {
|
|
212
|
+
const rulesSpy: jest.SpyInstance = mockRules([
|
|
213
|
+
fakeRule({ subnetCidr: "10.0.0.0/8" }),
|
|
214
|
+
]);
|
|
215
|
+
const devicesSpy: jest.SpyInstance = mockDevices([]);
|
|
216
|
+
|
|
217
|
+
await run();
|
|
218
|
+
|
|
219
|
+
expect(rulesSpy.mock.calls[0]![0].query.projectId.toString()).toBe(
|
|
220
|
+
PROJECT_ID.toString(),
|
|
221
|
+
);
|
|
222
|
+
expect(devicesSpy.mock.calls[0]![0].query.projectId.toString()).toBe(
|
|
223
|
+
PROJECT_ID.toString(),
|
|
224
|
+
);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
/*
|
|
228
|
+
* Paging over rows the run is itself writing to is only safe under a
|
|
229
|
+
* stable sort. Ids never change when a site is assigned; a createdAt or
|
|
230
|
+
* name sort would let a row shift between pages and be skipped.
|
|
231
|
+
*/
|
|
232
|
+
it("pages devices under a stable id sort", async () => {
|
|
233
|
+
mockRules([fakeRule({ subnetCidr: "10.0.0.0/8" })]);
|
|
234
|
+
const devicesSpy: jest.SpyInstance = mockDevices([]);
|
|
235
|
+
|
|
236
|
+
await run();
|
|
237
|
+
|
|
238
|
+
expect(devicesSpy.mock.calls[0]![0].sort).toEqual({
|
|
239
|
+
_id: SortOrder.Ascending,
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe("matching", () => {
|
|
245
|
+
it("assigns a matching device that has no site", async () => {
|
|
246
|
+
mockRules([fakeRule({ subnetCidr: "10.0.5.0/24" })]);
|
|
247
|
+
mockDevices([fakeDevice({ hostname: "10.0.5.9" })]);
|
|
248
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
249
|
+
|
|
250
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
251
|
+
|
|
252
|
+
expect(result.devicesEvaluated).toBe(1);
|
|
253
|
+
expect(result.devicesMatched).toBe(1);
|
|
254
|
+
expect(result.devicesAssigned).toBe(1);
|
|
255
|
+
expect(updateSpy).toHaveBeenCalledTimes(1);
|
|
256
|
+
|
|
257
|
+
const args: any = updateSpy.mock.calls[0]![0];
|
|
258
|
+
expect(args.id.toString()).toBe(deviceId(0).toString());
|
|
259
|
+
expect(args.data.siteId.toString()).toBe(SITE_A_ID.toString());
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
/*
|
|
263
|
+
* Through updateOneById as root, never a bulk UPDATE: onUpdateSuccess is
|
|
264
|
+
* what refreshes the rollups of the site the device left and the one it
|
|
265
|
+
* joined, and a raw write would leave both stale.
|
|
266
|
+
*/
|
|
267
|
+
it("assigns through updateOneById so site rollups are refreshed", async () => {
|
|
268
|
+
mockRules([fakeRule({ subnetCidr: "10.0.5.0/24" })]);
|
|
269
|
+
mockDevices([fakeDevice({ hostname: "10.0.5.9" })]);
|
|
270
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
271
|
+
|
|
272
|
+
await run();
|
|
273
|
+
|
|
274
|
+
expect(updateSpy.mock.calls[0]![0].props).toEqual({ isRoot: true });
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("leaves a device no criterion matches alone", async () => {
|
|
278
|
+
mockRules([fakeRule({ subnetCidr: "10.0.5.0/24" })]);
|
|
279
|
+
mockDevices([fakeDevice({ hostname: "172.16.0.1" })]);
|
|
280
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
281
|
+
|
|
282
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
283
|
+
|
|
284
|
+
expect(result.devicesEvaluated).toBe(1);
|
|
285
|
+
expect(result.devicesMatched).toBe(0);
|
|
286
|
+
expect(result.devicesAssigned).toBe(0);
|
|
287
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
/*
|
|
291
|
+
* A discovery import stores the responding IP in `hostname` and the SNMP
|
|
292
|
+
* identity in `name` / `sysName`, so a retroactive run has to try a
|
|
293
|
+
* hostname pattern against all three - exactly as the per-device path
|
|
294
|
+
* does, or the button would disagree with the scan.
|
|
295
|
+
*/
|
|
296
|
+
it("matches a hostname pattern against the SNMP sysName", async () => {
|
|
297
|
+
mockRules([fakeRule({ hostnamePattern: "unit-1042-*" })]);
|
|
298
|
+
mockDevices([
|
|
299
|
+
fakeDevice({ hostname: "10.0.5.9", sysName: "unit-1042-core" }),
|
|
300
|
+
]);
|
|
301
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
302
|
+
|
|
303
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
304
|
+
|
|
305
|
+
expect(result.devicesAssigned).toBe(1);
|
|
306
|
+
expect(updateSpy.mock.calls[0]![0].data.siteId.toString()).toBe(
|
|
307
|
+
SITE_A_ID.toString(),
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("matches a hostname pattern against the display name", async () => {
|
|
312
|
+
mockRules([fakeRule({ hostnamePattern: "*0664*" })]);
|
|
313
|
+
mockDevices([
|
|
314
|
+
fakeDevice({ hostname: "10.0.5.9", name: "UN0664LANSWI03" }),
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
mockUpdate();
|
|
318
|
+
|
|
319
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
320
|
+
|
|
321
|
+
expect(result.devicesAssigned).toBe(1);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("requires every populated criterion to match", async () => {
|
|
325
|
+
mockRules([
|
|
326
|
+
fakeRule({ subnetCidr: "10.0.5.0/24", hostnamePattern: "core-*" }),
|
|
327
|
+
]);
|
|
328
|
+
mockDevices([
|
|
329
|
+
// In the subnet, wrong name.
|
|
330
|
+
fakeDevice({ index: 0, hostname: "10.0.5.9", name: "edge-1" }),
|
|
331
|
+
// Right name, outside the subnet.
|
|
332
|
+
fakeDevice({ index: 1, hostname: "172.16.0.4", name: "core-1" }),
|
|
333
|
+
// Both.
|
|
334
|
+
fakeDevice({ index: 2, hostname: "10.0.5.10", name: "core-2" }),
|
|
335
|
+
]);
|
|
336
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
337
|
+
|
|
338
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
339
|
+
|
|
340
|
+
expect(result.devicesEvaluated).toBe(3);
|
|
341
|
+
expect(result.devicesMatched).toBe(1);
|
|
342
|
+
expect(updateSpy).toHaveBeenCalledTimes(1);
|
|
343
|
+
expect(updateSpy.mock.calls[0]![0].id.toString()).toBe(
|
|
344
|
+
deviceId(2).toString(),
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("never matches a rule with no criteria at all", async () => {
|
|
349
|
+
mockRules([fakeRule({})]);
|
|
350
|
+
mockDevices([
|
|
351
|
+
fakeDevice({ index: 0, hostname: "10.0.5.9" }),
|
|
352
|
+
fakeDevice({ index: 1, name: "anything" }),
|
|
353
|
+
]);
|
|
354
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
355
|
+
|
|
356
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
357
|
+
|
|
358
|
+
expect(result.devicesEvaluated).toBe(2);
|
|
359
|
+
expect(result.devicesMatched).toBe(0);
|
|
360
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe("devices that already belong to a site", () => {
|
|
365
|
+
it("counts a device already in this rule's site without updating it", async () => {
|
|
366
|
+
mockRules([fakeRule({ subnetCidr: "10.0.5.0/24" })]);
|
|
367
|
+
mockDevices([fakeDevice({ hostname: "10.0.5.9", siteId: SITE_A_ID })]);
|
|
368
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
369
|
+
|
|
370
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
371
|
+
|
|
372
|
+
expect(result.devicesMatched).toBe(1);
|
|
373
|
+
expect(result.devicesAlreadyInRuleSite).toBe(1);
|
|
374
|
+
expect(result.devicesAssigned).toBe(0);
|
|
375
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
/*
|
|
379
|
+
* The default. Nothing in the schema says whether SITE_B was chosen by a
|
|
380
|
+
* rule or by a person, so a plain run must not overwrite it.
|
|
381
|
+
*/
|
|
382
|
+
it("leaves a device in another site alone by default", async () => {
|
|
383
|
+
mockRules([fakeRule({ subnetCidr: "10.0.5.0/24" })]);
|
|
384
|
+
mockDevices([fakeDevice({ hostname: "10.0.5.9", siteId: SITE_B_ID })]);
|
|
385
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
386
|
+
|
|
387
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
388
|
+
|
|
389
|
+
expect(result.devicesMatched).toBe(1);
|
|
390
|
+
expect(result.devicesSkippedAlreadyInAnotherSite).toBe(1);
|
|
391
|
+
expect(result.devicesAssigned).toBe(0);
|
|
392
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("moves a device out of another site when explicitly asked to", async () => {
|
|
396
|
+
mockRules([fakeRule({ subnetCidr: "10.0.5.0/24" })]);
|
|
397
|
+
mockDevices([fakeDevice({ hostname: "10.0.5.9", siteId: SITE_B_ID })]);
|
|
398
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
399
|
+
|
|
400
|
+
const result: SiteAssignmentRuleRunResult = await run({
|
|
401
|
+
reassignDevicesAlreadyInASite: true,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
expect(result.devicesAssigned).toBe(1);
|
|
405
|
+
expect(result.devicesSkippedAlreadyInAnotherSite).toBe(0);
|
|
406
|
+
expect(updateSpy.mock.calls[0]![0].data.siteId.toString()).toBe(
|
|
407
|
+
SITE_A_ID.toString(),
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
/*
|
|
412
|
+
* Even with the overwrite flag on, a device already in the rule's own
|
|
413
|
+
* site is a no-op - re-running a rule must not churn writes (each one
|
|
414
|
+
* would recompute a site rollup) for nothing.
|
|
415
|
+
*/
|
|
416
|
+
it("still skips a device already in this rule's site when overwriting", async () => {
|
|
417
|
+
mockRules([fakeRule({ subnetCidr: "10.0.5.0/24" })]);
|
|
418
|
+
mockDevices([fakeDevice({ hostname: "10.0.5.9", siteId: SITE_A_ID })]);
|
|
419
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
420
|
+
|
|
421
|
+
const result: SiteAssignmentRuleRunResult = await run({
|
|
422
|
+
reassignDevicesAlreadyInASite: true,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
expect(result.devicesAlreadyInRuleSite).toBe(1);
|
|
426
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe("priority", () => {
|
|
431
|
+
it("leaves a device a higher-priority rule also matches to that rule", async () => {
|
|
432
|
+
mockRules([
|
|
433
|
+
fakeRule({ subnetCidr: "10.0.0.0/8", priority: 1 }),
|
|
434
|
+
fakeRule({
|
|
435
|
+
id: OTHER_RULE_ID,
|
|
436
|
+
siteId: SITE_B_ID,
|
|
437
|
+
subnetCidr: "10.0.5.0/24",
|
|
438
|
+
priority: 10,
|
|
439
|
+
}),
|
|
440
|
+
]);
|
|
441
|
+
mockDevices([fakeDevice({ hostname: "10.0.5.9" })]);
|
|
442
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
443
|
+
|
|
444
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
445
|
+
|
|
446
|
+
expect(result.devicesMatched).toBe(1);
|
|
447
|
+
expect(result.devicesClaimedByHigherPriorityRule).toBe(1);
|
|
448
|
+
expect(result.devicesAssigned).toBe(0);
|
|
449
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("assigns when this rule is the one that outranks the others", async () => {
|
|
453
|
+
mockRules([
|
|
454
|
+
fakeRule({ subnetCidr: "10.0.5.0/24", priority: 10 }),
|
|
455
|
+
fakeRule({
|
|
456
|
+
id: OTHER_RULE_ID,
|
|
457
|
+
siteId: SITE_B_ID,
|
|
458
|
+
subnetCidr: "10.0.0.0/8",
|
|
459
|
+
priority: 1,
|
|
460
|
+
}),
|
|
461
|
+
]);
|
|
462
|
+
mockDevices([fakeDevice({ hostname: "10.0.5.9" })]);
|
|
463
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
464
|
+
|
|
465
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
466
|
+
|
|
467
|
+
expect(result.devicesAssigned).toBe(1);
|
|
468
|
+
expect(result.devicesClaimedByHigherPriorityRule).toBe(0);
|
|
469
|
+
expect(updateSpy.mock.calls[0]![0].data.siteId.toString()).toBe(
|
|
470
|
+
SITE_A_ID.toString(),
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Ties go to the older rule, the same way the automatic path breaks them.
|
|
475
|
+
it("yields to an equally ranked but older rule", async () => {
|
|
476
|
+
mockRules([
|
|
477
|
+
fakeRule({
|
|
478
|
+
subnetCidr: "10.0.5.0/24",
|
|
479
|
+
priority: 5,
|
|
480
|
+
createdAt: new Date("2024-02-01T00:00:00Z"),
|
|
481
|
+
}),
|
|
482
|
+
fakeRule({
|
|
483
|
+
id: OTHER_RULE_ID,
|
|
484
|
+
siteId: SITE_B_ID,
|
|
485
|
+
subnetCidr: "10.0.5.0/24",
|
|
486
|
+
priority: 5,
|
|
487
|
+
createdAt: new Date("2024-01-01T00:00:00Z"),
|
|
488
|
+
}),
|
|
489
|
+
]);
|
|
490
|
+
mockDevices([fakeDevice({ hostname: "10.0.5.9" })]);
|
|
491
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
492
|
+
|
|
493
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
494
|
+
|
|
495
|
+
expect(result.devicesClaimedByHigherPriorityRule).toBe(1);
|
|
496
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("wins an equally ranked tie when it is the older rule", async () => {
|
|
500
|
+
mockRules([
|
|
501
|
+
fakeRule({
|
|
502
|
+
subnetCidr: "10.0.5.0/24",
|
|
503
|
+
priority: 5,
|
|
504
|
+
createdAt: new Date("2024-01-01T00:00:00Z"),
|
|
505
|
+
}),
|
|
506
|
+
fakeRule({
|
|
507
|
+
id: OTHER_RULE_ID,
|
|
508
|
+
siteId: SITE_B_ID,
|
|
509
|
+
subnetCidr: "10.0.5.0/24",
|
|
510
|
+
priority: 5,
|
|
511
|
+
createdAt: new Date("2024-02-01T00:00:00Z"),
|
|
512
|
+
}),
|
|
513
|
+
]);
|
|
514
|
+
mockDevices([fakeDevice({ hostname: "10.0.5.9" })]);
|
|
515
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
516
|
+
|
|
517
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
518
|
+
|
|
519
|
+
expect(result.devicesAssigned).toBe(1);
|
|
520
|
+
expect(updateSpy.mock.calls[0]![0].data.siteId.toString()).toBe(
|
|
521
|
+
SITE_A_ID.toString(),
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
/*
|
|
526
|
+
* Priority is checked AFTER the already-in-a-site checks, so a device a
|
|
527
|
+
* higher-priority rule wants is still reported under the reason the
|
|
528
|
+
* operator can act on first.
|
|
529
|
+
*/
|
|
530
|
+
it("reports a device already in another site before consulting priority", async () => {
|
|
531
|
+
mockRules([
|
|
532
|
+
fakeRule({ subnetCidr: "10.0.0.0/8", priority: 1 }),
|
|
533
|
+
fakeRule({
|
|
534
|
+
id: OTHER_RULE_ID,
|
|
535
|
+
siteId: SITE_B_ID,
|
|
536
|
+
subnetCidr: "10.0.5.0/24",
|
|
537
|
+
priority: 10,
|
|
538
|
+
}),
|
|
539
|
+
]);
|
|
540
|
+
mockDevices([fakeDevice({ hostname: "10.0.5.9", siteId: SITE_B_ID })]);
|
|
541
|
+
|
|
542
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
543
|
+
|
|
544
|
+
expect(result.devicesSkippedAlreadyInAnotherSite).toBe(1);
|
|
545
|
+
expect(result.devicesClaimedByHigherPriorityRule).toBe(0);
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
describe("resilience and reporting", () => {
|
|
550
|
+
it("counts a failed device update and keeps going", async () => {
|
|
551
|
+
mockRules([fakeRule({ subnetCidr: "10.0.5.0/24" })]);
|
|
552
|
+
mockDevices([
|
|
553
|
+
fakeDevice({ index: 0, hostname: "10.0.5.9" }),
|
|
554
|
+
fakeDevice({ index: 1, hostname: "10.0.5.10" }),
|
|
555
|
+
fakeDevice({ index: 2, hostname: "10.0.5.11" }),
|
|
556
|
+
]);
|
|
557
|
+
|
|
558
|
+
const updateSpy: jest.SpyInstance = jest
|
|
559
|
+
.spyOn(NetworkDeviceService, "updateOneById")
|
|
560
|
+
.mockImplementation(async (data: any) => {
|
|
561
|
+
if (data.id.toString() === deviceId(1).toString()) {
|
|
562
|
+
throw new Error("deadlock detected");
|
|
563
|
+
}
|
|
564
|
+
return undefined as never;
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
568
|
+
|
|
569
|
+
expect(updateSpy).toHaveBeenCalledTimes(3);
|
|
570
|
+
expect(result.devicesMatched).toBe(3);
|
|
571
|
+
expect(result.devicesAssigned).toBe(2);
|
|
572
|
+
expect(result.devicesFailed).toBe(1);
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
/*
|
|
576
|
+
* A row with no usable id cannot be updated. It still has to be
|
|
577
|
+
* accounted for, or the buckets would not add up to devicesMatched and
|
|
578
|
+
* the summary would silently under-report.
|
|
579
|
+
*/
|
|
580
|
+
it("counts a device with no id as a failure rather than losing it", async () => {
|
|
581
|
+
mockRules([fakeRule({ subnetCidr: "10.0.5.0/24" })]);
|
|
582
|
+
mockDevices([
|
|
583
|
+
{
|
|
584
|
+
projectId: PROJECT_ID,
|
|
585
|
+
hostname: "10.0.5.9",
|
|
586
|
+
} as unknown as NetworkDevice,
|
|
587
|
+
]);
|
|
588
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
589
|
+
|
|
590
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
591
|
+
|
|
592
|
+
expect(result.devicesMatched).toBe(1);
|
|
593
|
+
expect(result.devicesFailed).toBe(1);
|
|
594
|
+
expect(result.devicesAssigned).toBe(0);
|
|
595
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it("returns an all-zero result for a project with no devices", async () => {
|
|
599
|
+
mockRules([fakeRule({ subnetCidr: "10.0.5.0/24" })]);
|
|
600
|
+
mockDevices([]);
|
|
601
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
602
|
+
|
|
603
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
604
|
+
|
|
605
|
+
expect(result).toEqual({
|
|
606
|
+
devicesEvaluated: 0,
|
|
607
|
+
devicesMatched: 0,
|
|
608
|
+
devicesAssigned: 0,
|
|
609
|
+
devicesAlreadyInRuleSite: 0,
|
|
610
|
+
devicesSkippedAlreadyInAnotherSite: 0,
|
|
611
|
+
devicesClaimedByHigherPriorityRule: 0,
|
|
612
|
+
devicesFailed: 0,
|
|
613
|
+
isTruncated: false,
|
|
614
|
+
});
|
|
615
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
/*
|
|
619
|
+
* Every evaluated device lands in exactly one bucket, or a zero
|
|
620
|
+
* assignment count could never be explained to the operator.
|
|
621
|
+
*/
|
|
622
|
+
it("accounts for every matched device in exactly one bucket", async () => {
|
|
623
|
+
mockRules([
|
|
624
|
+
fakeRule({ subnetCidr: "10.0.0.0/8", priority: 1 }),
|
|
625
|
+
fakeRule({
|
|
626
|
+
id: OTHER_RULE_ID,
|
|
627
|
+
siteId: SITE_B_ID,
|
|
628
|
+
subnetCidr: "10.0.9.0/24",
|
|
629
|
+
priority: 10,
|
|
630
|
+
}),
|
|
631
|
+
]);
|
|
632
|
+
mockDevices([
|
|
633
|
+
// Assigned.
|
|
634
|
+
fakeDevice({ index: 0, hostname: "10.0.5.9" }),
|
|
635
|
+
// Already in this rule's site.
|
|
636
|
+
fakeDevice({ index: 1, hostname: "10.0.5.10", siteId: SITE_A_ID }),
|
|
637
|
+
// In another site.
|
|
638
|
+
fakeDevice({ index: 2, hostname: "10.0.5.11", siteId: SITE_B_ID }),
|
|
639
|
+
// Claimed by the higher-priority rule.
|
|
640
|
+
fakeDevice({ index: 3, hostname: "10.0.9.4" }),
|
|
641
|
+
// No match at all.
|
|
642
|
+
fakeDevice({ index: 4, hostname: "172.16.0.1" }),
|
|
643
|
+
]);
|
|
644
|
+
mockUpdate();
|
|
645
|
+
|
|
646
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
647
|
+
|
|
648
|
+
expect(result.devicesEvaluated).toBe(5);
|
|
649
|
+
expect(result.devicesMatched).toBe(4);
|
|
650
|
+
expect(
|
|
651
|
+
result.devicesAssigned +
|
|
652
|
+
result.devicesAlreadyInRuleSite +
|
|
653
|
+
result.devicesSkippedAlreadyInAnotherSite +
|
|
654
|
+
result.devicesClaimedByHigherPriorityRule +
|
|
655
|
+
result.devicesFailed,
|
|
656
|
+
).toBe(result.devicesMatched);
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
describe("paging and the run cap", () => {
|
|
661
|
+
it("walks every page of a multi-page estate", async () => {
|
|
662
|
+
mockRules([fakeRule({ subnetCidr: "10.0.5.0/24" })]);
|
|
663
|
+
const devicesSpy: jest.SpyInstance = mockGeneratedDevices(2500);
|
|
664
|
+
const updateSpy: jest.SpyInstance = mockUpdate();
|
|
665
|
+
|
|
666
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
667
|
+
|
|
668
|
+
expect(result.devicesEvaluated).toBe(2500);
|
|
669
|
+
expect(result.devicesAssigned).toBe(2500);
|
|
670
|
+
expect(updateSpy).toHaveBeenCalledTimes(2500);
|
|
671
|
+
expect(result.isTruncated).toBe(false);
|
|
672
|
+
|
|
673
|
+
// 1000 + 1000 + 500, and the short page ends the walk.
|
|
674
|
+
expect(devicesSpy).toHaveBeenCalledTimes(3);
|
|
675
|
+
expect(devicesSpy.mock.calls[1]![0].skip).toBe(1000);
|
|
676
|
+
expect(devicesSpy.mock.calls[2]![0].skip).toBe(2000);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
it("stops at the cap and reports the run as truncated", async () => {
|
|
680
|
+
mockRules([fakeRule({ subnetCidr: "10.0.5.0/24" })]);
|
|
681
|
+
mockGeneratedDevices(10500);
|
|
682
|
+
mockUpdate();
|
|
683
|
+
|
|
684
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
685
|
+
|
|
686
|
+
expect(result.devicesEvaluated).toBe(10000);
|
|
687
|
+
expect(result.isTruncated).toBe(true);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
/*
|
|
691
|
+
* A full last page is not proof that more devices exist. An estate of
|
|
692
|
+
* exactly the cap must not report a truncation, or the UI would tell the
|
|
693
|
+
* operator to run it again forever.
|
|
694
|
+
*/
|
|
695
|
+
it("does not report truncation for an estate of exactly the cap", async () => {
|
|
696
|
+
mockRules([fakeRule({ subnetCidr: "10.0.5.0/24" })]);
|
|
697
|
+
mockGeneratedDevices(10000);
|
|
698
|
+
mockUpdate();
|
|
699
|
+
|
|
700
|
+
const result: SiteAssignmentRuleRunResult = await run();
|
|
701
|
+
|
|
702
|
+
expect(result.devicesEvaluated).toBe(10000);
|
|
703
|
+
expect(result.isTruncated).toBe(false);
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
});
|