@oneuptime/common 12.0.10 → 12.0.12
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/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/NetworkDeviceLinkService.ts +338 -2
- package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
- package/Server/Types/Database/JSONColumnQuery.ts +42 -4
- package/Server/Types/Workflow/Components/Conditions/IfElse.ts +9 -0
- package/Server/Types/Workflow/Components/JavaScript.ts +9 -0
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +12 -0
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
- package/Server/Utils/VM/VMRunner.ts +693 -874
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
- 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/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/NetworkDeviceLinkParentValidation.test.ts +1043 -0
- package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
- package/Tests/Server/Types/Workflow/Components/CustomCodeScriptError.test.ts +121 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorJavascriptExpression.test.ts +92 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
- package/Tests/Server/Utils/VM/VMRunnerIsolation.test.ts +385 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +3 -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/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
- package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
- package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
- package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
- package/Types/Billing/PayAsYouGoPricing.ts +47 -0
- package/Types/CustomField/CustomFieldType.ts +28 -0
- package/Types/Monitor/CustomCodeMonitor/CustomCodeMonitorResponse.ts +14 -2
- package/Types/Monitor/MonitorType.ts +11 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
- 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/ModelTable/CustomFieldColumns.tsx +55 -0
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
- 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/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/NetworkDeviceLinkService.js +253 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.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/Types/Workflow/Components/Conditions/IfElse.js +8 -0
- package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/JavaScript.js +8 -0
- package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +12 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.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/Server/Utils/VM/VMRunner.js +402 -525
- package/build/dist/Server/Utils/VM/VMRunner.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/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/ModelTable/CustomFieldColumns.js +42 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.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/package.json +1 -1
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
import NetworkTopologyUtil, {
|
|
2
|
+
TopologyBuildResult,
|
|
3
|
+
TopologyDeviceInput,
|
|
4
|
+
TopologyManualLinkInput,
|
|
5
|
+
} from "../../../Utils/Monitor/NetworkTopologyUtil";
|
|
6
|
+
import {
|
|
7
|
+
NetworkTopologyEdge,
|
|
8
|
+
NetworkTopologyNode,
|
|
9
|
+
} from "../../../Types/Monitor/SnmpMonitor/NetworkTopology";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* Issue #3192. Two things the map could not previously express, and both
|
|
13
|
+
* of them are statements a PERSON made rather than something SNMP said:
|
|
14
|
+
*
|
|
15
|
+
* - what a device IS, on a box nothing walks. The classifier is
|
|
16
|
+
* evidence-driven and a ping-only device offers it a hostname and
|
|
17
|
+
* nothing else, so `NetworkDevice.deviceRole` is the only answer
|
|
18
|
+
* available at all — and it outranks the classifier when both speak.
|
|
19
|
+
* - which end of a link is the PARENT. LLDP and CDP report a cable, not
|
|
20
|
+
* a hierarchy, so `NetworkDeviceLink.parentDeviceId` is carried onto
|
|
21
|
+
* the edge as `parentNodeId` for the layouts to honour.
|
|
22
|
+
*
|
|
23
|
+
* Both travel through buildTopology, which is what this file exercises:
|
|
24
|
+
* the units are tested next door, this is the wiring.
|
|
25
|
+
*/
|
|
26
|
+
describe("NetworkTopologyUtil.buildTopology — operator-declared role and hierarchy", () => {
|
|
27
|
+
const now: Date = new Date("2026-07-22T12:00:00Z");
|
|
28
|
+
const fresh: Date = new Date("2026-07-22T11:55:00Z");
|
|
29
|
+
|
|
30
|
+
const makeDevice: (
|
|
31
|
+
id: string,
|
|
32
|
+
name: string,
|
|
33
|
+
overrides?: Partial<TopologyDeviceInput>,
|
|
34
|
+
) => TopologyDeviceInput = (
|
|
35
|
+
id: string,
|
|
36
|
+
name: string,
|
|
37
|
+
overrides?: Partial<TopologyDeviceInput>,
|
|
38
|
+
): TopologyDeviceInput => {
|
|
39
|
+
return {
|
|
40
|
+
id,
|
|
41
|
+
name,
|
|
42
|
+
lastSeenAt: fresh,
|
|
43
|
+
...overrides,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const nodeById: (
|
|
48
|
+
result: TopologyBuildResult,
|
|
49
|
+
id: string,
|
|
50
|
+
) => NetworkTopologyNode | undefined = (
|
|
51
|
+
result: TopologyBuildResult,
|
|
52
|
+
id: string,
|
|
53
|
+
): NetworkTopologyNode | undefined => {
|
|
54
|
+
return result.nodes.find((node: NetworkTopologyNode) => {
|
|
55
|
+
return node.id === id;
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* Edges are undirected as far as identity goes — which end landed in
|
|
61
|
+
* fromNodeId depends on who reported the link first — so they are looked
|
|
62
|
+
* up by the unordered pair, exactly the way the builder keys them.
|
|
63
|
+
*/
|
|
64
|
+
const edgeBetween: (
|
|
65
|
+
result: TopologyBuildResult,
|
|
66
|
+
a: string,
|
|
67
|
+
b: string,
|
|
68
|
+
) => NetworkTopologyEdge | undefined = (
|
|
69
|
+
result: TopologyBuildResult,
|
|
70
|
+
a: string,
|
|
71
|
+
b: string,
|
|
72
|
+
): NetworkTopologyEdge | undefined => {
|
|
73
|
+
const wanted: string = [a, b].sort().join("::");
|
|
74
|
+
return result.edges.find((edge: NetworkTopologyEdge) => {
|
|
75
|
+
return [edge.fromNodeId, edge.toNodeId].sort().join("::") === wanted;
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
describe("the operator's device role override", () => {
|
|
80
|
+
it("gives a role to a ping-only device the classifier could never place", () => {
|
|
81
|
+
/*
|
|
82
|
+
* The case the column exists for. Nothing walks this box, so there is
|
|
83
|
+
* no sysDescr, no sysObjectId, no model — and "ping-only-42" is not a
|
|
84
|
+
* naming convention the classifier recognises. Without the override
|
|
85
|
+
* this node is drawn "unknown" forever.
|
|
86
|
+
*/
|
|
87
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
88
|
+
[
|
|
89
|
+
makeDevice("d1", "ping-only-42", { deviceRole: "switch" }),
|
|
90
|
+
makeDevice("d2", "ping-only-43"),
|
|
91
|
+
],
|
|
92
|
+
now,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(nodeById(result, "d1")!.role).toBe("switch");
|
|
96
|
+
// The control: same absence of evidence, no override, still unknown.
|
|
97
|
+
expect(nodeById(result, "d2")!.role).toBe("unknown");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("outranks an SNMP identity that says something else entirely", () => {
|
|
101
|
+
/*
|
|
102
|
+
* The override is the only statement about the role that is not an
|
|
103
|
+
* inference, so it wins even against tier-1 evidence. Real cause: a
|
|
104
|
+
* chassis re-purposed as something its sysDescr never caught up with.
|
|
105
|
+
*/
|
|
106
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
107
|
+
[
|
|
108
|
+
makeDevice("d1", "core-1", {
|
|
109
|
+
sysDescr: "Cisco IOS Software, Catalyst 9300-48P",
|
|
110
|
+
deviceRole: "firewall",
|
|
111
|
+
}),
|
|
112
|
+
makeDevice("d2", "dmz-1", {
|
|
113
|
+
sysObjectId: "1.3.6.1.4.1.3375.2.1.3.4.10",
|
|
114
|
+
deviceRole: "router",
|
|
115
|
+
}),
|
|
116
|
+
],
|
|
117
|
+
now,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Classifier alone would answer "switch" and "loadBalancer" here.
|
|
121
|
+
expect(nodeById(result, "d1")!.role).toBe("firewall");
|
|
122
|
+
expect(nodeById(result, "d2")!.role).toBe("router");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("leaves the classifier's answer alone when no override is stored", () => {
|
|
126
|
+
/*
|
|
127
|
+
* The override must be additive: every device that had a correctly
|
|
128
|
+
* derived role before the column existed has to keep it.
|
|
129
|
+
*/
|
|
130
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
131
|
+
[
|
|
132
|
+
makeDevice("d1", "edge-1", { sysDescr: "FortiGate-60F v7.2.5" }),
|
|
133
|
+
makeDevice("d2", "edge-2", {
|
|
134
|
+
sysDescr: "FortiGate-60F v7.2.5",
|
|
135
|
+
deviceRole: undefined,
|
|
136
|
+
}),
|
|
137
|
+
],
|
|
138
|
+
now,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(nodeById(result, "d1")!.role).toBe("firewall");
|
|
142
|
+
expect(nodeById(result, "d2")!.role).toBe("firewall");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("falls back to the classifier rather than blanking a role it cannot parse", () => {
|
|
146
|
+
/*
|
|
147
|
+
* An override nobody can read is not an instruction to draw a neutral
|
|
148
|
+
* node — it is no instruction at all, so the evidence gets its say.
|
|
149
|
+
* "cor switch" is the shape a typo takes; the empty and whitespace
|
|
150
|
+
* cases are what an operator clearing the field actually stores.
|
|
151
|
+
*/
|
|
152
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
153
|
+
[
|
|
154
|
+
makeDevice("d1", "edge-1", {
|
|
155
|
+
sysDescr: "FortiGate-60F v7.2.5",
|
|
156
|
+
deviceRole: "cor switch",
|
|
157
|
+
}),
|
|
158
|
+
makeDevice("d2", "edge-2", {
|
|
159
|
+
sysDescr: "FortiGate-60F v7.2.5",
|
|
160
|
+
deviceRole: "",
|
|
161
|
+
}),
|
|
162
|
+
makeDevice("d3", "edge-3", {
|
|
163
|
+
sysDescr: "FortiGate-60F v7.2.5",
|
|
164
|
+
deviceRole: " ",
|
|
165
|
+
}),
|
|
166
|
+
],
|
|
167
|
+
now,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
expect(nodeById(result, "d1")!.role).toBe("firewall");
|
|
171
|
+
expect(nodeById(result, "d2")!.role).toBe("firewall");
|
|
172
|
+
expect(nodeById(result, "d3")!.role).toBe("firewall");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("refuses 'unknown' as an override so the classifier still gets to run", () => {
|
|
176
|
+
/*
|
|
177
|
+
* Storing "unknown" would silently disable the classifier on a device
|
|
178
|
+
* the operator was only declining to classify. d1 proves the evidence
|
|
179
|
+
* still wins; d2 proves the refusal is not itself an error — a device
|
|
180
|
+
* with nothing to go on lands on "unknown" via the classifier.
|
|
181
|
+
*/
|
|
182
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
183
|
+
[
|
|
184
|
+
makeDevice("d1", "edge-1", {
|
|
185
|
+
sysDescr: "FortiGate-60F v7.2.5",
|
|
186
|
+
deviceRole: "unknown",
|
|
187
|
+
}),
|
|
188
|
+
makeDevice("d2", "device-42", { deviceRole: "unknown" }),
|
|
189
|
+
],
|
|
190
|
+
now,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
expect(nodeById(result, "d1")!.role).toBe("firewall");
|
|
194
|
+
expect(nodeById(result, "d2")!.role).toBe("unknown");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("reads the stored value case-insensitively and ignores surrounding space", () => {
|
|
198
|
+
/*
|
|
199
|
+
* The column is ShortText, so what lands in it depends on which form
|
|
200
|
+
* or import wrote it. A role that only applies when it was typed in
|
|
201
|
+
* exactly the internal camelCase spelling would be a trap.
|
|
202
|
+
*/
|
|
203
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
204
|
+
[
|
|
205
|
+
makeDevice("d1", "a", { deviceRole: "SWITCH" }),
|
|
206
|
+
makeDevice("d2", "b", { deviceRole: " Router " }),
|
|
207
|
+
makeDevice("d3", "c", { deviceRole: "wirelessaccesspoint" }),
|
|
208
|
+
makeDevice("d4", "d", { deviceRole: "loadBalancer" }),
|
|
209
|
+
],
|
|
210
|
+
now,
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
expect(nodeById(result, "d1")!.role).toBe("switch");
|
|
214
|
+
expect(nodeById(result, "d2")!.role).toBe("router");
|
|
215
|
+
expect(nodeById(result, "d3")!.role).toBe("wirelessAccessPoint");
|
|
216
|
+
expect(nodeById(result, "d4")!.role).toBe("loadBalancer");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("applies the override to the device that owns it and to nothing else", () => {
|
|
220
|
+
/*
|
|
221
|
+
* A role is a per-node property. The unmanaged peer d1 reports is a
|
|
222
|
+
* different box that nobody has overridden, and it must keep being
|
|
223
|
+
* classified from what it advertises about itself.
|
|
224
|
+
*/
|
|
225
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
226
|
+
[
|
|
227
|
+
makeDevice("d1", "edge-1", {
|
|
228
|
+
sysName: "edge-1",
|
|
229
|
+
deviceRole: "firewall",
|
|
230
|
+
cdpNeighbors: [
|
|
231
|
+
{
|
|
232
|
+
localInterfaceIndex: 1,
|
|
233
|
+
remoteDeviceId: "ap-lobby",
|
|
234
|
+
remotePlatform: "cisco AIR-CAP3702I-A-K9",
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
}),
|
|
238
|
+
makeDevice("d2", "device-99"),
|
|
239
|
+
],
|
|
240
|
+
now,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
expect(nodeById(result, "d1")!.role).toBe("firewall");
|
|
244
|
+
expect(nodeById(result, "unmanaged:ap-lobby")!.role).toBe(
|
|
245
|
+
"wirelessAccessPoint",
|
|
246
|
+
);
|
|
247
|
+
expect(nodeById(result, "d2")!.role).toBe("unknown");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("resolves every device's role independently of the order they arrive in", () => {
|
|
251
|
+
/*
|
|
252
|
+
* Devices come off a query whose order is not guaranteed. A role that
|
|
253
|
+
* depended on it would make the map flicker between rebuilds.
|
|
254
|
+
*/
|
|
255
|
+
const devices: Array<TopologyDeviceInput> = [
|
|
256
|
+
makeDevice("d1", "core-1", {
|
|
257
|
+
sysDescr: "Cisco IOS Software, Catalyst 9300-48P",
|
|
258
|
+
deviceRole: "firewall",
|
|
259
|
+
}),
|
|
260
|
+
makeDevice("d2", "edge-1", { sysDescr: "FortiGate-60F v7.2.5" }),
|
|
261
|
+
makeDevice("d3", "ping-only", { deviceRole: "router" }),
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
const forward: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
265
|
+
devices,
|
|
266
|
+
now,
|
|
267
|
+
);
|
|
268
|
+
const reversed: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
269
|
+
[...devices].reverse(),
|
|
270
|
+
now,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
for (const deviceId of ["d1", "d2", "d3"]) {
|
|
274
|
+
expect(nodeById(reversed, deviceId)!.role).toBe(
|
|
275
|
+
nodeById(forward, deviceId)!.role,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
expect(nodeById(forward, "d1")!.role).toBe("firewall");
|
|
279
|
+
expect(nodeById(forward, "d2")!.role).toBe("firewall");
|
|
280
|
+
expect(nodeById(forward, "d3")!.role).toBe("router");
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("the parent an operator declared on a link", () => {
|
|
285
|
+
it("carries a parent declared as the link's 'from' end onto the edge", () => {
|
|
286
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
287
|
+
[makeDevice("d1", "core-1"), makeDevice("d2", "access-1")],
|
|
288
|
+
now,
|
|
289
|
+
[],
|
|
290
|
+
[],
|
|
291
|
+
[{ fromDeviceId: "d1", toDeviceId: "d2", parentDeviceId: "d1" }],
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
expect(result.edges).toHaveLength(1);
|
|
295
|
+
expect(result.edges[0]!.parentNodeId).toBe("d1");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("carries a parent declared as the link's 'to' end just the same", () => {
|
|
299
|
+
/*
|
|
300
|
+
* Which column the operator's parent landed in is an artefact of how
|
|
301
|
+
* they drew the link. The declaration names an END, not a side.
|
|
302
|
+
*/
|
|
303
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
304
|
+
[makeDevice("d1", "access-1"), makeDevice("d2", "core-1")],
|
|
305
|
+
now,
|
|
306
|
+
[],
|
|
307
|
+
[],
|
|
308
|
+
[{ fromDeviceId: "d1", toDeviceId: "d2", parentDeviceId: "d2" }],
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
expect(result.edges).toHaveLength(1);
|
|
312
|
+
expect(result.edges[0]!.parentNodeId).toBe("d2");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("still draws the link when the declared parent is a third device", () => {
|
|
316
|
+
/*
|
|
317
|
+
* A parent that is not on the link cannot be rendered as one, so the
|
|
318
|
+
* declaration is dropped — but the CABLE is a separate fact and it is
|
|
319
|
+
* still real. Losing the line as well would delete information the
|
|
320
|
+
* operator did get right.
|
|
321
|
+
*/
|
|
322
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
323
|
+
[
|
|
324
|
+
makeDevice("d1", "core-1"),
|
|
325
|
+
makeDevice("d2", "access-1"),
|
|
326
|
+
makeDevice("d3", "bystander"),
|
|
327
|
+
],
|
|
328
|
+
now,
|
|
329
|
+
[],
|
|
330
|
+
[],
|
|
331
|
+
[
|
|
332
|
+
{
|
|
333
|
+
fromDeviceId: "d1",
|
|
334
|
+
toDeviceId: "d2",
|
|
335
|
+
name: "IDF-2 uplink",
|
|
336
|
+
parentDeviceId: "d3",
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const edge: NetworkTopologyEdge | undefined = edgeBetween(
|
|
342
|
+
result,
|
|
343
|
+
"d1",
|
|
344
|
+
"d2",
|
|
345
|
+
);
|
|
346
|
+
expect(edge).toBeDefined();
|
|
347
|
+
expect(edge!.name).toBe("IDF-2 uplink");
|
|
348
|
+
expect(edge!.parentNodeId).toBeUndefined();
|
|
349
|
+
// The bystander gains nothing from having been named.
|
|
350
|
+
expect(edgeBetween(result, "d1", "d3")).toBeUndefined();
|
|
351
|
+
expect(edgeBetween(result, "d2", "d3")).toBeUndefined();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("skips a link with an end outside the graph, declaration or not", () => {
|
|
355
|
+
/*
|
|
356
|
+
* A site-scoped map, or a link to a device since archived. A declared
|
|
357
|
+
* parent does not make an unattachable link attachable — the pre-existing
|
|
358
|
+
* skip has to happen before the declaration is even looked at.
|
|
359
|
+
*/
|
|
360
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
361
|
+
[makeDevice("d1", "core-1")],
|
|
362
|
+
now,
|
|
363
|
+
[],
|
|
364
|
+
[],
|
|
365
|
+
[
|
|
366
|
+
{
|
|
367
|
+
fromDeviceId: "d1",
|
|
368
|
+
toDeviceId: "d-elsewhere",
|
|
369
|
+
parentDeviceId: "d1",
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
fromDeviceId: "d-elsewhere",
|
|
373
|
+
toDeviceId: "d1",
|
|
374
|
+
parentDeviceId: "d-elsewhere",
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
expect(result.edges).toHaveLength(0);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("leaves a purely discovered edge with no parent at all", () => {
|
|
383
|
+
/*
|
|
384
|
+
* LLDP reports a cable, not a hierarchy: the direction a neighbour
|
|
385
|
+
* entry happens to be read from says nothing about which box is
|
|
386
|
+
* upstream. Absent must mean "not stated" so the layout keeps
|
|
387
|
+
* inferring instead of trusting an accident of polling order.
|
|
388
|
+
*/
|
|
389
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
390
|
+
[
|
|
391
|
+
makeDevice("d1", "edge-1", {
|
|
392
|
+
sysName: "edge-1",
|
|
393
|
+
lldpNeighbors: [
|
|
394
|
+
{ localInterfaceIndex: 24, remoteSysName: "core-1" },
|
|
395
|
+
],
|
|
396
|
+
}),
|
|
397
|
+
makeDevice("d2", "core-1", { sysName: "core-1" }),
|
|
398
|
+
],
|
|
399
|
+
now,
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
expect(result.edges).toHaveLength(1);
|
|
403
|
+
expect(result.edges[0]!.protocols).toEqual(["lldp"]);
|
|
404
|
+
expect(result.edges[0]!.parentNodeId).toBeUndefined();
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("declares the same parent whichever order the devices were queried in", () => {
|
|
408
|
+
const devices: Array<TopologyDeviceInput> = [
|
|
409
|
+
makeDevice("d1", "core-1"),
|
|
410
|
+
makeDevice("d2", "access-1"),
|
|
411
|
+
];
|
|
412
|
+
const links: Array<TopologyManualLinkInput> = [
|
|
413
|
+
{ fromDeviceId: "d2", toDeviceId: "d1", parentDeviceId: "d1" },
|
|
414
|
+
];
|
|
415
|
+
|
|
416
|
+
const forward: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
417
|
+
devices,
|
|
418
|
+
now,
|
|
419
|
+
[],
|
|
420
|
+
[],
|
|
421
|
+
links,
|
|
422
|
+
);
|
|
423
|
+
const reversed: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
424
|
+
[...devices].reverse(),
|
|
425
|
+
now,
|
|
426
|
+
[],
|
|
427
|
+
[],
|
|
428
|
+
links,
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
expect(forward.edges[0]!.parentNodeId).toBe("d1");
|
|
432
|
+
expect(reversed.edges[0]!.parentNodeId).toBe("d1");
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe("merging a declared parent into an edge that already exists", () => {
|
|
437
|
+
it("keeps one edge carrying both protocols and the declaration", () => {
|
|
438
|
+
/*
|
|
439
|
+
* The whole point of merging rather than doubling: an operator who
|
|
440
|
+
* declares the hierarchy on a cable LLDP already found must not end
|
|
441
|
+
* up with two lines between one pair of boxes.
|
|
442
|
+
*/
|
|
443
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
444
|
+
[
|
|
445
|
+
makeDevice("d1", "edge-1", {
|
|
446
|
+
sysName: "edge-1",
|
|
447
|
+
lldpNeighbors: [
|
|
448
|
+
{
|
|
449
|
+
localInterfaceIndex: 24,
|
|
450
|
+
remoteSysName: "core-1",
|
|
451
|
+
remotePortId: "Gi0/1",
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
}),
|
|
455
|
+
makeDevice("d2", "core-1", { sysName: "core-1" }),
|
|
456
|
+
],
|
|
457
|
+
now,
|
|
458
|
+
[],
|
|
459
|
+
[],
|
|
460
|
+
[{ fromDeviceId: "d1", toDeviceId: "d2", parentDeviceId: "d2" }],
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
expect(result.edges).toHaveLength(1);
|
|
464
|
+
expect(result.edges[0]!.protocols).toEqual(["lldp", "manual"]);
|
|
465
|
+
// The measured port survives; the declaration has no discovered rival.
|
|
466
|
+
expect(result.edges[0]!.toPort).toBe("Gi0/1");
|
|
467
|
+
expect(result.edges[0]!.parentNodeId).toBe("d2");
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
it("names the parent by END, not by which side the discovered edge stored", () => {
|
|
471
|
+
/*
|
|
472
|
+
* d2 is the one that reported the neighbour, so the edge is stored
|
|
473
|
+
* d2 -> d1 while the operator drew their link d1 -> d2. If the merge
|
|
474
|
+
* copied a SIDE rather than an id, the hierarchy would come out
|
|
475
|
+
* upside down for exactly half the estate.
|
|
476
|
+
*/
|
|
477
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
478
|
+
[
|
|
479
|
+
makeDevice("d1", "core-1", { sysName: "core-1" }),
|
|
480
|
+
makeDevice("d2", "access-1", {
|
|
481
|
+
sysName: "access-1",
|
|
482
|
+
lldpNeighbors: [
|
|
483
|
+
{ localInterfaceIndex: 1, remoteSysName: "core-1" },
|
|
484
|
+
],
|
|
485
|
+
}),
|
|
486
|
+
],
|
|
487
|
+
now,
|
|
488
|
+
[],
|
|
489
|
+
[],
|
|
490
|
+
[{ fromDeviceId: "d1", toDeviceId: "d2", parentDeviceId: "d1" }],
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
expect(result.edges).toHaveLength(1);
|
|
494
|
+
// The discovered direction is untouched...
|
|
495
|
+
expect(result.edges[0]!.fromNodeId).toBe("d2");
|
|
496
|
+
expect(result.edges[0]!.toNodeId).toBe("d1");
|
|
497
|
+
// ...and the parent is still the core switch the operator named.
|
|
498
|
+
expect(result.edges[0]!.parentNodeId).toBe("d1");
|
|
499
|
+
expect(result.edges[0]!.protocols).toEqual(["lldp", "manual"]);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("keeps the FIRST declaration when two links over one pair disagree", () => {
|
|
503
|
+
/*
|
|
504
|
+
* Mirrors the ordering the caller already relies on: explicit links
|
|
505
|
+
* are merged before rule-derived ones, so a hierarchy set on the link
|
|
506
|
+
* itself beats a rule that happens to cover the same pair. The
|
|
507
|
+
* specific statement beats the general one — the same precedence the
|
|
508
|
+
* ports and the name use.
|
|
509
|
+
*/
|
|
510
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
511
|
+
[makeDevice("d1", "core-1"), makeDevice("d2", "access-1")],
|
|
512
|
+
now,
|
|
513
|
+
[],
|
|
514
|
+
[],
|
|
515
|
+
[
|
|
516
|
+
{ fromDeviceId: "d1", toDeviceId: "d2", parentDeviceId: "d1" },
|
|
517
|
+
{ fromDeviceId: "d1", toDeviceId: "d2", parentDeviceId: "d2" },
|
|
518
|
+
],
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
expect(result.edges).toHaveLength(1);
|
|
522
|
+
expect(result.edges[0]!.parentNodeId).toBe("d1");
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("takes the second link's declaration when the first stated none", () => {
|
|
526
|
+
/*
|
|
527
|
+
* "First wins" is about first STATEMENT, not first row: a silent link
|
|
528
|
+
* has nothing to defend, so a later one that does name a parent fills
|
|
529
|
+
* the gap rather than being ignored.
|
|
530
|
+
*/
|
|
531
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
532
|
+
[makeDevice("d1", "core-1"), makeDevice("d2", "access-1")],
|
|
533
|
+
now,
|
|
534
|
+
[],
|
|
535
|
+
[],
|
|
536
|
+
[
|
|
537
|
+
{ fromDeviceId: "d1", toDeviceId: "d2", name: "cable A" },
|
|
538
|
+
{ fromDeviceId: "d2", toDeviceId: "d1", parentDeviceId: "d1" },
|
|
539
|
+
],
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
expect(result.edges).toHaveLength(1);
|
|
543
|
+
expect(result.edges[0]!.name).toBe("cable A");
|
|
544
|
+
expect(result.edges[0]!.parentNodeId).toBe("d1");
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("never lets an unparseable declaration displace a valid one, in either order", () => {
|
|
548
|
+
/*
|
|
549
|
+
* A parent naming a third device contributes nothing at all, so the
|
|
550
|
+
* one real declaration has to survive whichever row it sits in. This
|
|
551
|
+
* is the order-independence that matters: only DISAGREEING valid
|
|
552
|
+
* declarations are allowed to depend on ordering.
|
|
553
|
+
*/
|
|
554
|
+
const validFirst: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
555
|
+
[
|
|
556
|
+
makeDevice("d1", "core-1"),
|
|
557
|
+
makeDevice("d2", "access-1"),
|
|
558
|
+
makeDevice("d3", "bystander"),
|
|
559
|
+
],
|
|
560
|
+
now,
|
|
561
|
+
[],
|
|
562
|
+
[],
|
|
563
|
+
[
|
|
564
|
+
{ fromDeviceId: "d1", toDeviceId: "d2", parentDeviceId: "d1" },
|
|
565
|
+
{ fromDeviceId: "d1", toDeviceId: "d2", parentDeviceId: "d3" },
|
|
566
|
+
],
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
const invalidFirst: TopologyBuildResult =
|
|
570
|
+
NetworkTopologyUtil.buildTopology(
|
|
571
|
+
[
|
|
572
|
+
makeDevice("d1", "core-1"),
|
|
573
|
+
makeDevice("d2", "access-1"),
|
|
574
|
+
makeDevice("d3", "bystander"),
|
|
575
|
+
],
|
|
576
|
+
now,
|
|
577
|
+
[],
|
|
578
|
+
[],
|
|
579
|
+
[
|
|
580
|
+
{ fromDeviceId: "d1", toDeviceId: "d2", parentDeviceId: "d3" },
|
|
581
|
+
{ fromDeviceId: "d1", toDeviceId: "d2", parentDeviceId: "d1" },
|
|
582
|
+
],
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
expect(edgeBetween(validFirst, "d1", "d2")!.parentNodeId).toBe("d1");
|
|
586
|
+
expect(edgeBetween(invalidFirst, "d1", "d2")!.parentNodeId).toBe("d1");
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it("is order-independent when two links agree about the parent", () => {
|
|
590
|
+
/*
|
|
591
|
+
* Two rows saying the same thing — an explicit link and a rule that
|
|
592
|
+
* derived the same hierarchy — must not depend on merge order at all.
|
|
593
|
+
*/
|
|
594
|
+
const links: Array<TopologyManualLinkInput> = [
|
|
595
|
+
{ fromDeviceId: "d1", toDeviceId: "d2", parentDeviceId: "d1" },
|
|
596
|
+
// Stored the other way round, same declaration.
|
|
597
|
+
{ fromDeviceId: "d2", toDeviceId: "d1", parentDeviceId: "d1" },
|
|
598
|
+
];
|
|
599
|
+
const devices: Array<TopologyDeviceInput> = [
|
|
600
|
+
makeDevice("d1", "core-1"),
|
|
601
|
+
makeDevice("d2", "access-1"),
|
|
602
|
+
];
|
|
603
|
+
|
|
604
|
+
const forward: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
605
|
+
devices,
|
|
606
|
+
now,
|
|
607
|
+
[],
|
|
608
|
+
[],
|
|
609
|
+
links,
|
|
610
|
+
);
|
|
611
|
+
const reversed: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
612
|
+
devices,
|
|
613
|
+
now,
|
|
614
|
+
[],
|
|
615
|
+
[],
|
|
616
|
+
[...links].reverse(),
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
expect(forward.edges).toHaveLength(1);
|
|
620
|
+
expect(reversed.edges).toHaveLength(1);
|
|
621
|
+
expect(forward.edges[0]!.parentNodeId).toBe("d1");
|
|
622
|
+
expect(reversed.edges[0]!.parentNodeId).toBe("d1");
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it("declares a parent on one pair without touching the pairs around it", () => {
|
|
626
|
+
/*
|
|
627
|
+
* A three-device chain where only the middle cable is declared. The
|
|
628
|
+
* layout has to be able to tell a stated hierarchy from an inferred
|
|
629
|
+
* one per edge, so a declaration must not bleed sideways.
|
|
630
|
+
*/
|
|
631
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
632
|
+
[
|
|
633
|
+
makeDevice("d1", "core-1"),
|
|
634
|
+
makeDevice("d2", "dist-1"),
|
|
635
|
+
makeDevice("d3", "access-1"),
|
|
636
|
+
],
|
|
637
|
+
now,
|
|
638
|
+
[],
|
|
639
|
+
[],
|
|
640
|
+
[
|
|
641
|
+
{ fromDeviceId: "d1", toDeviceId: "d2", parentDeviceId: "d1" },
|
|
642
|
+
{ fromDeviceId: "d2", toDeviceId: "d3" },
|
|
643
|
+
],
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
expect(result.edges).toHaveLength(2);
|
|
647
|
+
expect(edgeBetween(result, "d1", "d2")!.parentNodeId).toBe("d1");
|
|
648
|
+
expect(edgeBetween(result, "d2", "d3")!.parentNodeId).toBeUndefined();
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
it("keeps declarations off the fdb edges that hang endpoints on a switch", () => {
|
|
652
|
+
/*
|
|
653
|
+
* Endpoint attachment is measured, not declared, and its edges are
|
|
654
|
+
* appended after the manual merge entirely. A declared parent on the
|
|
655
|
+
* uplink must not leak onto them.
|
|
656
|
+
*/
|
|
657
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
658
|
+
[makeDevice("d1", "core-1"), makeDevice("d2", "access-1")],
|
|
659
|
+
now,
|
|
660
|
+
[],
|
|
661
|
+
[
|
|
662
|
+
{
|
|
663
|
+
id: "ep-1",
|
|
664
|
+
macAddress: "aa:bb:cc:dd:ee:01",
|
|
665
|
+
attachedNetworkDeviceId: "d2",
|
|
666
|
+
lastSeenAt: fresh,
|
|
667
|
+
},
|
|
668
|
+
],
|
|
669
|
+
[{ fromDeviceId: "d1", toDeviceId: "d2", parentDeviceId: "d1" }],
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
expect(edgeBetween(result, "d1", "d2")!.parentNodeId).toBe("d1");
|
|
673
|
+
const fdbEdge: NetworkTopologyEdge | undefined = edgeBetween(
|
|
674
|
+
result,
|
|
675
|
+
"d2",
|
|
676
|
+
"endpoint:ep-1",
|
|
677
|
+
);
|
|
678
|
+
expect(fdbEdge).toBeDefined();
|
|
679
|
+
expect(fdbEdge!.protocols).toEqual(["fdb"]);
|
|
680
|
+
expect(fdbEdge!.parentNodeId).toBeUndefined();
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
describe("both declarations on one graph", () => {
|
|
685
|
+
it("draws a ping-only access switch under its declared parent", () => {
|
|
686
|
+
/*
|
|
687
|
+
* The end-to-end shape of issue #3192: a device that only answers
|
|
688
|
+
* ping, given a role by hand and hung under a core switch by hand,
|
|
689
|
+
* with neither statement needing SNMP to have said anything.
|
|
690
|
+
*/
|
|
691
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
692
|
+
[
|
|
693
|
+
makeDevice("d1", "core-1", {
|
|
694
|
+
sysName: "core-1",
|
|
695
|
+
sysDescr: "Cisco IOS Software, Catalyst 9300-48P",
|
|
696
|
+
}),
|
|
697
|
+
makeDevice("d2", "idf2-ping-only", {
|
|
698
|
+
deviceRole: "switch",
|
|
699
|
+
lastSeenAt: undefined,
|
|
700
|
+
monitorStatus: "up",
|
|
701
|
+
isNeighborDiscoveryEnabled: false,
|
|
702
|
+
}),
|
|
703
|
+
],
|
|
704
|
+
now,
|
|
705
|
+
[],
|
|
706
|
+
[],
|
|
707
|
+
[
|
|
708
|
+
{
|
|
709
|
+
fromDeviceId: "d2",
|
|
710
|
+
toDeviceId: "d1",
|
|
711
|
+
name: "IDF-2 uplink",
|
|
712
|
+
parentDeviceId: "d1",
|
|
713
|
+
},
|
|
714
|
+
],
|
|
715
|
+
);
|
|
716
|
+
|
|
717
|
+
expect(nodeById(result, "d1")!.role).toBe("switch");
|
|
718
|
+
expect(nodeById(result, "d2")!.role).toBe("switch");
|
|
719
|
+
expect(nodeById(result, "d2")!.status).toBe("up");
|
|
720
|
+
|
|
721
|
+
expect(result.edges).toHaveLength(1);
|
|
722
|
+
expect(result.edges[0]!.protocols).toEqual(["manual"]);
|
|
723
|
+
expect(result.edges[0]!.name).toBe("IDF-2 uplink");
|
|
724
|
+
expect(result.edges[0]!.parentNodeId).toBe("d1");
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it("drops a declared parent along with the node the project hid", () => {
|
|
728
|
+
/*
|
|
729
|
+
* Suppression runs last, on the finished graph, and takes an edge's
|
|
730
|
+
* declaration with the edge. What must NOT happen is the hierarchy
|
|
731
|
+
* surviving as a dangling reference to a node nobody draws.
|
|
732
|
+
*/
|
|
733
|
+
const result: TopologyBuildResult = NetworkTopologyUtil.buildTopology(
|
|
734
|
+
[
|
|
735
|
+
makeDevice("d1", "core-1"),
|
|
736
|
+
makeDevice("d2", "dist-1"),
|
|
737
|
+
makeDevice("d3", "access-1"),
|
|
738
|
+
],
|
|
739
|
+
now,
|
|
740
|
+
[],
|
|
741
|
+
[],
|
|
742
|
+
[
|
|
743
|
+
{ fromDeviceId: "d1", toDeviceId: "d2", parentDeviceId: "d1" },
|
|
744
|
+
{ fromDeviceId: "d2", toDeviceId: "d3", parentDeviceId: "d2" },
|
|
745
|
+
],
|
|
746
|
+
new Set<string>(["d1"]),
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
expect(nodeById(result, "d1")).toBeUndefined();
|
|
750
|
+
expect(result.suppressedNodeCount).toBe(1);
|
|
751
|
+
expect(result.edges).toHaveLength(1);
|
|
752
|
+
expect(result.edges[0]!.parentNodeId).toBe("d2");
|
|
753
|
+
// No edge left pointing at the hidden parent.
|
|
754
|
+
for (const edge of result.edges) {
|
|
755
|
+
expect(edge.parentNodeId).not.toBe("d1");
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
});
|
|
759
|
+
});
|