@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,618 @@
|
|
|
1
|
+
import NetworkDeviceLabelRuleEngineService from "../../../Server/Services/NetworkDeviceLabelRuleEngineService";
|
|
2
|
+
import NetworkDeviceLabelRuleService from "../../../Server/Services/NetworkDeviceLabelRuleService";
|
|
3
|
+
import NetworkDeviceService from "../../../Server/Services/NetworkDeviceService";
|
|
4
|
+
import Label from "../../../Models/DatabaseModels/Label";
|
|
5
|
+
import NetworkDevice from "../../../Models/DatabaseModels/NetworkDevice";
|
|
6
|
+
import NetworkDeviceLabelRule from "../../../Models/DatabaseModels/NetworkDeviceLabelRule";
|
|
7
|
+
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
|
8
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
9
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
10
|
+
import { LabelRuleRunResult } from "../../../Types/NetworkAutomation/RuleRunResult";
|
|
11
|
+
import { describe, expect, it, afterEach, beforeEach } from "@jest/globals";
|
|
12
|
+
|
|
13
|
+
/*
|
|
14
|
+
* Contract under test - "Run now" for a network device label rule
|
|
15
|
+
* (OneUptime/oneuptime#3191).
|
|
16
|
+
*
|
|
17
|
+
* The automatic engine only fires when a device is created, so a rule
|
|
18
|
+
* written after an estate was discovered never touches any of it.
|
|
19
|
+
* applyRuleToExistingNetworkDevices is the manual counterpart: the same
|
|
20
|
+
* matcher, the same "already-attached labels are not duplicated" guarantee,
|
|
21
|
+
* applied across a whole project instead of to one row.
|
|
22
|
+
*
|
|
23
|
+
* The things worth pinning down are the ones a bulk path can get wrong that a
|
|
24
|
+
* one-row path cannot: it must attach labels in as few statements as the work
|
|
25
|
+
* allows, count a device once no matter how many pages or labels it appears
|
|
26
|
+
* under, survive a failed batch, and refuse to run a rule the user has
|
|
27
|
+
* switched off.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
31
|
+
"22222222-2222-4222-8222-222222222222",
|
|
32
|
+
);
|
|
33
|
+
const OTHER_PROJECT_ID: ObjectID = new ObjectID(
|
|
34
|
+
"99999999-9999-4999-8999-999999999999",
|
|
35
|
+
);
|
|
36
|
+
const RULE_ID: ObjectID = new ObjectID("77777777-7777-4777-8777-777777777777");
|
|
37
|
+
const LABEL_A_ID: ObjectID = new ObjectID(
|
|
38
|
+
"11111111-1111-4111-8111-111111111111",
|
|
39
|
+
);
|
|
40
|
+
const LABEL_B_ID: ObjectID = new ObjectID(
|
|
41
|
+
"33333333-3333-4333-8333-333333333333",
|
|
42
|
+
);
|
|
43
|
+
const PREREQUISITE_LABEL_ID: ObjectID = new ObjectID(
|
|
44
|
+
"44444444-4444-4444-8444-444444444444",
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
function deviceId(index: number): ObjectID {
|
|
48
|
+
const suffix: string = index.toString(16).padStart(12, "0");
|
|
49
|
+
return new ObjectID(`00000000-0000-4000-8000-${suffix}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fakeLabel(id: ObjectID): Label {
|
|
53
|
+
return { id: id, _id: id.toString() } as unknown as Label;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function fakeDevice(data: {
|
|
57
|
+
index?: number | undefined;
|
|
58
|
+
name?: string | undefined;
|
|
59
|
+
description?: string | undefined;
|
|
60
|
+
labels?: Array<ObjectID> | undefined;
|
|
61
|
+
}): NetworkDevice {
|
|
62
|
+
const id: ObjectID = deviceId(data.index ?? 0);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
id: id,
|
|
66
|
+
_id: id.toString(),
|
|
67
|
+
projectId: PROJECT_ID,
|
|
68
|
+
name: data.name,
|
|
69
|
+
description: data.description,
|
|
70
|
+
labels: (data.labels || []).map((labelId: ObjectID) => {
|
|
71
|
+
return fakeLabel(labelId);
|
|
72
|
+
}),
|
|
73
|
+
} as unknown as NetworkDevice;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function fakeRule(data: {
|
|
77
|
+
isEnabled?: boolean | undefined;
|
|
78
|
+
labelsToAdd?: Array<ObjectID> | undefined;
|
|
79
|
+
networkDeviceLabels?: Array<ObjectID> | undefined;
|
|
80
|
+
networkDeviceNamePattern?: string | undefined;
|
|
81
|
+
networkDeviceDescriptionPattern?: string | undefined;
|
|
82
|
+
}): NetworkDeviceLabelRule {
|
|
83
|
+
return {
|
|
84
|
+
id: RULE_ID,
|
|
85
|
+
_id: RULE_ID.toString(),
|
|
86
|
+
projectId: PROJECT_ID,
|
|
87
|
+
name: "Tag core switches",
|
|
88
|
+
isEnabled: data.isEnabled ?? true,
|
|
89
|
+
labelsToAdd: (data.labelsToAdd || [LABEL_A_ID]).map((id: ObjectID) => {
|
|
90
|
+
return fakeLabel(id);
|
|
91
|
+
}),
|
|
92
|
+
networkDeviceLabels: (data.networkDeviceLabels || []).map(
|
|
93
|
+
(id: ObjectID) => {
|
|
94
|
+
return fakeLabel(id);
|
|
95
|
+
},
|
|
96
|
+
),
|
|
97
|
+
networkDeviceNamePattern: data.networkDeviceNamePattern,
|
|
98
|
+
networkDeviceDescriptionPattern: data.networkDeviceDescriptionPattern,
|
|
99
|
+
} as unknown as NetworkDeviceLabelRule;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function mockRule(rule: NetworkDeviceLabelRule | null): jest.SpyInstance {
|
|
103
|
+
return jest
|
|
104
|
+
.spyOn(NetworkDeviceLabelRuleService, "findOneBy")
|
|
105
|
+
.mockResolvedValue(rule);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function mockDevices(devices: Array<NetworkDevice>): jest.SpyInstance {
|
|
109
|
+
return jest
|
|
110
|
+
.spyOn(NetworkDeviceService, "findBy")
|
|
111
|
+
.mockImplementation(async (data: any) => {
|
|
112
|
+
const skip: number = data.skip || 0;
|
|
113
|
+
const limit: number = data.limit || devices.length;
|
|
114
|
+
return devices.slice(skip, skip + limit) as any;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function mockGeneratedDevices(totalDevices: number): jest.SpyInstance {
|
|
119
|
+
return jest
|
|
120
|
+
.spyOn(NetworkDeviceService, "findBy")
|
|
121
|
+
.mockImplementation(async (data: any) => {
|
|
122
|
+
const skip: number = data.skip || 0;
|
|
123
|
+
const limit: number = data.limit || 0;
|
|
124
|
+
const page: Array<NetworkDevice> = [];
|
|
125
|
+
|
|
126
|
+
for (
|
|
127
|
+
let index: number = skip;
|
|
128
|
+
index < Math.min(skip + limit, totalDevices);
|
|
129
|
+
index++
|
|
130
|
+
) {
|
|
131
|
+
page.push(fakeDevice({ index: index, name: "core-switch" }));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return page as any;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/*
|
|
139
|
+
* The relation query builder the engine attaches labels through. Recorded as
|
|
140
|
+
* (labelId, deviceIds) pairs so a test can assert the shape of the writes -
|
|
141
|
+
* one statement per label, not one per device.
|
|
142
|
+
*/
|
|
143
|
+
interface RelationAdd {
|
|
144
|
+
labelId: string;
|
|
145
|
+
deviceIds: Array<string>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function mockRelationBuilder(data?: {
|
|
149
|
+
failOnLabelId?: string | undefined;
|
|
150
|
+
}): Array<RelationAdd> {
|
|
151
|
+
const adds: Array<RelationAdd> = [];
|
|
152
|
+
|
|
153
|
+
const builder: any = {
|
|
154
|
+
createQueryBuilder: () => {
|
|
155
|
+
return builder;
|
|
156
|
+
},
|
|
157
|
+
relation: () => {
|
|
158
|
+
return builder;
|
|
159
|
+
},
|
|
160
|
+
of: (deviceIds: Array<string>) => {
|
|
161
|
+
builder.pendingDeviceIds = deviceIds;
|
|
162
|
+
return builder;
|
|
163
|
+
},
|
|
164
|
+
add: async (labelId: string) => {
|
|
165
|
+
if (data?.failOnLabelId && labelId === data.failOnLabelId) {
|
|
166
|
+
throw new Error("duplicate key value violates unique constraint");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
adds.push({
|
|
170
|
+
labelId: labelId,
|
|
171
|
+
deviceIds: [...(builder.pendingDeviceIds as Array<string>)],
|
|
172
|
+
});
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
jest
|
|
177
|
+
.spyOn(NetworkDeviceService, "getRepository")
|
|
178
|
+
.mockReturnValue(builder as any);
|
|
179
|
+
|
|
180
|
+
return adds;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function run(
|
|
184
|
+
overrides: {
|
|
185
|
+
ruleId?: ObjectID | undefined;
|
|
186
|
+
projectId?: ObjectID | undefined;
|
|
187
|
+
} = {},
|
|
188
|
+
): Promise<LabelRuleRunResult> {
|
|
189
|
+
return NetworkDeviceLabelRuleEngineService.applyRuleToExistingNetworkDevices({
|
|
190
|
+
ruleId: overrides.ruleId || RULE_ID,
|
|
191
|
+
projectId: overrides.projectId || PROJECT_ID,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
describe("NetworkDeviceLabelRuleEngineService.applyRuleToExistingNetworkDevices", () => {
|
|
196
|
+
beforeEach(() => {
|
|
197
|
+
// Silence the logs the failure-path tests deliberately provoke.
|
|
198
|
+
jest.spyOn(console, "error").mockImplementation(() => {});
|
|
199
|
+
jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
afterEach(() => {
|
|
203
|
+
jest.restoreAllMocks();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe("rule resolution", () => {
|
|
207
|
+
it("rejects a rule that does not exist", async () => {
|
|
208
|
+
mockRule(null);
|
|
209
|
+
mockDevices([]);
|
|
210
|
+
|
|
211
|
+
await expect(run()).rejects.toThrow(BadDataException);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
/*
|
|
215
|
+
* The tenant guard: the lookup is scoped, so another project's rule id
|
|
216
|
+
* reads as "not found" and nothing of that project is touched.
|
|
217
|
+
*/
|
|
218
|
+
it("looks the rule up scoped to the project", async () => {
|
|
219
|
+
const ruleSpy: jest.SpyInstance = mockRule(null);
|
|
220
|
+
const devicesSpy: jest.SpyInstance = mockDevices([]);
|
|
221
|
+
|
|
222
|
+
await expect(run({ projectId: OTHER_PROJECT_ID })).rejects.toThrow(
|
|
223
|
+
"Label rule not found.",
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const query: any = ruleSpy.mock.calls[0]![0].query;
|
|
227
|
+
expect(query._id.toString()).toBe(RULE_ID.toString());
|
|
228
|
+
expect(query.projectId.toString()).toBe(OTHER_PROJECT_ID.toString());
|
|
229
|
+
expect(devicesSpy).not.toHaveBeenCalled();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/*
|
|
233
|
+
* Running a rule the operator has switched off would contradict the
|
|
234
|
+
* toggle sitting right next to the button.
|
|
235
|
+
*/
|
|
236
|
+
it("refuses to run a disabled rule", async () => {
|
|
237
|
+
mockRule(fakeRule({ isEnabled: false }));
|
|
238
|
+
const devicesSpy: jest.SpyInstance = mockDevices([]);
|
|
239
|
+
|
|
240
|
+
await expect(run()).rejects.toThrow(
|
|
241
|
+
"This label rule is disabled. Enable it before running it.",
|
|
242
|
+
);
|
|
243
|
+
expect(devicesSpy).not.toHaveBeenCalled();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("refuses to run a rule that attaches no labels", async () => {
|
|
247
|
+
mockRule(fakeRule({ labelsToAdd: [] }));
|
|
248
|
+
const devicesSpy: jest.SpyInstance = mockDevices([]);
|
|
249
|
+
|
|
250
|
+
await expect(run()).rejects.toThrow(
|
|
251
|
+
"This label rule has no labels to add, so running it would do nothing.",
|
|
252
|
+
);
|
|
253
|
+
expect(devicesSpy).not.toHaveBeenCalled();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("scopes the device query to the project and pages it by id", async () => {
|
|
257
|
+
mockRule(fakeRule({}));
|
|
258
|
+
const devicesSpy: jest.SpyInstance = mockDevices([]);
|
|
259
|
+
mockRelationBuilder();
|
|
260
|
+
|
|
261
|
+
await run();
|
|
262
|
+
|
|
263
|
+
expect(devicesSpy.mock.calls[0]![0].query.projectId.toString()).toBe(
|
|
264
|
+
PROJECT_ID.toString(),
|
|
265
|
+
);
|
|
266
|
+
expect(devicesSpy.mock.calls[0]![0].sort).toEqual({
|
|
267
|
+
_id: SortOrder.Ascending,
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("matching", () => {
|
|
273
|
+
it("attaches the rule's labels to every matching device", async () => {
|
|
274
|
+
mockRule(
|
|
275
|
+
fakeRule({
|
|
276
|
+
networkDeviceNamePattern: "core-.*",
|
|
277
|
+
labelsToAdd: [LABEL_A_ID],
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
mockDevices([
|
|
281
|
+
fakeDevice({ index: 0, name: "core-switch-1" }),
|
|
282
|
+
fakeDevice({ index: 1, name: "edge-router-1" }),
|
|
283
|
+
fakeDevice({ index: 2, name: "core-switch-2" }),
|
|
284
|
+
]);
|
|
285
|
+
const adds: Array<RelationAdd> = mockRelationBuilder();
|
|
286
|
+
|
|
287
|
+
const result: LabelRuleRunResult = await run();
|
|
288
|
+
|
|
289
|
+
expect(result.devicesEvaluated).toBe(3);
|
|
290
|
+
expect(result.devicesMatched).toBe(2);
|
|
291
|
+
expect(result.devicesLabeled).toBe(2);
|
|
292
|
+
expect(result.labelsAttached).toBe(2);
|
|
293
|
+
expect(adds).toEqual([
|
|
294
|
+
{
|
|
295
|
+
labelId: LABEL_A_ID.toString(),
|
|
296
|
+
deviceIds: [deviceId(0).toString(), deviceId(2).toString()],
|
|
297
|
+
},
|
|
298
|
+
]);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
/*
|
|
302
|
+
* The glob syntax the neighbouring site assignment rules use has to keep
|
|
303
|
+
* working here too - a rule written as `*0664*` used to compile to
|
|
304
|
+
* nothing and silently label no device (OneUptime/oneuptime#2940).
|
|
305
|
+
*/
|
|
306
|
+
it("accepts a '*' wildcard pattern as well as a regex", async () => {
|
|
307
|
+
mockRule(fakeRule({ networkDeviceNamePattern: "*0664*" }));
|
|
308
|
+
mockDevices([
|
|
309
|
+
fakeDevice({ index: 0, name: "UN0664LANSWI03" }),
|
|
310
|
+
fakeDevice({ index: 1, name: "UN0999LANSWI03" }),
|
|
311
|
+
]);
|
|
312
|
+
const adds: Array<RelationAdd> = mockRelationBuilder();
|
|
313
|
+
|
|
314
|
+
const result: LabelRuleRunResult = await run();
|
|
315
|
+
|
|
316
|
+
expect(result.devicesMatched).toBe(1);
|
|
317
|
+
expect(adds[0]!.deviceIds).toEqual([deviceId(0).toString()]);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("matches on the description pattern too", async () => {
|
|
321
|
+
mockRule(
|
|
322
|
+
fakeRule({ networkDeviceDescriptionPattern: "production|critical" }),
|
|
323
|
+
);
|
|
324
|
+
mockDevices([
|
|
325
|
+
fakeDevice({ index: 0, name: "sw-1", description: "production core" }),
|
|
326
|
+
fakeDevice({ index: 1, name: "sw-2", description: "lab bench" }),
|
|
327
|
+
]);
|
|
328
|
+
const adds: Array<RelationAdd> = mockRelationBuilder();
|
|
329
|
+
|
|
330
|
+
const result: LabelRuleRunResult = await run();
|
|
331
|
+
|
|
332
|
+
expect(result.devicesMatched).toBe(1);
|
|
333
|
+
expect(adds[0]!.deviceIds).toEqual([deviceId(0).toString()]);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("requires every populated criterion to match", async () => {
|
|
337
|
+
mockRule(
|
|
338
|
+
fakeRule({
|
|
339
|
+
networkDeviceNamePattern: "core-.*",
|
|
340
|
+
networkDeviceDescriptionPattern: "production",
|
|
341
|
+
}),
|
|
342
|
+
);
|
|
343
|
+
mockDevices([
|
|
344
|
+
fakeDevice({ index: 0, name: "core-1", description: "lab" }),
|
|
345
|
+
fakeDevice({ index: 1, name: "edge-1", description: "production" }),
|
|
346
|
+
fakeDevice({ index: 2, name: "core-2", description: "production" }),
|
|
347
|
+
]);
|
|
348
|
+
const adds: Array<RelationAdd> = mockRelationBuilder();
|
|
349
|
+
|
|
350
|
+
const result: LabelRuleRunResult = await run();
|
|
351
|
+
|
|
352
|
+
expect(result.devicesMatched).toBe(1);
|
|
353
|
+
expect(adds[0]!.deviceIds).toEqual([deviceId(2).toString()]);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// The prerequisite filter is any-of, matching the automatic engine.
|
|
357
|
+
it("honours the prerequisite label filter", async () => {
|
|
358
|
+
mockRule(
|
|
359
|
+
fakeRule({
|
|
360
|
+
networkDeviceLabels: [PREREQUISITE_LABEL_ID],
|
|
361
|
+
labelsToAdd: [LABEL_A_ID],
|
|
362
|
+
}),
|
|
363
|
+
);
|
|
364
|
+
mockDevices([
|
|
365
|
+
fakeDevice({ index: 0, name: "sw-1", labels: [PREREQUISITE_LABEL_ID] }),
|
|
366
|
+
fakeDevice({ index: 1, name: "sw-2", labels: [] }),
|
|
367
|
+
fakeDevice({ index: 2, name: "sw-3", labels: [LABEL_B_ID] }),
|
|
368
|
+
]);
|
|
369
|
+
const adds: Array<RelationAdd> = mockRelationBuilder();
|
|
370
|
+
|
|
371
|
+
const result: LabelRuleRunResult = await run();
|
|
372
|
+
|
|
373
|
+
expect(result.devicesMatched).toBe(1);
|
|
374
|
+
expect(adds[0]!.deviceIds).toEqual([deviceId(0).toString()]);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
/*
|
|
378
|
+
* A rule with no criteria matches everything, which is what the
|
|
379
|
+
* automatic engine already does on device create. The manual run has to
|
|
380
|
+
* agree with it, or the button would mean something different from the
|
|
381
|
+
* rule it is attached to.
|
|
382
|
+
*/
|
|
383
|
+
it("matches every device when the rule has no criteria", async () => {
|
|
384
|
+
mockRule(fakeRule({}));
|
|
385
|
+
mockDevices([
|
|
386
|
+
fakeDevice({ index: 0, name: "anything" }),
|
|
387
|
+
fakeDevice({ index: 1 }),
|
|
388
|
+
]);
|
|
389
|
+
const adds: Array<RelationAdd> = mockRelationBuilder();
|
|
390
|
+
|
|
391
|
+
const result: LabelRuleRunResult = await run();
|
|
392
|
+
|
|
393
|
+
expect(result.devicesMatched).toBe(2);
|
|
394
|
+
expect(adds[0]!.deviceIds).toHaveLength(2);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
/*
|
|
398
|
+
* A pattern that is neither a valid regex nor a wildcard glob can never
|
|
399
|
+
* match; it must label nothing rather than throw the run away.
|
|
400
|
+
*/
|
|
401
|
+
it("matches nothing for an unusable pattern instead of failing", async () => {
|
|
402
|
+
mockRule(fakeRule({ networkDeviceNamePattern: "core-[" }));
|
|
403
|
+
mockDevices([fakeDevice({ index: 0, name: "core-[" })]);
|
|
404
|
+
const adds: Array<RelationAdd> = mockRelationBuilder();
|
|
405
|
+
|
|
406
|
+
const result: LabelRuleRunResult = await run();
|
|
407
|
+
|
|
408
|
+
expect(result.devicesMatched).toBe(0);
|
|
409
|
+
expect(adds).toHaveLength(0);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe("idempotence", () => {
|
|
414
|
+
it("does not re-attach a label the device already carries", async () => {
|
|
415
|
+
mockRule(fakeRule({ labelsToAdd: [LABEL_A_ID] }));
|
|
416
|
+
mockDevices([
|
|
417
|
+
fakeDevice({ index: 0, name: "sw-1", labels: [LABEL_A_ID] }),
|
|
418
|
+
fakeDevice({ index: 1, name: "sw-2", labels: [] }),
|
|
419
|
+
]);
|
|
420
|
+
const adds: Array<RelationAdd> = mockRelationBuilder();
|
|
421
|
+
|
|
422
|
+
const result: LabelRuleRunResult = await run();
|
|
423
|
+
|
|
424
|
+
expect(result.devicesMatched).toBe(2);
|
|
425
|
+
expect(result.devicesLabeled).toBe(1);
|
|
426
|
+
expect(result.labelsAttached).toBe(1);
|
|
427
|
+
expect(adds[0]!.deviceIds).toEqual([deviceId(1).toString()]);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
/*
|
|
431
|
+
* A second run of the same rule is the common case - the operator clicks
|
|
432
|
+
* it again to be sure. It has to be a clean no-op, reported as such.
|
|
433
|
+
*/
|
|
434
|
+
it("attaches nothing on a second run of the same rule", async () => {
|
|
435
|
+
mockRule(fakeRule({ labelsToAdd: [LABEL_A_ID] }));
|
|
436
|
+
mockDevices([
|
|
437
|
+
fakeDevice({ index: 0, name: "sw-1", labels: [LABEL_A_ID] }),
|
|
438
|
+
fakeDevice({ index: 1, name: "sw-2", labels: [LABEL_A_ID] }),
|
|
439
|
+
]);
|
|
440
|
+
const adds: Array<RelationAdd> = mockRelationBuilder();
|
|
441
|
+
|
|
442
|
+
const result: LabelRuleRunResult = await run();
|
|
443
|
+
|
|
444
|
+
expect(result.devicesMatched).toBe(2);
|
|
445
|
+
expect(result.devicesLabeled).toBe(0);
|
|
446
|
+
expect(result.labelsAttached).toBe(0);
|
|
447
|
+
expect(adds).toHaveLength(0);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("adds only the labels a partially labelled device is missing", async () => {
|
|
451
|
+
mockRule(fakeRule({ labelsToAdd: [LABEL_A_ID, LABEL_B_ID] }));
|
|
452
|
+
mockDevices([
|
|
453
|
+
fakeDevice({ index: 0, name: "sw-1", labels: [LABEL_A_ID] }),
|
|
454
|
+
]);
|
|
455
|
+
const adds: Array<RelationAdd> = mockRelationBuilder();
|
|
456
|
+
|
|
457
|
+
const result: LabelRuleRunResult = await run();
|
|
458
|
+
|
|
459
|
+
expect(result.devicesLabeled).toBe(1);
|
|
460
|
+
expect(result.labelsAttached).toBe(1);
|
|
461
|
+
expect(adds).toEqual([
|
|
462
|
+
{
|
|
463
|
+
labelId: LABEL_B_ID.toString(),
|
|
464
|
+
deviceIds: [deviceId(0).toString()],
|
|
465
|
+
},
|
|
466
|
+
]);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
describe("write batching", () => {
|
|
471
|
+
/*
|
|
472
|
+
* One statement per label, not one per device. A per-device write would
|
|
473
|
+
* turn a ten-thousand-device estate into ten thousand round trips inside
|
|
474
|
+
* a single HTTP request.
|
|
475
|
+
*/
|
|
476
|
+
it("writes one batch per label, whatever the device count", async () => {
|
|
477
|
+
mockRule(fakeRule({ labelsToAdd: [LABEL_A_ID, LABEL_B_ID] }));
|
|
478
|
+
mockDevices([
|
|
479
|
+
fakeDevice({ index: 0, name: "sw-1" }),
|
|
480
|
+
fakeDevice({ index: 1, name: "sw-2" }),
|
|
481
|
+
fakeDevice({ index: 2, name: "sw-3" }),
|
|
482
|
+
]);
|
|
483
|
+
const adds: Array<RelationAdd> = mockRelationBuilder();
|
|
484
|
+
|
|
485
|
+
const result: LabelRuleRunResult = await run();
|
|
486
|
+
|
|
487
|
+
expect(adds).toHaveLength(2);
|
|
488
|
+
expect(adds[0]!.deviceIds).toHaveLength(3);
|
|
489
|
+
expect(adds[1]!.deviceIds).toHaveLength(3);
|
|
490
|
+
expect(result.devicesLabeled).toBe(3);
|
|
491
|
+
expect(result.labelsAttached).toBe(6);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
/*
|
|
495
|
+
* Postgres refuses a statement with more than 65535 parameters, so a
|
|
496
|
+
* batch is chunked well below that.
|
|
497
|
+
*/
|
|
498
|
+
it("chunks a batch that would otherwise be one huge statement", async () => {
|
|
499
|
+
mockRule(fakeRule({ labelsToAdd: [LABEL_A_ID] }));
|
|
500
|
+
mockGeneratedDevices(1000);
|
|
501
|
+
const adds: Array<RelationAdd> = mockRelationBuilder();
|
|
502
|
+
|
|
503
|
+
const result: LabelRuleRunResult = await run();
|
|
504
|
+
|
|
505
|
+
expect(adds).toHaveLength(2);
|
|
506
|
+
expect(adds[0]!.deviceIds).toHaveLength(500);
|
|
507
|
+
expect(adds[1]!.deviceIds).toHaveLength(500);
|
|
508
|
+
expect(result.labelsAttached).toBe(1000);
|
|
509
|
+
expect(result.devicesLabeled).toBe(1000);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
describe("resilience and reporting", () => {
|
|
514
|
+
it("counts a failed batch and keeps going with the other labels", async () => {
|
|
515
|
+
mockRule(fakeRule({ labelsToAdd: [LABEL_A_ID, LABEL_B_ID] }));
|
|
516
|
+
mockDevices([
|
|
517
|
+
fakeDevice({ index: 0, name: "sw-1" }),
|
|
518
|
+
fakeDevice({ index: 1, name: "sw-2" }),
|
|
519
|
+
]);
|
|
520
|
+
const adds: Array<RelationAdd> = mockRelationBuilder({
|
|
521
|
+
failOnLabelId: LABEL_A_ID.toString(),
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const result: LabelRuleRunResult = await run();
|
|
525
|
+
|
|
526
|
+
expect(adds).toHaveLength(1);
|
|
527
|
+
expect(adds[0]!.labelId).toBe(LABEL_B_ID.toString());
|
|
528
|
+
expect(result.labelsAttached).toBe(2);
|
|
529
|
+
expect(result.labelsFailed).toBe(2);
|
|
530
|
+
// The devices still gained label B, so they count as labelled.
|
|
531
|
+
expect(result.devicesLabeled).toBe(2);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
it("reports no devices labelled when every batch fails", async () => {
|
|
535
|
+
mockRule(fakeRule({ labelsToAdd: [LABEL_A_ID] }));
|
|
536
|
+
mockDevices([fakeDevice({ index: 0, name: "sw-1" })]);
|
|
537
|
+
mockRelationBuilder({ failOnLabelId: LABEL_A_ID.toString() });
|
|
538
|
+
|
|
539
|
+
const result: LabelRuleRunResult = await run();
|
|
540
|
+
|
|
541
|
+
expect(result.devicesMatched).toBe(1);
|
|
542
|
+
expect(result.devicesLabeled).toBe(0);
|
|
543
|
+
expect(result.labelsAttached).toBe(0);
|
|
544
|
+
expect(result.labelsFailed).toBe(1);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("returns an all-zero result for a project with no devices", async () => {
|
|
548
|
+
mockRule(fakeRule({}));
|
|
549
|
+
mockDevices([]);
|
|
550
|
+
const adds: Array<RelationAdd> = mockRelationBuilder();
|
|
551
|
+
|
|
552
|
+
const result: LabelRuleRunResult = await run();
|
|
553
|
+
|
|
554
|
+
expect(result).toEqual({
|
|
555
|
+
devicesEvaluated: 0,
|
|
556
|
+
devicesMatched: 0,
|
|
557
|
+
devicesLabeled: 0,
|
|
558
|
+
labelsAttached: 0,
|
|
559
|
+
labelsFailed: 0,
|
|
560
|
+
isTruncated: false,
|
|
561
|
+
});
|
|
562
|
+
expect(adds).toHaveLength(0);
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
describe("paging and the run cap", () => {
|
|
567
|
+
it("walks every page of a multi-page estate", async () => {
|
|
568
|
+
mockRule(fakeRule({ labelsToAdd: [LABEL_A_ID] }));
|
|
569
|
+
const devicesSpy: jest.SpyInstance = mockGeneratedDevices(2500);
|
|
570
|
+
mockRelationBuilder();
|
|
571
|
+
|
|
572
|
+
const result: LabelRuleRunResult = await run();
|
|
573
|
+
|
|
574
|
+
expect(result.devicesEvaluated).toBe(2500);
|
|
575
|
+
expect(result.devicesLabeled).toBe(2500);
|
|
576
|
+
expect(result.isTruncated).toBe(false);
|
|
577
|
+
expect(devicesSpy).toHaveBeenCalledTimes(3);
|
|
578
|
+
expect(devicesSpy.mock.calls[1]![0].skip).toBe(1000);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
/*
|
|
582
|
+
* devicesLabeled is a distinct count, so a device cannot be counted
|
|
583
|
+
* twice by appearing under two labels or in two chunks.
|
|
584
|
+
*/
|
|
585
|
+
it("counts each labelled device once across pages and labels", async () => {
|
|
586
|
+
mockRule(fakeRule({ labelsToAdd: [LABEL_A_ID, LABEL_B_ID] }));
|
|
587
|
+
mockGeneratedDevices(1500);
|
|
588
|
+
mockRelationBuilder();
|
|
589
|
+
|
|
590
|
+
const result: LabelRuleRunResult = await run();
|
|
591
|
+
|
|
592
|
+
expect(result.devicesLabeled).toBe(1500);
|
|
593
|
+
expect(result.labelsAttached).toBe(3000);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it("stops at the cap and reports the run as truncated", async () => {
|
|
597
|
+
mockRule(fakeRule({ labelsToAdd: [LABEL_A_ID] }));
|
|
598
|
+
mockGeneratedDevices(10500);
|
|
599
|
+
mockRelationBuilder();
|
|
600
|
+
|
|
601
|
+
const result: LabelRuleRunResult = await run();
|
|
602
|
+
|
|
603
|
+
expect(result.devicesEvaluated).toBe(10000);
|
|
604
|
+
expect(result.isTruncated).toBe(true);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it("does not report truncation for an estate of exactly the cap", async () => {
|
|
608
|
+
mockRule(fakeRule({ labelsToAdd: [LABEL_A_ID] }));
|
|
609
|
+
mockGeneratedDevices(10000);
|
|
610
|
+
mockRelationBuilder();
|
|
611
|
+
|
|
612
|
+
const result: LabelRuleRunResult = await run();
|
|
613
|
+
|
|
614
|
+
expect(result.devicesEvaluated).toBe(10000);
|
|
615
|
+
expect(result.isTruncated).toBe(false);
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
});
|