@oneuptime/common 12.0.31 → 12.0.32
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/AlertState.ts +0 -2
- package/Models/DatabaseModels/Domain.ts +1 -1
- package/Models/DatabaseModels/Incident.ts +1 -1
- package/Models/DatabaseModels/IncidentTemplate.ts +1 -1
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/MetricType.ts +0 -2
- package/Models/DatabaseModels/MonitorStatusTimeline.ts +0 -2
- package/Models/DatabaseModels/MonitorTest.ts +0 -2
- package/Models/DatabaseModels/NetworkDevice.ts +121 -2
- package/Models/DatabaseModels/NetworkDeviceOidTemplate.ts +3 -1
- package/Models/DatabaseModels/NetworkDeviceRole.ts +648 -0
- package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -1
- package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -1
- package/Models/DatabaseModels/StatusPageAnnouncement.ts +0 -2
- package/Models/DatabaseModels/StatusPageAnnouncementTemplate.ts +0 -2
- package/Models/DatabaseModels/StatusPagePrivateUser.ts +0 -2
- package/Models/DatabaseModels/StatusPageResource.ts +0 -2
- package/Models/DatabaseModels/StatusPageSubscriber.ts +0 -2
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.ts +72 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/DatabaseService.ts +39 -9
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/NetworkDeviceRoleService.ts +90 -0
- package/Server/Services/ProjectService.ts +85 -0
- package/Server/Services/UserNotificationSettingService.ts +0 -6
- package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +31 -1
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +35 -8
- package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +107 -3
- package/Tests/App/Dashboard/DashboardWideChartZoom.test.tsx +401 -0
- package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +318 -0
- package/Tests/App/Dashboard/NetworkDeviceBulkDelete.test.tsx +357 -0
- package/Tests/App/Dashboard/NetworkSideMenu.test.tsx +590 -0
- package/Tests/App/Dashboard/SideMenuHarness.tsx +11 -3
- package/Tests/Models/DatabaseModels/ModelRegistryInvariants.test.ts +115 -41
- package/Tests/Server/Infrastructure/Postgres/NetworkDeviceRoleTableMigration.test.ts +476 -0
- package/Tests/Server/Services/DatabaseServiceGenerateSlug.test.ts +215 -0
- package/Tests/Server/Services/NetworkDeviceRoleModel.test.ts +672 -0
- package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +1 -1
- package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +67 -4
- package/Tests/Server/Services/ScheduledMaintenanceSubscriberEndTime.test.ts +369 -0
- package/Tests/Server/Services/UserNotificationSettingMonitorDefaults.test.ts +248 -0
- package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +43 -2
- package/Tests/Server/Utils/BasicCron.test.ts +283 -0
- package/Tests/Server/Utils/IncomingCallPhoneNumber.test.ts +174 -0
- package/Tests/Types/NetworkDevice/DefaultNetworkDeviceRole.test.ts +417 -0
- package/Tests/UI/Components/Charts/ChartClickDoubleClickDisambiguation.test.tsx +221 -0
- package/Tests/UI/Components/Charts/ChartDoubleClickReset.test.tsx +220 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +751 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +475 -43
- package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +89 -0
- package/Tests/UI/Components/NavBarCommandKGating.test.tsx +103 -8
- package/Tests/UI/Components/SideMenuItem.test.tsx +30 -0
- package/Tests/UI/Utils/TableColumnsToCsv.test.ts +30 -0
- package/Tests/UI/Utils/UseDashboardTimeRangeZoom.test.tsx +187 -0
- package/Tests/Utils/Dashboard/DashboardResourceListSharedArgs.test.ts +302 -0
- package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +242 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleCatalog.test.ts +981 -0
- package/Tests/Utils/Monitor/NetworkTopologyRoleCatalog.test.ts +865 -0
- package/Tests/Utils/NetworkDevice/DeviceRoleKeyUtil.test.ts +538 -0
- package/Tests/Utils/Slo/SloEvaluation.test.ts +171 -0
- package/Tests/Utils/Slug.test.ts +61 -1
- package/Tests/jest.setup.ts +19 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +61 -0
- package/Types/NetworkDevice/DefaultNetworkDeviceRole.ts +145 -0
- package/Types/Permission.ts +45 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +7 -0
- package/UI/Components/Charts/Bar/BarChart.tsx +6 -0
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +9 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +97 -35
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +14 -0
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +97 -35
- package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +13 -0
- package/UI/Components/Charts/Line/LineChart.tsx +7 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +50 -15
- package/UI/Components/Navbar/NavBar.tsx +62 -13
- package/UI/Components/Navbar/NavBarMenuModal.tsx +7 -2
- package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +2 -0
- package/UI/Components/SideMenu/SideMenu.tsx +12 -2
- package/UI/Components/SideMenu/SideMenuItem.tsx +4 -1
- package/UI/Components/SideMenu/SideMenuSection.tsx +14 -2
- package/UI/Utils/UseDashboardTimeRangeZoom.ts +77 -0
- package/Utils/Dashboard/DashboardTimeRangeZoom.ts +118 -0
- package/Utils/Monitor/NetworkDeviceRoleCatalog.ts +232 -0
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +2 -1
- package/Utils/Monitor/NetworkTopologyUtil.ts +116 -33
- package/Utils/NetworkDevice/DeviceRoleKeyUtil.ts +137 -0
- package/Utils/Rules/RulePatternMatchUtil.ts +1 -1
- package/Utils/Slug.ts +34 -1
- package/build/dist/Models/DatabaseModels/AlertState.js +0 -2
- package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Domain.js +1 -1
- package/build/dist/Models/DatabaseModels/Domain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js +1 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MetricType.js +0 -2
- package/build/dist/Models/DatabaseModels/MetricType.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +0 -2
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorTest.js +0 -2
- package/build/dist/Models/DatabaseModels/MonitorTest.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +124 -3
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js +3 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js +677 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageResource.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js +39 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.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/DatabaseService.js +31 -5
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceRoleService.js +70 -0
- package/build/dist/Server/Services/NetworkDeviceRoleService.js.map +1 -0
- package/build/dist/Server/Services/ProjectService.js +65 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +0 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js +105 -0
- package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js.map +1 -0
- package/build/dist/Types/Permission.js +40 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +6 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +79 -31
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +7 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +79 -31
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +14 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +39 -13
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBar.js +44 -9
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +1 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenu.js +9 -6
- package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuItem.js +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuItem.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuSection.js +7 -2
- package/build/dist/UI/Components/SideMenu/SideMenuSection.js.map +1 -1
- package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js +42 -0
- package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js.map +1 -0
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +74 -0
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js +126 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +2 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +67 -32
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js +114 -0
- package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js.map +1 -0
- package/build/dist/Utils/Rules/RulePatternMatchUtil.js +1 -1
- package/build/dist/Utils/Slug.js +31 -1
- package/build/dist/Utils/Slug.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import DEFAULT_NETWORK_DEVICE_ROLES, {
|
|
3
|
+
DefaultNetworkDeviceRole,
|
|
4
|
+
} from "../../../Types/NetworkDevice/DefaultNetworkDeviceRole";
|
|
5
|
+
import {
|
|
6
|
+
FALLBACK_DEVICE_ROLE_KEY,
|
|
7
|
+
buildUniqueDeviceRoleKey,
|
|
8
|
+
deriveDeviceRoleKey,
|
|
9
|
+
} from "../../../Utils/NetworkDevice/DeviceRoleKeyUtil";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* Device roles became per-project rows an operator can rename at will, so the
|
|
13
|
+
* NAME stopped being able to identify one. The KEY took that job: it is what
|
|
14
|
+
* the SNMP classifier's answer is matched against, what a topology payload
|
|
15
|
+
* carries, and what survives a rename.
|
|
16
|
+
*
|
|
17
|
+
* That makes this module load-bearing in a quiet way — nothing here can throw
|
|
18
|
+
* or look broken, it can only produce a key that fails to match later, on a
|
|
19
|
+
* different machine, in a different module. So every branch is pinned, and
|
|
20
|
+
* the three places the derivation is ASYMMETRIC (a multi-word name has its
|
|
21
|
+
* first word lowercased whole, every later word keeps its capitals, and a
|
|
22
|
+
* one-word name is left alone) are pinned loudest, with the reason each is
|
|
23
|
+
* what it is.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
describe("deriveDeviceRoleKey — the shape of a key", () => {
|
|
27
|
+
test("a single word becomes its lowercase self", () => {
|
|
28
|
+
expect(deriveDeviceRoleKey("Router")).toBe("router");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("a multi-word name becomes lowerCamelCase", () => {
|
|
32
|
+
expect(deriveDeviceRoleKey("Load balancer")).toBe("loadBalancer");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("leading and trailing whitespace is not part of any word", () => {
|
|
36
|
+
expect(deriveDeviceRoleKey(" Router ")).toBe("router");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
* The single most important non-obvious rule in the module, and it is
|
|
41
|
+
* asymmetric. A LATER word keeps its interior, so an acronym stays an
|
|
42
|
+
* acronym and the boundaries a reader navigates by survive.
|
|
43
|
+
*/
|
|
44
|
+
test("a later word keeps its capitals, so acronyms stay acronyms", () => {
|
|
45
|
+
expect(deriveDeviceRoleKey("Wireless AP")).toBe("wirelessAP");
|
|
46
|
+
expect(deriveDeviceRoleKey("SD-WAN Edge")).toBe("sdWANEdge");
|
|
47
|
+
expect(deriveDeviceRoleKey("Wi-Fi AP")).toBe("wiFiAP");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
* The other half of the asymmetry: the FIRST word is lowercased whole. Only
|
|
52
|
+
* re-casing its first letter is what used to turn "PoS Terminal" into
|
|
53
|
+
* "poSTerminal" and "IP phone" into "iPPhone" - and the key is a column an
|
|
54
|
+
* operator reads on the settings page.
|
|
55
|
+
*/
|
|
56
|
+
test("the first word is lowercased whole, so a leading acronym reads as a word", () => {
|
|
57
|
+
expect(deriveDeviceRoleKey("IoT Gateway")).toBe("iotGateway");
|
|
58
|
+
expect(deriveDeviceRoleKey("IP phone")).toBe("ipPhone");
|
|
59
|
+
expect(deriveDeviceRoleKey("PoS Terminal")).toBe("posTerminal");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/*
|
|
63
|
+
* ...and a ONE-WORD name is exempt from it, re-cased only at its first
|
|
64
|
+
* letter. A name with no word boundaries has none to normalise, and the
|
|
65
|
+
* exemption is what makes every derived key a fixed point of the deriver:
|
|
66
|
+
* a key is always a single run of letters and digits, so it re-splits into
|
|
67
|
+
* exactly one word. See the fixed-point block below.
|
|
68
|
+
*/
|
|
69
|
+
test("a one-word name keeps its interior, so an already-camelCase name survives", () => {
|
|
70
|
+
expect(deriveDeviceRoleKey("wirelessAccessPoint")).toBe(
|
|
71
|
+
"wirelessAccessPoint",
|
|
72
|
+
);
|
|
73
|
+
expect(deriveDeviceRoleKey("loadBalancer")).toBe("loadBalancer");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test.each([
|
|
77
|
+
["pos-terminal", "posTerminal"],
|
|
78
|
+
["pos_terminal", "posTerminal"],
|
|
79
|
+
["pos terminal", "posTerminal"],
|
|
80
|
+
["pos.terminal", "posTerminal"],
|
|
81
|
+
["pos/terminal", "posTerminal"],
|
|
82
|
+
["pos terminal", "posTerminal"],
|
|
83
|
+
[" pos terminal! ", "posTerminal"],
|
|
84
|
+
])(
|
|
85
|
+
"anything that is not a letter or a digit is a word boundary: %p derives %p",
|
|
86
|
+
(name: string, expected: string) => {
|
|
87
|
+
expect(deriveDeviceRoleKey(name)).toBe(expected);
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
test("digits are word characters, and a key is allowed to start with one", () => {
|
|
92
|
+
/*
|
|
93
|
+
* A key is a lookup value in a database column, never a JavaScript
|
|
94
|
+
* identifier, so "5gRouter" is a perfectly good key and is deliberately
|
|
95
|
+
* not prefixed or rejected. The G is lowercase because "5G" is the first
|
|
96
|
+
* word of a multi-word name and the head of a camelCase key is lowercase;
|
|
97
|
+
* an acronym in a LATER word keeps its capitals ("sdWANEdge").
|
|
98
|
+
*/
|
|
99
|
+
expect(deriveDeviceRoleKey("5G Router")).toBe("5gRouter");
|
|
100
|
+
expect(deriveDeviceRoleKey("3rd party")).toBe("3rdParty");
|
|
101
|
+
expect(deriveDeviceRoleKey("router_1")).toBe("router1");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a derived key only ever contains letters and digits", () => {
|
|
105
|
+
const messyNames: ReadonlyArray<string> = [
|
|
106
|
+
"Core / Distribution Switch",
|
|
107
|
+
"PoE+ Access Switch (2024)",
|
|
108
|
+
" --- Edge --- ",
|
|
109
|
+
"SD-WAN\tEdge\nRouter",
|
|
110
|
+
"role@example.com",
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
for (const name of messyNames) {
|
|
114
|
+
expect(deriveDeviceRoleKey(name)).toMatch(/^[a-zA-Z0-9]+$/);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("deriveDeviceRoleKey — names it cannot build a key from", () => {
|
|
120
|
+
test.each(["", " ", "\t\n", "---", "!!!", "***", "/", "路由器", "…"])(
|
|
121
|
+
"falls back to the reserved key for %p, which has no letters or digits to use",
|
|
122
|
+
(name: string) => {
|
|
123
|
+
/*
|
|
124
|
+
* Returning "" here would fail the column's NOT NULL and surface to the
|
|
125
|
+
* operator as a database error on a form they filled in correctly. The
|
|
126
|
+
* fallback plus the caller's suffixing turns a second such name into
|
|
127
|
+
* "role2" instead.
|
|
128
|
+
*/
|
|
129
|
+
expect(deriveDeviceRoleKey(name)).toBe(FALLBACK_DEVICE_ROLE_KEY);
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
test("the fallback key is 'role'", () => {
|
|
134
|
+
expect(FALLBACK_DEVICE_ROLE_KEY).toBe("role");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/*
|
|
138
|
+
* Worth knowing about rather than worth fixing here: the word split is
|
|
139
|
+
* ASCII-only, so a non-ASCII name keeps only its ASCII letters. A name with
|
|
140
|
+
* NO ASCII letters at all ("路由器") therefore lands on the fallback, and a
|
|
141
|
+
* project naming its roles in a non-Latin script would get "role", "role2",
|
|
142
|
+
* "role3". The keys still work — they are opaque identifiers and the NAME is
|
|
143
|
+
* what the operator ever sees — but they carry no meaning.
|
|
144
|
+
*/
|
|
145
|
+
test("non-ASCII letters are treated as separators, not as word characters", () => {
|
|
146
|
+
expect(deriveDeviceRoleKey("Café Router")).toBe("cafRouter");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("deriveDeviceRoleKey — length", () => {
|
|
151
|
+
test("a 200-character name is capped at the column's 100 characters", () => {
|
|
152
|
+
const longName: string = "a".repeat(200);
|
|
153
|
+
|
|
154
|
+
expect(longName).toHaveLength(200);
|
|
155
|
+
expect(deriveDeviceRoleKey(longName)).toBe("a".repeat(100));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("a long multi-word name is capped at 100 too", () => {
|
|
159
|
+
// 8 words x 25 chars, so the un-capped key would be far past the limit.
|
|
160
|
+
const longName: string = "Distribution Layer Switch ".repeat(8).trim();
|
|
161
|
+
|
|
162
|
+
expect(longName.length).toBeGreaterThan(100);
|
|
163
|
+
expect(deriveDeviceRoleKey(longName)).toHaveLength(100);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("a name that derives exactly 100 characters is not truncated", () => {
|
|
167
|
+
const exactName: string = "b".repeat(100);
|
|
168
|
+
|
|
169
|
+
expect(deriveDeviceRoleKey(exactName)).toBe(exactName);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("deriveDeviceRoleKey — purity", () => {
|
|
174
|
+
test("the same name always derives the same key", () => {
|
|
175
|
+
const names: ReadonlyArray<string> = [
|
|
176
|
+
"Wireless AP",
|
|
177
|
+
"PoS Terminal",
|
|
178
|
+
"---",
|
|
179
|
+
"",
|
|
180
|
+
"5G Router",
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
for (const name of names) {
|
|
184
|
+
expect(deriveDeviceRoleKey(name)).toBe(deriveDeviceRoleKey(name));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
/*
|
|
189
|
+
* Idempotence is what makes the key safe to re-derive. The service derives
|
|
190
|
+
* on create; a later import, backfill or copy of a project may well hand the
|
|
191
|
+
* KEY back in as a name, and doing so must not shift the identity of the
|
|
192
|
+
* role. It holds for every input because a derived key contains only
|
|
193
|
+
* letters and digits (so it re-splits into exactly one word) whose first
|
|
194
|
+
* character is already lowercase.
|
|
195
|
+
*/
|
|
196
|
+
test("running a derived key back through the deriver returns it unchanged", () => {
|
|
197
|
+
const names: ReadonlyArray<string> = [
|
|
198
|
+
"Router",
|
|
199
|
+
"Wireless AP",
|
|
200
|
+
"IP phone",
|
|
201
|
+
"Load balancer",
|
|
202
|
+
"PoS Terminal",
|
|
203
|
+
"POS terminal!",
|
|
204
|
+
"SD-WAN Edge",
|
|
205
|
+
"5G Router",
|
|
206
|
+
"Café Router",
|
|
207
|
+
"路由器",
|
|
208
|
+
"",
|
|
209
|
+
"---",
|
|
210
|
+
"a".repeat(200),
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
for (const name of names) {
|
|
214
|
+
const key: string = deriveDeviceRoleKey(name);
|
|
215
|
+
expect(deriveDeviceRoleKey(key)).toBe(key);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
/*
|
|
221
|
+
* The eleven built-in roles are seeded with an EXPLICIT key rather than one
|
|
222
|
+
* derived from their name. These tests exist to show why that is not
|
|
223
|
+
* belt-and-braces: two of the eleven names do not derive their seeded key, so
|
|
224
|
+
* a seeder that let the service derive would create a project whose "Wireless
|
|
225
|
+
* AP" row is keyed "wirelessAP" — a key the SNMP classifier, which only ever
|
|
226
|
+
* says "wirelessAccessPoint", could never match.
|
|
227
|
+
*/
|
|
228
|
+
describe("deriveDeviceRoleKey — the eleven built-in default names", () => {
|
|
229
|
+
test.each([
|
|
230
|
+
["Router", "router"],
|
|
231
|
+
["Switch", "switch"],
|
|
232
|
+
["Firewall", "firewall"],
|
|
233
|
+
// Does NOT round-trip: the seeded key is "wirelessAccessPoint".
|
|
234
|
+
["Wireless AP", "wirelessAP"],
|
|
235
|
+
["Load balancer", "loadBalancer"],
|
|
236
|
+
["Server", "server"],
|
|
237
|
+
["Storage", "storage"],
|
|
238
|
+
["Printer", "printer"],
|
|
239
|
+
["Camera", "camera"],
|
|
240
|
+
// Does NOT round-trip: the seeded key is "phone".
|
|
241
|
+
["IP phone", "ipPhone"],
|
|
242
|
+
["Host", "host"],
|
|
243
|
+
])("the default name %p derives %p", (name: string, expected: string) => {
|
|
244
|
+
expect(deriveDeviceRoleKey(name)).toBe(expected);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("exactly two of the eleven default names do not derive their seeded key", () => {
|
|
248
|
+
const mismatched: Array<string> = DEFAULT_NETWORK_DEVICE_ROLES.filter(
|
|
249
|
+
(role: DefaultNetworkDeviceRole): boolean => {
|
|
250
|
+
return deriveDeviceRoleKey(role.name) !== role.key;
|
|
251
|
+
},
|
|
252
|
+
).map((role: DefaultNetworkDeviceRole): string => {
|
|
253
|
+
return role.name;
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
/*
|
|
257
|
+
* "Wireless AP" is an abbreviation of its key and "IP phone" is a
|
|
258
|
+
* different word from its key, so neither can be recovered from the name.
|
|
259
|
+
* This is exactly the reason ProjectService.addDefaultNetworkDeviceRoles
|
|
260
|
+
* and the BackfillNetworkDeviceRoles migration pass `key` explicitly
|
|
261
|
+
* instead of letting NetworkDeviceRoleService.onBeforeCreate derive it.
|
|
262
|
+
* If this list ever shrinks to zero the explicit key is still correct —
|
|
263
|
+
* but if it GROWS, a new default was added whose name and key disagree,
|
|
264
|
+
* and the seeder must keep passing the key.
|
|
265
|
+
*/
|
|
266
|
+
expect(mismatched).toEqual(["Wireless AP", "IP phone"]);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("the other nine default names do derive their seeded key", () => {
|
|
270
|
+
const matched: Array<string> = DEFAULT_NETWORK_DEVICE_ROLES.filter(
|
|
271
|
+
(role: DefaultNetworkDeviceRole): boolean => {
|
|
272
|
+
return deriveDeviceRoleKey(role.name) === role.key;
|
|
273
|
+
},
|
|
274
|
+
).map((role: DefaultNetworkDeviceRole): string => {
|
|
275
|
+
return role.key;
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
expect(matched).toEqual([
|
|
279
|
+
"router",
|
|
280
|
+
"switch",
|
|
281
|
+
"firewall",
|
|
282
|
+
"loadBalancer",
|
|
283
|
+
"server",
|
|
284
|
+
"storage",
|
|
285
|
+
"printer",
|
|
286
|
+
"camera",
|
|
287
|
+
"host",
|
|
288
|
+
]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("every seeded key is itself a fixed point of the deriver", () => {
|
|
292
|
+
/*
|
|
293
|
+
* Even the two that the NAME cannot produce: the KEY must still survive
|
|
294
|
+
* being re-derived, because a backfill or an import can hand the key in as
|
|
295
|
+
* a name and the role's identity must not move.
|
|
296
|
+
*/
|
|
297
|
+
for (const role of DEFAULT_NETWORK_DEVICE_ROLES) {
|
|
298
|
+
expect(deriveDeviceRoleKey(role.key)).toBe(role.key);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
/*
|
|
304
|
+
* Three spellings of one name derive one key, letter for letter. That is what
|
|
305
|
+
* lowercasing the first word buys: without it these were "poSTerminal",
|
|
306
|
+
* "posTerminal" and "pOSTerminal" — three near-duplicate rows in the settings
|
|
307
|
+
* list, differing only in case, each of them a key an operator would read as
|
|
308
|
+
* a bug.
|
|
309
|
+
*
|
|
310
|
+
* Pinned here because the exact strings are what land in the database, and
|
|
311
|
+
* because the collapse still has a second line of defence one level up:
|
|
312
|
+
* buildUniqueDeviceRoleKey compares case-insensitively and normalizeRoleKey
|
|
313
|
+
* (NetworkDeviceRoleCatalog) looks keys up case-insensitively, so even a name
|
|
314
|
+
* whose derived key differs only in case from an existing one is suffixed
|
|
315
|
+
* rather than duplicated.
|
|
316
|
+
*/
|
|
317
|
+
describe("deriveDeviceRoleKey — three spellings of one name derive one key", () => {
|
|
318
|
+
const nearDuplicates: ReadonlyArray<string> = [
|
|
319
|
+
"PoS Terminal",
|
|
320
|
+
"pos-terminal",
|
|
321
|
+
"POS terminal!",
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
test.each([
|
|
325
|
+
["PoS Terminal", "posTerminal"],
|
|
326
|
+
["pos-terminal", "posTerminal"],
|
|
327
|
+
["POS terminal!", "posTerminal"],
|
|
328
|
+
])(
|
|
329
|
+
"%p derives %p — three spellings of one name, one key",
|
|
330
|
+
(name: string, expected: string) => {
|
|
331
|
+
expect(deriveDeviceRoleKey(name)).toBe(expected);
|
|
332
|
+
},
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
test("all three are the same key once cased the way every lookup cases it", () => {
|
|
336
|
+
const lowered: Array<string> = nearDuplicates.map(
|
|
337
|
+
(name: string): string => {
|
|
338
|
+
return deriveDeviceRoleKey(name).toLowerCase();
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
expect(new Set<string>(lowered).size).toBe(1);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("so the second and third of them are suffixed rather than duplicated", () => {
|
|
346
|
+
const taken: Set<string> = new Set<string>();
|
|
347
|
+
const assigned: Array<string> = nearDuplicates.map(
|
|
348
|
+
(name: string): string => {
|
|
349
|
+
const key: string = buildUniqueDeviceRoleKey(name, taken);
|
|
350
|
+
taken.add(key);
|
|
351
|
+
return key;
|
|
352
|
+
},
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
expect(assigned).toEqual(["posTerminal", "posTerminal2", "posTerminal3"]);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe("buildUniqueDeviceRoleKey — when the base key is free", () => {
|
|
360
|
+
test("an empty project gets the plain derived key", () => {
|
|
361
|
+
expect(buildUniqueDeviceRoleKey("Router", new Set<string>())).toBe(
|
|
362
|
+
"router",
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("keys taken by unrelated roles do not perturb it", () => {
|
|
367
|
+
expect(
|
|
368
|
+
buildUniqueDeviceRoleKey(
|
|
369
|
+
"Load balancer",
|
|
370
|
+
new Set<string>(["router", "switch", "firewall"]),
|
|
371
|
+
),
|
|
372
|
+
).toBe("loadBalancer");
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("a name with no usable letters still gets the fallback when it is free", () => {
|
|
376
|
+
expect(buildUniqueDeviceRoleKey("---", new Set<string>())).toBe(
|
|
377
|
+
FALLBACK_DEVICE_ROLE_KEY,
|
|
378
|
+
);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
describe("buildUniqueDeviceRoleKey — when the base key is taken", () => {
|
|
383
|
+
test("the first collision is suffixed 2, never 1", () => {
|
|
384
|
+
// "router1" would read as the first of a series that does not exist.
|
|
385
|
+
expect(
|
|
386
|
+
buildUniqueDeviceRoleKey("Router", new Set<string>(["router"])),
|
|
387
|
+
).toBe("router2");
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("suffixes count up past every taken candidate", () => {
|
|
391
|
+
expect(
|
|
392
|
+
buildUniqueDeviceRoleKey(
|
|
393
|
+
"Router",
|
|
394
|
+
new Set<string>(["router", "router2", "router3"]),
|
|
395
|
+
),
|
|
396
|
+
).toBe("router4");
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("a gap in the sequence is filled rather than skipped", () => {
|
|
400
|
+
// Roles get deleted; the next one should reuse the hole, not run away.
|
|
401
|
+
expect(
|
|
402
|
+
buildUniqueDeviceRoleKey(
|
|
403
|
+
"Router",
|
|
404
|
+
new Set<string>(["router", "router3", "router4"]),
|
|
405
|
+
),
|
|
406
|
+
).toBe("router2");
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("the suffix goes on the derived key, not on the name", () => {
|
|
410
|
+
expect(
|
|
411
|
+
buildUniqueDeviceRoleKey(
|
|
412
|
+
"Load balancer",
|
|
413
|
+
new Set<string>(["loadBalancer"]),
|
|
414
|
+
),
|
|
415
|
+
).toBe("loadBalancer2");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("a punctuation-only name collides on the fallback and becomes 'role2'", () => {
|
|
419
|
+
expect(
|
|
420
|
+
buildUniqueDeviceRoleKey(
|
|
421
|
+
"!!!",
|
|
422
|
+
new Set<string>([FALLBACK_DEVICE_ROLE_KEY]),
|
|
423
|
+
),
|
|
424
|
+
).toBe("role2");
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
describe("buildUniqueDeviceRoleKey — comparison is case- and whitespace-insensitive", () => {
|
|
429
|
+
/*
|
|
430
|
+
* It has to be: normalizeRoleKey lowercases and trims before looking a role
|
|
431
|
+
* up, so two keys differing only in case would be one key at lookup time and
|
|
432
|
+
* whichever row the index happened to keep would silently win.
|
|
433
|
+
*/
|
|
434
|
+
test.each(["ROUTER", "Router", "RoUtEr"])(
|
|
435
|
+
"a taken key spelled %p still blocks the base key",
|
|
436
|
+
(taken: string) => {
|
|
437
|
+
expect(buildUniqueDeviceRoleKey("Router", new Set<string>([taken]))).toBe(
|
|
438
|
+
"router2",
|
|
439
|
+
);
|
|
440
|
+
},
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
test("a taken key with surrounding whitespace still blocks the base key", () => {
|
|
444
|
+
expect(
|
|
445
|
+
buildUniqueDeviceRoleKey("Router", new Set<string>([" router "])),
|
|
446
|
+
).toBe("router2");
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("the suffixed candidates are compared case-insensitively as well", () => {
|
|
450
|
+
expect(
|
|
451
|
+
buildUniqueDeviceRoleKey(
|
|
452
|
+
"Router",
|
|
453
|
+
new Set<string>(["router", "ROUTER2"]),
|
|
454
|
+
),
|
|
455
|
+
).toBe("router3");
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("a name whose derived key differs only in case from a taken key is suffixed", () => {
|
|
459
|
+
// "wirelessAP" vs a hand-imported "WIRELESSAP" is one key at lookup time.
|
|
460
|
+
expect(
|
|
461
|
+
buildUniqueDeviceRoleKey("Wireless AP", new Set<string>(["WIRELESSAP"])),
|
|
462
|
+
).toBe("wirelessAP2");
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe("buildUniqueDeviceRoleKey — the search is bounded", () => {
|
|
467
|
+
type TakenKeysFunction = (base: string, upTo: number) => Set<string>;
|
|
468
|
+
|
|
469
|
+
const takenThrough: TakenKeysFunction = (
|
|
470
|
+
base: string,
|
|
471
|
+
upTo: number,
|
|
472
|
+
): Set<string> => {
|
|
473
|
+
const taken: Set<string> = new Set<string>([base]);
|
|
474
|
+
for (let suffix: number = 2; suffix <= upTo; suffix++) {
|
|
475
|
+
taken.add(`${base}${suffix}`);
|
|
476
|
+
}
|
|
477
|
+
return taken;
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
test("the last suffix it will ever hand out is 999", () => {
|
|
481
|
+
expect(
|
|
482
|
+
buildUniqueDeviceRoleKey("Router", takenThrough("router", 998)),
|
|
483
|
+
).toBe("router999");
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
/*
|
|
487
|
+
* Past the bound the caller gets the base key back, which the unique index
|
|
488
|
+
* on (projectId, key) then rejects. That is deliberate: a loud failure on a
|
|
489
|
+
* project with a thousand identically-named roles beats a loop that spins.
|
|
490
|
+
*/
|
|
491
|
+
test("a project that has exhausted the bound gets the base key back, so the database refuses the create", () => {
|
|
492
|
+
const taken: Set<string> = takenThrough("router", 999);
|
|
493
|
+
|
|
494
|
+
expect(taken.size).toBe(999);
|
|
495
|
+
expect(buildUniqueDeviceRoleKey("Router", taken)).toBe("router");
|
|
496
|
+
expect(taken.has("router")).toBe(true);
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
describe("buildUniqueDeviceRoleKey — purity", () => {
|
|
501
|
+
test("the taken set is not mutated", () => {
|
|
502
|
+
const taken: Set<string> = new Set<string>(["router", "ROUTER2"]);
|
|
503
|
+
|
|
504
|
+
buildUniqueDeviceRoleKey("Router", taken);
|
|
505
|
+
|
|
506
|
+
expect(Array.from(taken)).toEqual(["router", "ROUTER2"]);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("the same name and the same taken set always give the same key", () => {
|
|
510
|
+
const taken: ReadonlySet<string> = new Set<string>(["router", "router2"]);
|
|
511
|
+
|
|
512
|
+
expect(buildUniqueDeviceRoleKey("Router", taken)).toBe(
|
|
513
|
+
buildUniqueDeviceRoleKey("Router", taken),
|
|
514
|
+
);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test("seeding a fresh project hands every default name an unsuffixed key", () => {
|
|
518
|
+
/*
|
|
519
|
+
* The real sequence the seeder runs: eleven names, each checked against
|
|
520
|
+
* the keys already assigned. None of the eleven derive the same key, so
|
|
521
|
+
* none of them may pick up a numeric suffix — a seeded "router2" would be
|
|
522
|
+
* a role the classifier can never match.
|
|
523
|
+
*/
|
|
524
|
+
const taken: Set<string> = new Set<string>();
|
|
525
|
+
const assigned: Array<string> = DEFAULT_NETWORK_DEVICE_ROLES.map(
|
|
526
|
+
(role: DefaultNetworkDeviceRole): string => {
|
|
527
|
+
const key: string = buildUniqueDeviceRoleKey(role.name, taken);
|
|
528
|
+
taken.add(key);
|
|
529
|
+
return key;
|
|
530
|
+
},
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
for (const key of assigned) {
|
|
534
|
+
expect(key).not.toMatch(/\d$/);
|
|
535
|
+
}
|
|
536
|
+
expect(new Set<string>(assigned).size).toBe(assigned.length);
|
|
537
|
+
});
|
|
538
|
+
});
|