@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,865 @@
|
|
|
1
|
+
import NetworkTopologyUtil, {
|
|
2
|
+
TopologyBuildResult,
|
|
3
|
+
TopologyDeviceInput,
|
|
4
|
+
TopologyEndpointInput,
|
|
5
|
+
} from "../../../Utils/Monitor/NetworkTopologyUtil";
|
|
6
|
+
import { TopologyDeviceRoleInput } from "../../../Utils/Monitor/NetworkDeviceRoleCatalog";
|
|
7
|
+
import { NetworkTopologyNode } from "../../../Types/Monitor/SnmpMonitor/NetworkTopology";
|
|
8
|
+
import DEFAULT_NETWORK_DEVICE_ROLES, {
|
|
9
|
+
DefaultNetworkDeviceRole,
|
|
10
|
+
} from "../../../Types/NetworkDevice/DefaultNetworkDeviceRole";
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Device roles are rows now (NetworkDeviceRole), one set per project, and the
|
|
14
|
+
* client is deliberately NOT handed the table to look things up in. Instead
|
|
15
|
+
* buildTopology stamps the project's answers — label, silhouette, "is this a
|
|
16
|
+
* core device", "is this worth walking with SNMP" — onto each node as it
|
|
17
|
+
* builds it, so the shape module, the three layouts, the legend and the adopt
|
|
18
|
+
* flow all keep their existing pure signatures.
|
|
19
|
+
*
|
|
20
|
+
* NetworkDeviceRoleCatalog's units are tested on their own; this file is the
|
|
21
|
+
* wiring: what actually lands on a node once a real graph has been built.
|
|
22
|
+
*
|
|
23
|
+
* The load-bearing invariant, and the reason most of the first block exists:
|
|
24
|
+
* every stamped field is ADDITIVE and OPTIONAL. A project with no roles
|
|
25
|
+
* configured, a caller that predates the parameter, and a payload serialised
|
|
26
|
+
* before the table existed must all produce exactly the nodes they produced
|
|
27
|
+
* before — which means the fields have to be ABSENT, not present-and-undefined,
|
|
28
|
+
* because "absent means fall back to the built-in" is the contract every
|
|
29
|
+
* consumer downstream reads.
|
|
30
|
+
*/
|
|
31
|
+
describe("NetworkTopologyUtil.buildTopology — the project's configured device roles", () => {
|
|
32
|
+
const now: Date = new Date("2026-07-22T12:00:00Z");
|
|
33
|
+
const fresh: Date = new Date("2026-07-22T11:55:00Z");
|
|
34
|
+
|
|
35
|
+
// Every field the role catalogue is allowed to put on a node.
|
|
36
|
+
const ROLE_STAMP_FIELDS: ReadonlyArray<string> = [
|
|
37
|
+
"roleId",
|
|
38
|
+
"roleKey",
|
|
39
|
+
"roleLabel",
|
|
40
|
+
"roleShape",
|
|
41
|
+
"isCoreLayerRole",
|
|
42
|
+
"isSnmpWalkableRole",
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const makeDevice: (
|
|
46
|
+
id: string,
|
|
47
|
+
name: string,
|
|
48
|
+
overrides?: Partial<TopologyDeviceInput>,
|
|
49
|
+
) => TopologyDeviceInput = (
|
|
50
|
+
id: string,
|
|
51
|
+
name: string,
|
|
52
|
+
overrides?: Partial<TopologyDeviceInput>,
|
|
53
|
+
): TopologyDeviceInput => {
|
|
54
|
+
return {
|
|
55
|
+
id,
|
|
56
|
+
name,
|
|
57
|
+
lastSeenAt: fresh,
|
|
58
|
+
...overrides,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const nodeById: (
|
|
63
|
+
result: TopologyBuildResult,
|
|
64
|
+
id: string,
|
|
65
|
+
) => NetworkTopologyNode | undefined = (
|
|
66
|
+
result: TopologyBuildResult,
|
|
67
|
+
id: string,
|
|
68
|
+
): NetworkTopologyNode | undefined => {
|
|
69
|
+
return result.nodes.find((node: NetworkTopologyNode) => {
|
|
70
|
+
return node.id === id;
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/*
|
|
75
|
+
* Absence, not `undefined`. A consumer that checks with `in`, or a payload
|
|
76
|
+
* that goes through JSON, cannot tell the two apart afterwards — and a node
|
|
77
|
+
* carrying `roleShape: undefined` would still be a node whose author claimed
|
|
78
|
+
* to know something about its shape.
|
|
79
|
+
*/
|
|
80
|
+
const expectNoRoleStamp: (node: NetworkTopologyNode) => void = (
|
|
81
|
+
node: NetworkTopologyNode,
|
|
82
|
+
): void => {
|
|
83
|
+
for (const field of ROLE_STAMP_FIELDS) {
|
|
84
|
+
expect(node).not.toHaveProperty(field);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
* The eleven roles seeded into every project, in the shape the API hands
|
|
90
|
+
* buildTopology. Built from the shared defaults rather than retyped, so a
|
|
91
|
+
* change to what a fresh project starts with cannot pass here and fail in
|
|
92
|
+
* production.
|
|
93
|
+
*/
|
|
94
|
+
const seededRoles: () => Array<TopologyDeviceRoleInput> =
|
|
95
|
+
(): Array<TopologyDeviceRoleInput> => {
|
|
96
|
+
return DEFAULT_NETWORK_DEVICE_ROLES.map(
|
|
97
|
+
(role: DefaultNetworkDeviceRole): TopologyDeviceRoleInput => {
|
|
98
|
+
return {
|
|
99
|
+
id: `role-${role.key}`,
|
|
100
|
+
key: role.key,
|
|
101
|
+
name: role.name,
|
|
102
|
+
topologyShape: role.topologyShape,
|
|
103
|
+
isCoreLayer: role.isCoreLayer,
|
|
104
|
+
isSnmpWalkable: role.isSnmpWalkable,
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// The seeded set with one row edited, which is what "renaming a role" is.
|
|
111
|
+
const seededRolesWith: (
|
|
112
|
+
key: string,
|
|
113
|
+
patch: Partial<TopologyDeviceRoleInput>,
|
|
114
|
+
) => Array<TopologyDeviceRoleInput> = (
|
|
115
|
+
key: string,
|
|
116
|
+
patch: Partial<TopologyDeviceRoleInput>,
|
|
117
|
+
): Array<TopologyDeviceRoleInput> => {
|
|
118
|
+
return seededRoles().map(
|
|
119
|
+
(role: TopologyDeviceRoleInput): TopologyDeviceRoleInput => {
|
|
120
|
+
return role.key === key ? { ...role, ...patch } : role;
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/*
|
|
126
|
+
* One small estate that exercises all three node kinds at once: a managed
|
|
127
|
+
* device the classifier can place, one it cannot, a CDP peer nobody manages,
|
|
128
|
+
* and two ARP/FDB endpoints. Reused so the "before" and "after" pictures are
|
|
129
|
+
* the same graph.
|
|
130
|
+
*/
|
|
131
|
+
const estateDevices: () => Array<TopologyDeviceInput> =
|
|
132
|
+
(): Array<TopologyDeviceInput> => {
|
|
133
|
+
return [
|
|
134
|
+
makeDevice("d-router", "core-rtr-1", {
|
|
135
|
+
sysName: "core-rtr-1",
|
|
136
|
+
deviceModel: "ISR4331/K9",
|
|
137
|
+
}),
|
|
138
|
+
makeDevice("d-switch", "access-1", {
|
|
139
|
+
sysName: "access-1",
|
|
140
|
+
sysDescr: "Cisco IOS Software, Catalyst 9300-48P",
|
|
141
|
+
cdpNeighbors: [
|
|
142
|
+
{
|
|
143
|
+
localInterfaceIndex: 1,
|
|
144
|
+
remoteDeviceId: "ap-lobby",
|
|
145
|
+
remotePlatform: "cisco AIR-CAP3702I-A-K9",
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
}),
|
|
149
|
+
// Nothing walks this one and its name says nothing: honestly "unknown".
|
|
150
|
+
makeDevice("d-blank", "device-42"),
|
|
151
|
+
];
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const estateEndpoints: () => Array<TopologyEndpointInput> =
|
|
155
|
+
(): Array<TopologyEndpointInput> => {
|
|
156
|
+
return [
|
|
157
|
+
{
|
|
158
|
+
id: "ep-phone",
|
|
159
|
+
macAddress: "aa:bb:cc:dd:ee:01",
|
|
160
|
+
classification: "VoIP handset",
|
|
161
|
+
attachedNetworkDeviceId: "d-switch",
|
|
162
|
+
lastSeenAt: fresh,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: "ep-plain",
|
|
166
|
+
macAddress: "aa:bb:cc:dd:ee:02",
|
|
167
|
+
attachedNetworkDeviceId: "d-switch",
|
|
168
|
+
lastSeenAt: fresh,
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
describe("a project that has configured nothing", () => {
|
|
174
|
+
it("stamps no role fields at all when the catalogue argument is omitted", () => {
|
|
175
|
+
/*
|
|
176
|
+
* The compatibility case that matters most: this is every caller written
|
|
177
|
+
* before the parameter existed, and every project whose backfill has not
|
|
178
|
+
* run yet. The node must come out byte-identical to the one built before
|
|
179
|
+
* roles were configurable, which means `role` intact and not one of the
|
|
180
|
+
* six new fields present.
|
|
181
|
+
*/
|
|
182
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
183
|
+
estateDevices(),
|
|
184
|
+
now,
|
|
185
|
+
[],
|
|
186
|
+
estateEndpoints(),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect(nodeById(result, "d-router")!.role).toBe("router");
|
|
190
|
+
expect(nodeById(result, "d-switch")!.role).toBe("switch");
|
|
191
|
+
expect(nodeById(result, "d-blank")!.role).toBe("unknown");
|
|
192
|
+
expect(nodeById(result, "unmanaged:ap-lobby")!.role).toBe(
|
|
193
|
+
"wirelessAccessPoint",
|
|
194
|
+
);
|
|
195
|
+
expect(nodeById(result, "endpoint:ep-phone")!.role).toBe("phone");
|
|
196
|
+
expect(nodeById(result, "endpoint:ep-plain")!.role).toBe("host");
|
|
197
|
+
|
|
198
|
+
for (const node of result.nodes) {
|
|
199
|
+
expectNoRoleStamp(node);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("treats an empty catalogue exactly as it treats no catalogue", () => {
|
|
204
|
+
/*
|
|
205
|
+
* A project that deleted every role is not a project asking for blank
|
|
206
|
+
* nodes — it is a project with nothing to say, which is the same thing
|
|
207
|
+
* the omitted argument says. Compared whole rather than field by field
|
|
208
|
+
* so a future field cannot be added to one path and not the other.
|
|
209
|
+
*/
|
|
210
|
+
const omitted: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
211
|
+
estateDevices(),
|
|
212
|
+
now,
|
|
213
|
+
[],
|
|
214
|
+
estateEndpoints(),
|
|
215
|
+
);
|
|
216
|
+
const empty: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
217
|
+
estateDevices(),
|
|
218
|
+
now,
|
|
219
|
+
[],
|
|
220
|
+
estateEndpoints(),
|
|
221
|
+
[],
|
|
222
|
+
new Set<string>(),
|
|
223
|
+
[],
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(empty.nodes).toStrictEqual(omitted.nodes);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("stamps nothing from rows that carry no usable key", () => {
|
|
230
|
+
/*
|
|
231
|
+
* A key is what a row is FOUND by, so a row without one cannot be found
|
|
232
|
+
* — and must not be silently applied to everything either. Blank keys
|
|
233
|
+
* are what a half-written import leaves behind.
|
|
234
|
+
*/
|
|
235
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
236
|
+
estateDevices(),
|
|
237
|
+
now,
|
|
238
|
+
[],
|
|
239
|
+
[],
|
|
240
|
+
[],
|
|
241
|
+
new Set<string>(),
|
|
242
|
+
[
|
|
243
|
+
{ id: "role-a", key: "", name: "Nameless" },
|
|
244
|
+
{ id: "role-b", key: " ", name: "Whitespace" },
|
|
245
|
+
],
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
expect(nodeById(result, "d-router")!.role).toBe("router");
|
|
249
|
+
for (const node of result.nodes) {
|
|
250
|
+
expectNoRoleStamp(node);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe("a role the classifier chose, drawn the way the project configured it", () => {
|
|
256
|
+
it("relabels a device the operator never touched when its role is renamed", () => {
|
|
257
|
+
/*
|
|
258
|
+
* The whole point of keying on the classifier's answer. Nobody has ever
|
|
259
|
+
* opened this router's settings — the role came from its model string —
|
|
260
|
+
* and renaming the "router" row still has to reach it, because otherwise
|
|
261
|
+
* a rename only ever applies to devices somebody edited by hand.
|
|
262
|
+
*/
|
|
263
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
264
|
+
estateDevices(),
|
|
265
|
+
now,
|
|
266
|
+
[],
|
|
267
|
+
[],
|
|
268
|
+
[],
|
|
269
|
+
new Set<string>(),
|
|
270
|
+
seededRolesWith("router", {
|
|
271
|
+
name: "Edge Router",
|
|
272
|
+
topologyShape: "hexagon",
|
|
273
|
+
}),
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const router: NetworkTopologyNode = nodeById(result, "d-router")!;
|
|
277
|
+
// The classifier's vocabulary is untouched — only the presentation moved.
|
|
278
|
+
expect(router.role).toBe("router");
|
|
279
|
+
expect(router.roleKey).toBe("router");
|
|
280
|
+
expect(router.roleLabel).toBe("Edge Router");
|
|
281
|
+
expect(router.roleShape).toBe("hexagon");
|
|
282
|
+
expect(router.isCoreLayerRole).toBe(true);
|
|
283
|
+
expect(router.isSnmpWalkableRole).toBe(true);
|
|
284
|
+
// So a form can preselect the row the map is already showing.
|
|
285
|
+
expect(router.roleId).toBe("role-router");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("carries a role that was turned OFF the core layer through as false", () => {
|
|
289
|
+
/*
|
|
290
|
+
* The tiered and radial layouts read isCoreLayerRole BEFORE `role`, so
|
|
291
|
+
* `false` is a real instruction — "draw routers with the access kit" —
|
|
292
|
+
* and not the absence of one. If a false were dropped as falsy the node
|
|
293
|
+
* would silently fall back to the built-in core set and the layout would
|
|
294
|
+
* ignore what the project configured.
|
|
295
|
+
*/
|
|
296
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
297
|
+
estateDevices(),
|
|
298
|
+
now,
|
|
299
|
+
[],
|
|
300
|
+
[],
|
|
301
|
+
[],
|
|
302
|
+
new Set<string>(),
|
|
303
|
+
seededRolesWith("router", { isCoreLayer: false }),
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const router: NetworkTopologyNode = nodeById(result, "d-router")!;
|
|
307
|
+
expect(router.isCoreLayerRole).toBe(false);
|
|
308
|
+
expect(router).toHaveProperty("isCoreLayerRole");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("omits the flags a row does not state rather than inventing them", () => {
|
|
312
|
+
/*
|
|
313
|
+
* A row that says nothing about the core layer is not a row saying "not
|
|
314
|
+
* core". Absent has to stay absent so the client falls back to the
|
|
315
|
+
* built-in answer for `role`, which is the pre-feature behaviour.
|
|
316
|
+
*/
|
|
317
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
318
|
+
estateDevices(),
|
|
319
|
+
now,
|
|
320
|
+
[],
|
|
321
|
+
[],
|
|
322
|
+
[],
|
|
323
|
+
new Set<string>(),
|
|
324
|
+
[{ key: "router", name: "Edge Router" }],
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const router: NetworkTopologyNode = nodeById(result, "d-router")!;
|
|
328
|
+
expect(router.roleKey).toBe("router");
|
|
329
|
+
expect(router.roleLabel).toBe("Edge Router");
|
|
330
|
+
expect(router).not.toHaveProperty("isCoreLayerRole");
|
|
331
|
+
expect(router).not.toHaveProperty("isSnmpWalkableRole");
|
|
332
|
+
expect(router).not.toHaveProperty("roleShape");
|
|
333
|
+
// No row id to preselect, because this catalogue carried none.
|
|
334
|
+
expect(router).not.toHaveProperty("roleId");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("drops a silhouette the renderer has no geometry for", () => {
|
|
338
|
+
/*
|
|
339
|
+
* topologyShape is free text — adding a shape must not need a migration
|
|
340
|
+
* — so a value the renderer cannot draw is dropped and the node falls
|
|
341
|
+
* back to the built-in shape for its role. Passing it through would hand
|
|
342
|
+
* the renderer a shape with no path and draw nothing at all.
|
|
343
|
+
*/
|
|
344
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
345
|
+
estateDevices(),
|
|
346
|
+
now,
|
|
347
|
+
[],
|
|
348
|
+
[],
|
|
349
|
+
[],
|
|
350
|
+
new Set<string>(),
|
|
351
|
+
seededRolesWith("router", { topologyShape: "sparkle" }),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
const router: NetworkTopologyNode = nodeById(result, "d-router")!;
|
|
355
|
+
// The rest of the row still applies; only the unusable field is dropped.
|
|
356
|
+
expect(router.roleLabel).toBe("Router");
|
|
357
|
+
expect(router).not.toHaveProperty("roleShape");
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("stamps the catalogue's own spelling of a key so the legend groups once", () => {
|
|
361
|
+
/*
|
|
362
|
+
* The legend groups by roleKey. A device that got its key from the
|
|
363
|
+
* classifier ("switch") and one an operator assigned ("switch") must
|
|
364
|
+
* therefore land on the SAME string, or one estate's switches split into
|
|
365
|
+
* two legend entries that read identically.
|
|
366
|
+
*/
|
|
367
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
368
|
+
[
|
|
369
|
+
makeDevice("d1", "access-1", {
|
|
370
|
+
sysDescr: "Cisco IOS Software, Catalyst 9300-48P",
|
|
371
|
+
}),
|
|
372
|
+
makeDevice("d2", "ping-only-9", { deviceRole: "switch" }),
|
|
373
|
+
],
|
|
374
|
+
now,
|
|
375
|
+
[],
|
|
376
|
+
[],
|
|
377
|
+
[],
|
|
378
|
+
new Set<string>(),
|
|
379
|
+
[
|
|
380
|
+
{
|
|
381
|
+
id: "role-switch",
|
|
382
|
+
key: "Switch",
|
|
383
|
+
name: "Access Switch",
|
|
384
|
+
topologyShape: "rounded-square",
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
expect(nodeById(result, "d1")!.roleKey).toBe("Switch");
|
|
390
|
+
expect(nodeById(result, "d2")!.roleKey).toBe("Switch");
|
|
391
|
+
expect(nodeById(result, "d1")!.roleLabel).toBe("Access Switch");
|
|
392
|
+
expect(nodeById(result, "d2")!.roleLabel).toBe("Access Switch");
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("gives a node the classifier declined to place no stamp at all", () => {
|
|
396
|
+
/*
|
|
397
|
+
* "unknown" is the classifier saying it has no answer, not a role, and
|
|
398
|
+
* no project has a row for it — offering one would mean "stop
|
|
399
|
+
* classifying this device forever". A neutral node is the honest
|
|
400
|
+
* drawing, so nothing is stamped even with a full catalogue loaded.
|
|
401
|
+
*/
|
|
402
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
403
|
+
estateDevices(),
|
|
404
|
+
now,
|
|
405
|
+
[],
|
|
406
|
+
[],
|
|
407
|
+
[],
|
|
408
|
+
new Set<string>(),
|
|
409
|
+
seededRoles(),
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
const blank: NetworkTopologyNode = nodeById(result, "d-blank")!;
|
|
413
|
+
expect(blank.role).toBe("unknown");
|
|
414
|
+
expectNoRoleStamp(blank);
|
|
415
|
+
// The devices around it were stamped, so this is a decision, not a miss.
|
|
416
|
+
expect(nodeById(result, "d-router")!.roleKey).toBe("router");
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
describe("a role the operator assigned to a device", () => {
|
|
421
|
+
it("stamps the assigned built-in role, over anything the evidence said", () => {
|
|
422
|
+
/*
|
|
423
|
+
* The assignment is the only statement about the role that is not an
|
|
424
|
+
* inference, so it decides both halves: the built-in `role` AND which
|
|
425
|
+
* configured row the node is drawn from. The classifier would answer
|
|
426
|
+
* "switch" here off the Catalyst model string.
|
|
427
|
+
*/
|
|
428
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
429
|
+
[
|
|
430
|
+
makeDevice("d1", "dmz-1", {
|
|
431
|
+
sysDescr: "Cisco IOS Software, Catalyst 9300-48P",
|
|
432
|
+
deviceRole: "firewall",
|
|
433
|
+
}),
|
|
434
|
+
],
|
|
435
|
+
now,
|
|
436
|
+
[],
|
|
437
|
+
[],
|
|
438
|
+
[],
|
|
439
|
+
new Set<string>(),
|
|
440
|
+
seededRolesWith("firewall", { name: "Perimeter Firewall" }),
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const node: NetworkTopologyNode = nodeById(result, "d1")!;
|
|
444
|
+
expect(node.role).toBe("firewall");
|
|
445
|
+
expect(node.roleKey).toBe("firewall");
|
|
446
|
+
// From the row, not from the built-in DEVICE_ROLE_LABELS map.
|
|
447
|
+
expect(node.roleLabel).toBe("Perimeter Firewall");
|
|
448
|
+
expect(node.roleShape).toBe("diamond");
|
|
449
|
+
expect(node.isCoreLayerRole).toBe(true);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("draws a device as its CUSTOM role while `role` keeps the built-in answer", () => {
|
|
453
|
+
/*
|
|
454
|
+
* A custom role has no place in NetworkTopologyDeviceRole and never can
|
|
455
|
+
* — that union is the vocabulary the SNMP classifier speaks, and it
|
|
456
|
+
* cannot invent a project's own roles. So the two fields deliberately
|
|
457
|
+
* disagree here: `role` stays whatever the evidence supports (this box
|
|
458
|
+
* reports a Linux kernel, so "server"), and every field a human reads —
|
|
459
|
+
* label, silhouette, tier — comes from the PoS Terminal row.
|
|
460
|
+
*
|
|
461
|
+
* Leaving `role` alone is not an oversight. It is what any consumer that
|
|
462
|
+
* still speaks only the built-in union falls back to, and blanking it to
|
|
463
|
+
* "unknown" would downgrade those consumers rather than extend them.
|
|
464
|
+
*/
|
|
465
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
466
|
+
[
|
|
467
|
+
makeDevice("d-pos", "lane-4-till", {
|
|
468
|
+
sysDescr: "Linux 5.10.0 x86_64",
|
|
469
|
+
deviceRole: "posTerminal",
|
|
470
|
+
}),
|
|
471
|
+
],
|
|
472
|
+
now,
|
|
473
|
+
[],
|
|
474
|
+
[],
|
|
475
|
+
[],
|
|
476
|
+
new Set<string>(),
|
|
477
|
+
[
|
|
478
|
+
...seededRoles(),
|
|
479
|
+
{
|
|
480
|
+
id: "role-pos",
|
|
481
|
+
key: "posTerminal",
|
|
482
|
+
name: "PoS Terminal",
|
|
483
|
+
topologyShape: "hexagon",
|
|
484
|
+
isCoreLayer: false,
|
|
485
|
+
isSnmpWalkable: false,
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const node: NetworkTopologyNode = nodeById(result, "d-pos")!;
|
|
491
|
+
expect(node.role).toBe("server");
|
|
492
|
+
expect(node.roleKey).toBe("posTerminal");
|
|
493
|
+
expect(node.roleLabel).toBe("PoS Terminal");
|
|
494
|
+
expect(node.roleShape).toBe("hexagon");
|
|
495
|
+
expect(node.isCoreLayerRole).toBe(false);
|
|
496
|
+
expect(node.isSnmpWalkableRole).toBe(false);
|
|
497
|
+
expect(node.roleId).toBe("role-pos");
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("matches an assigned key case- and whitespace-insensitively", () => {
|
|
501
|
+
/*
|
|
502
|
+
* The stored value is a ShortText column: what lands in it depends on
|
|
503
|
+
* which form, import or API call wrote it. A role that only applied when
|
|
504
|
+
* the key was typed in exactly the row's camelCase spelling would be a
|
|
505
|
+
* trap, and the same leniency parseDeviceRoleOverride already applies to
|
|
506
|
+
* the built-in half has to apply to the configured half.
|
|
507
|
+
*/
|
|
508
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
509
|
+
[
|
|
510
|
+
makeDevice("d1", "a", { deviceRole: " SWITCH " }),
|
|
511
|
+
makeDevice("d2", "b", { deviceRole: "POSTERMINAL" }),
|
|
512
|
+
makeDevice("d3", "c", { deviceRole: " posTerminal " }),
|
|
513
|
+
makeDevice("d4", "d", { deviceRole: "wirelessaccesspoint" }),
|
|
514
|
+
],
|
|
515
|
+
now,
|
|
516
|
+
[],
|
|
517
|
+
[],
|
|
518
|
+
[],
|
|
519
|
+
new Set<string>(),
|
|
520
|
+
[
|
|
521
|
+
...seededRoles(),
|
|
522
|
+
{
|
|
523
|
+
id: "role-pos",
|
|
524
|
+
key: "posTerminal",
|
|
525
|
+
name: "PoS Terminal",
|
|
526
|
+
topologyShape: "hexagon",
|
|
527
|
+
},
|
|
528
|
+
],
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
expect(nodeById(result, "d1")!.roleKey).toBe("switch");
|
|
532
|
+
expect(nodeById(result, "d1")!.roleLabel).toBe("Switch");
|
|
533
|
+
// Both spellings of the custom key reach the one row.
|
|
534
|
+
expect(nodeById(result, "d2")!.roleKey).toBe("posTerminal");
|
|
535
|
+
expect(nodeById(result, "d3")!.roleKey).toBe("posTerminal");
|
|
536
|
+
expect(nodeById(result, "d4")!.roleKey).toBe("wirelessAccessPoint");
|
|
537
|
+
expect(nodeById(result, "d4")!.roleLabel).toBe("Wireless AP");
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it("stamps nothing when the assigned key matches no row in the catalogue", () => {
|
|
541
|
+
/*
|
|
542
|
+
* The role the device was assigned has since been deleted, or the row
|
|
543
|
+
* belongs to a project this device was moved out of. There is nothing to
|
|
544
|
+
* draw it as, so it falls all the way back to the built-in behaviour —
|
|
545
|
+
* and NOT to the classifier's row, because the assignment is still the
|
|
546
|
+
* operator's statement about what this box is and using "firewall"'s
|
|
547
|
+
* label for a device somebody called an SD-WAN edge would be a lie.
|
|
548
|
+
*/
|
|
549
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
550
|
+
[
|
|
551
|
+
makeDevice("d1", "edge-1", {
|
|
552
|
+
sysDescr: "FortiGate-60F v7.2.5",
|
|
553
|
+
deviceRole: "sdWanEdge",
|
|
554
|
+
}),
|
|
555
|
+
],
|
|
556
|
+
now,
|
|
557
|
+
[],
|
|
558
|
+
[],
|
|
559
|
+
[],
|
|
560
|
+
new Set<string>(),
|
|
561
|
+
seededRoles(),
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
const node: NetworkTopologyNode = nodeById(result, "d1")!;
|
|
565
|
+
// The classifier still gets to answer, exactly as it did before.
|
|
566
|
+
expect(node.role).toBe("firewall");
|
|
567
|
+
expectNoRoleStamp(node);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("stamps nothing when the project deleted the row the classifier chose", () => {
|
|
571
|
+
/*
|
|
572
|
+
* The permanent case, not a transient one: a project may delete a seeded
|
|
573
|
+
* role the classifier can still produce. An empty stamp on those nodes
|
|
574
|
+
* is what makes them draw the pre-feature map instead of erroring or
|
|
575
|
+
* borrowing another role's shape.
|
|
576
|
+
*/
|
|
577
|
+
const withoutCameras: Array<TopologyDeviceRoleInput> =
|
|
578
|
+
seededRoles().filter((role: TopologyDeviceRoleInput) => {
|
|
579
|
+
return role.key !== "camera";
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
583
|
+
[
|
|
584
|
+
makeDevice("d1", "lobby-cam", {
|
|
585
|
+
deviceModel: "Hikvision DS-2CD2143G0",
|
|
586
|
+
}),
|
|
587
|
+
makeDevice("d2", "access-1", {
|
|
588
|
+
sysDescr: "Cisco IOS Software, Catalyst 9300-48P",
|
|
589
|
+
}),
|
|
590
|
+
],
|
|
591
|
+
now,
|
|
592
|
+
[],
|
|
593
|
+
[],
|
|
594
|
+
[],
|
|
595
|
+
new Set<string>(),
|
|
596
|
+
withoutCameras,
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
expect(nodeById(result, "d1")!.role).toBe("camera");
|
|
600
|
+
expectNoRoleStamp(nodeById(result, "d1")!);
|
|
601
|
+
// Its neighbour, whose row survives, is unaffected.
|
|
602
|
+
expect(nodeById(result, "d2")!.roleKey).toBe("switch");
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
describe("nodes nobody manages", () => {
|
|
607
|
+
it("stamps an unmanaged CDP peer from the classifier's answer", () => {
|
|
608
|
+
/*
|
|
609
|
+
* A peer is exactly the node an operator adopts, and the adopt flow
|
|
610
|
+
* reads isSnmpWalkableRole to decide whether the modal opens on SNMP
|
|
611
|
+
* polling or on a plain monitor. Without the stamp that decision falls
|
|
612
|
+
* back to a hardcoded set, which is what made a project's own roles
|
|
613
|
+
* unable to influence it.
|
|
614
|
+
*/
|
|
615
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
616
|
+
[
|
|
617
|
+
makeDevice("d1", "access-1", {
|
|
618
|
+
sysName: "access-1",
|
|
619
|
+
cdpNeighbors: [
|
|
620
|
+
{
|
|
621
|
+
localInterfaceIndex: 1,
|
|
622
|
+
remoteDeviceId: "ap-lobby",
|
|
623
|
+
remotePlatform: "cisco AIR-CAP3702I-A-K9",
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
localInterfaceIndex: 2,
|
|
627
|
+
remoteDeviceId: "handset-3f",
|
|
628
|
+
remotePlatform: "Cisco IP Phone 7961",
|
|
629
|
+
},
|
|
630
|
+
],
|
|
631
|
+
}),
|
|
632
|
+
],
|
|
633
|
+
now,
|
|
634
|
+
[],
|
|
635
|
+
[],
|
|
636
|
+
[],
|
|
637
|
+
new Set<string>(),
|
|
638
|
+
seededRolesWith("wirelessAccessPoint", { name: "Access Point" }),
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
const ap: NetworkTopologyNode = nodeById(result, "unmanaged:ap-lobby")!;
|
|
642
|
+
expect(ap.role).toBe("wirelessAccessPoint");
|
|
643
|
+
expect(ap.roleKey).toBe("wirelessAccessPoint");
|
|
644
|
+
expect(ap.roleLabel).toBe("Access Point");
|
|
645
|
+
expect(ap.roleShape).toBe("triangle");
|
|
646
|
+
// Walkable: adopting this one should open on SNMP.
|
|
647
|
+
expect(ap.isSnmpWalkableRole).toBe(true);
|
|
648
|
+
|
|
649
|
+
const phone: NetworkTopologyNode = nodeById(
|
|
650
|
+
result,
|
|
651
|
+
"unmanaged:handset-3f",
|
|
652
|
+
)!;
|
|
653
|
+
expect(phone.roleKey).toBe("phone");
|
|
654
|
+
// Not walkable: adopting a handset should open on a monitor instead.
|
|
655
|
+
expect(phone.isSnmpWalkableRole).toBe(false);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it("leaves an unmanaged peer nothing said anything about unstamped", () => {
|
|
659
|
+
/*
|
|
660
|
+
* Most peers come back "unknown" and should: a name and nothing else is
|
|
661
|
+
* not evidence. Those nodes keep drawing neutral, catalogue or no
|
|
662
|
+
* catalogue.
|
|
663
|
+
*/
|
|
664
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
665
|
+
[
|
|
666
|
+
makeDevice("d1", "access-1", {
|
|
667
|
+
sysName: "access-1",
|
|
668
|
+
lldpNeighbors: [
|
|
669
|
+
{ localInterfaceIndex: 3, remoteSysName: "prod-node-7" },
|
|
670
|
+
],
|
|
671
|
+
}),
|
|
672
|
+
],
|
|
673
|
+
now,
|
|
674
|
+
[],
|
|
675
|
+
[],
|
|
676
|
+
[],
|
|
677
|
+
new Set<string>(),
|
|
678
|
+
seededRoles(),
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
const peer: NetworkTopologyNode = nodeById(
|
|
682
|
+
result,
|
|
683
|
+
"unmanaged:prod-node-7",
|
|
684
|
+
)!;
|
|
685
|
+
expect(peer.role).toBe("unknown");
|
|
686
|
+
expectNoRoleStamp(peer);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it("stamps discovered endpoints, so a renamed handset role reaches them", () => {
|
|
690
|
+
/*
|
|
691
|
+
* Endpoints are learned from ARP/FDB and never assigned a role by hand,
|
|
692
|
+
* so the classifier's answer is all there is — and a project that
|
|
693
|
+
* renamed "IP phone" expects to see the new word on its handsets, which
|
|
694
|
+
* are the most numerous thing on the map.
|
|
695
|
+
*/
|
|
696
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
697
|
+
estateDevices(),
|
|
698
|
+
now,
|
|
699
|
+
[],
|
|
700
|
+
estateEndpoints(),
|
|
701
|
+
[],
|
|
702
|
+
new Set<string>(),
|
|
703
|
+
seededRolesWith("phone", { name: "Desk Handset" }),
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
const handset: NetworkTopologyNode = nodeById(
|
|
707
|
+
result,
|
|
708
|
+
"endpoint:ep-phone",
|
|
709
|
+
)!;
|
|
710
|
+
expect(handset.role).toBe("phone");
|
|
711
|
+
expect(handset.roleKey).toBe("phone");
|
|
712
|
+
expect(handset.roleLabel).toBe("Desk Handset");
|
|
713
|
+
expect(handset.roleShape).toBe("rect");
|
|
714
|
+
|
|
715
|
+
// An endpoint nothing was said about is still a host, and still stamped.
|
|
716
|
+
const plain: NetworkTopologyNode = nodeById(result, "endpoint:ep-plain")!;
|
|
717
|
+
expect(plain.role).toBe("host");
|
|
718
|
+
expect(plain.roleKey).toBe("host");
|
|
719
|
+
expect(plain.roleLabel).toBe("Host");
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it("keeps a stamp off the endpoints of a project with no host row", () => {
|
|
723
|
+
/*
|
|
724
|
+
* Same fallback as everywhere else, checked on the node kind that never
|
|
725
|
+
* has an assignment to fall back FROM. Endpoints are capped and numerous,
|
|
726
|
+
* so a missing row must be cheap and silent, not an error per node.
|
|
727
|
+
*/
|
|
728
|
+
const withoutHosts: Array<TopologyDeviceRoleInput> = seededRoles().filter(
|
|
729
|
+
(role: TopologyDeviceRoleInput) => {
|
|
730
|
+
return role.key !== "host";
|
|
731
|
+
},
|
|
732
|
+
);
|
|
733
|
+
|
|
734
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
735
|
+
estateDevices(),
|
|
736
|
+
now,
|
|
737
|
+
[],
|
|
738
|
+
estateEndpoints(),
|
|
739
|
+
[],
|
|
740
|
+
new Set<string>(),
|
|
741
|
+
withoutHosts,
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
expectNoRoleStamp(nodeById(result, "endpoint:ep-plain")!);
|
|
745
|
+
// The handset beside it, whose row survives, is still stamped.
|
|
746
|
+
expect(nodeById(result, "endpoint:ep-phone")!.roleKey).toBe("phone");
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
describe("the stamp is deterministic", () => {
|
|
751
|
+
it("produces identical role fields on two builds of the same input", () => {
|
|
752
|
+
/*
|
|
753
|
+
* The map is rebuilt on every poll and on every page load. A role field
|
|
754
|
+
* that varied between builds would make nodes change shape, tier or
|
|
755
|
+
* legend group under an operator watching the same screen.
|
|
756
|
+
*/
|
|
757
|
+
const roles: Array<TopologyDeviceRoleInput> = seededRolesWith("router", {
|
|
758
|
+
name: "Edge Router",
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
const first: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
762
|
+
estateDevices(),
|
|
763
|
+
now,
|
|
764
|
+
[],
|
|
765
|
+
estateEndpoints(),
|
|
766
|
+
[],
|
|
767
|
+
new Set<string>(),
|
|
768
|
+
roles,
|
|
769
|
+
);
|
|
770
|
+
const second: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
771
|
+
estateDevices(),
|
|
772
|
+
now,
|
|
773
|
+
[],
|
|
774
|
+
estateEndpoints(),
|
|
775
|
+
[],
|
|
776
|
+
new Set<string>(),
|
|
777
|
+
roles,
|
|
778
|
+
);
|
|
779
|
+
|
|
780
|
+
expect(second.nodes).toStrictEqual(first.nodes);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
it("ignores the order the catalogue rows arrive in", () => {
|
|
784
|
+
/*
|
|
785
|
+
* Rows come off a query ordered by the settings page's `order` column,
|
|
786
|
+
* and an operator may reorder that list at any time. Reordering the
|
|
787
|
+
* legend must not change a single thing about how a node is drawn.
|
|
788
|
+
*/
|
|
789
|
+
const roles: Array<TopologyDeviceRoleInput> = seededRoles();
|
|
790
|
+
|
|
791
|
+
const forward: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
792
|
+
estateDevices(),
|
|
793
|
+
now,
|
|
794
|
+
[],
|
|
795
|
+
estateEndpoints(),
|
|
796
|
+
[],
|
|
797
|
+
new Set<string>(),
|
|
798
|
+
roles,
|
|
799
|
+
);
|
|
800
|
+
const reversed: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
801
|
+
estateDevices(),
|
|
802
|
+
now,
|
|
803
|
+
[],
|
|
804
|
+
estateEndpoints(),
|
|
805
|
+
[],
|
|
806
|
+
new Set<string>(),
|
|
807
|
+
[...roles].reverse(),
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
expect(reversed.nodes).toStrictEqual(forward.nodes);
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it("resolves each device's stamp independently of the device query order", () => {
|
|
814
|
+
const roles: Array<TopologyDeviceRoleInput> = seededRoles();
|
|
815
|
+
const devices: Array<TopologyDeviceInput> = estateDevices();
|
|
816
|
+
|
|
817
|
+
const forward: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
818
|
+
devices,
|
|
819
|
+
now,
|
|
820
|
+
[],
|
|
821
|
+
[],
|
|
822
|
+
[],
|
|
823
|
+
new Set<string>(),
|
|
824
|
+
roles,
|
|
825
|
+
);
|
|
826
|
+
const reversed: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
827
|
+
[...devices].reverse(),
|
|
828
|
+
now,
|
|
829
|
+
[],
|
|
830
|
+
[],
|
|
831
|
+
[],
|
|
832
|
+
new Set<string>(),
|
|
833
|
+
roles,
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
for (const deviceId of ["d-router", "d-switch", "d-blank"]) {
|
|
837
|
+
expect(nodeById(reversed, deviceId)).toStrictEqual(
|
|
838
|
+
nodeById(forward, deviceId),
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it("does not modify the catalogue it was handed", () => {
|
|
844
|
+
/*
|
|
845
|
+
* The API loads the project's roles once and may reuse the array. A
|
|
846
|
+
* builder that wrote back into it would corrupt the next build in the
|
|
847
|
+
* same request.
|
|
848
|
+
*/
|
|
849
|
+
const roles: Array<TopologyDeviceRoleInput> = seededRoles();
|
|
850
|
+
const before: string = JSON.stringify(roles);
|
|
851
|
+
|
|
852
|
+
NetworkTopologyUtil.buildTopology(
|
|
853
|
+
estateDevices(),
|
|
854
|
+
now,
|
|
855
|
+
[],
|
|
856
|
+
estateEndpoints(),
|
|
857
|
+
[],
|
|
858
|
+
new Set<string>(),
|
|
859
|
+
roles,
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
expect(JSON.stringify(roles)).toBe(before);
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
});
|