@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.
Files changed (188) hide show
  1. package/Models/DatabaseModels/AlertState.ts +0 -2
  2. package/Models/DatabaseModels/Domain.ts +1 -1
  3. package/Models/DatabaseModels/Incident.ts +1 -1
  4. package/Models/DatabaseModels/IncidentTemplate.ts +1 -1
  5. package/Models/DatabaseModels/Index.ts +2 -0
  6. package/Models/DatabaseModels/MetricType.ts +0 -2
  7. package/Models/DatabaseModels/MonitorStatusTimeline.ts +0 -2
  8. package/Models/DatabaseModels/MonitorTest.ts +0 -2
  9. package/Models/DatabaseModels/NetworkDevice.ts +121 -2
  10. package/Models/DatabaseModels/NetworkDeviceOidTemplate.ts +3 -1
  11. package/Models/DatabaseModels/NetworkDeviceRole.ts +648 -0
  12. package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -1
  13. package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -1
  14. package/Models/DatabaseModels/StatusPageAnnouncement.ts +0 -2
  15. package/Models/DatabaseModels/StatusPageAnnouncementTemplate.ts +0 -2
  16. package/Models/DatabaseModels/StatusPagePrivateUser.ts +0 -2
  17. package/Models/DatabaseModels/StatusPageResource.ts +0 -2
  18. package/Models/DatabaseModels/StatusPageSubscriber.ts +0 -2
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.ts +72 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  21. package/Server/Services/DatabaseService.ts +39 -9
  22. package/Server/Services/Index.ts +2 -0
  23. package/Server/Services/NetworkDeviceRoleService.ts +90 -0
  24. package/Server/Services/ProjectService.ts +85 -0
  25. package/Server/Services/UserNotificationSettingService.ts +0 -6
  26. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +31 -1
  27. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +35 -8
  28. package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +107 -3
  29. package/Tests/App/Dashboard/DashboardWideChartZoom.test.tsx +401 -0
  30. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +318 -0
  31. package/Tests/App/Dashboard/NetworkDeviceBulkDelete.test.tsx +357 -0
  32. package/Tests/App/Dashboard/NetworkSideMenu.test.tsx +590 -0
  33. package/Tests/App/Dashboard/SideMenuHarness.tsx +11 -3
  34. package/Tests/Models/DatabaseModels/ModelRegistryInvariants.test.ts +115 -41
  35. package/Tests/Server/Infrastructure/Postgres/NetworkDeviceRoleTableMigration.test.ts +476 -0
  36. package/Tests/Server/Services/DatabaseServiceGenerateSlug.test.ts +215 -0
  37. package/Tests/Server/Services/NetworkDeviceRoleModel.test.ts +672 -0
  38. package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +1 -1
  39. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +67 -4
  40. package/Tests/Server/Services/ScheduledMaintenanceSubscriberEndTime.test.ts +369 -0
  41. package/Tests/Server/Services/UserNotificationSettingMonitorDefaults.test.ts +248 -0
  42. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +43 -2
  43. package/Tests/Server/Utils/BasicCron.test.ts +283 -0
  44. package/Tests/Server/Utils/IncomingCallPhoneNumber.test.ts +174 -0
  45. package/Tests/Types/NetworkDevice/DefaultNetworkDeviceRole.test.ts +417 -0
  46. package/Tests/UI/Components/Charts/ChartClickDoubleClickDisambiguation.test.tsx +221 -0
  47. package/Tests/UI/Components/Charts/ChartDoubleClickReset.test.tsx +220 -0
  48. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +751 -0
  49. package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +475 -43
  50. package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +89 -0
  51. package/Tests/UI/Components/NavBarCommandKGating.test.tsx +103 -8
  52. package/Tests/UI/Components/SideMenuItem.test.tsx +30 -0
  53. package/Tests/UI/Utils/TableColumnsToCsv.test.ts +30 -0
  54. package/Tests/UI/Utils/UseDashboardTimeRangeZoom.test.tsx +187 -0
  55. package/Tests/Utils/Dashboard/DashboardResourceListSharedArgs.test.ts +302 -0
  56. package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +242 -0
  57. package/Tests/Utils/Monitor/NetworkDeviceRoleCatalog.test.ts +981 -0
  58. package/Tests/Utils/Monitor/NetworkTopologyRoleCatalog.test.ts +865 -0
  59. package/Tests/Utils/NetworkDevice/DeviceRoleKeyUtil.test.ts +538 -0
  60. package/Tests/Utils/Slo/SloEvaluation.test.ts +171 -0
  61. package/Tests/Utils/Slug.test.ts +61 -1
  62. package/Tests/jest.setup.ts +19 -0
  63. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +61 -0
  64. package/Types/NetworkDevice/DefaultNetworkDeviceRole.ts +145 -0
  65. package/Types/Permission.ts +45 -0
  66. package/UI/Components/Charts/Area/AreaChart.tsx +7 -0
  67. package/UI/Components/Charts/Bar/BarChart.tsx +6 -0
  68. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +9 -1
  69. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +97 -35
  70. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +14 -0
  71. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +97 -35
  72. package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +13 -0
  73. package/UI/Components/Charts/Line/LineChart.tsx +7 -0
  74. package/UI/Components/ModelTable/BaseModelTable.tsx +50 -15
  75. package/UI/Components/Navbar/NavBar.tsx +62 -13
  76. package/UI/Components/Navbar/NavBarMenuModal.tsx +7 -2
  77. package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +2 -0
  78. package/UI/Components/SideMenu/SideMenu.tsx +12 -2
  79. package/UI/Components/SideMenu/SideMenuItem.tsx +4 -1
  80. package/UI/Components/SideMenu/SideMenuSection.tsx +14 -2
  81. package/UI/Utils/UseDashboardTimeRangeZoom.ts +77 -0
  82. package/Utils/Dashboard/DashboardTimeRangeZoom.ts +118 -0
  83. package/Utils/Monitor/NetworkDeviceRoleCatalog.ts +232 -0
  84. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +2 -1
  85. package/Utils/Monitor/NetworkTopologyUtil.ts +116 -33
  86. package/Utils/NetworkDevice/DeviceRoleKeyUtil.ts +137 -0
  87. package/Utils/Rules/RulePatternMatchUtil.ts +1 -1
  88. package/Utils/Slug.ts +34 -1
  89. package/build/dist/Models/DatabaseModels/AlertState.js +0 -2
  90. package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
  91. package/build/dist/Models/DatabaseModels/Domain.js +1 -1
  92. package/build/dist/Models/DatabaseModels/Domain.js.map +1 -1
  93. package/build/dist/Models/DatabaseModels/Incident.js +1 -1
  94. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  95. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +1 -1
  96. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  97. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  98. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/MetricType.js +0 -2
  100. package/build/dist/Models/DatabaseModels/MetricType.js.map +1 -1
  101. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +0 -2
  102. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  103. package/build/dist/Models/DatabaseModels/MonitorTest.js +0 -2
  104. package/build/dist/Models/DatabaseModels/MonitorTest.js.map +1 -1
  105. package/build/dist/Models/DatabaseModels/NetworkDevice.js +124 -3
  106. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  107. package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js +3 -1
  108. package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js.map +1 -1
  109. package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js +677 -0
  110. package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js.map +1 -0
  111. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -1
  112. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  113. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -1
  114. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  115. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +0 -2
  116. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  117. package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js +0 -2
  118. package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js.map +1 -1
  119. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +0 -2
  120. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
  121. package/build/dist/Models/DatabaseModels/StatusPageResource.js +0 -2
  122. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  123. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +0 -2
  124. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js +39 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js.map +1 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  129. package/build/dist/Server/Services/DatabaseService.js +31 -5
  130. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  131. package/build/dist/Server/Services/Index.js +2 -0
  132. package/build/dist/Server/Services/Index.js.map +1 -1
  133. package/build/dist/Server/Services/NetworkDeviceRoleService.js +70 -0
  134. package/build/dist/Server/Services/NetworkDeviceRoleService.js.map +1 -0
  135. package/build/dist/Server/Services/ProjectService.js +65 -0
  136. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  137. package/build/dist/Server/Services/UserNotificationSettingService.js +0 -1
  138. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  139. package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js +105 -0
  140. package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js.map +1 -0
  141. package/build/dist/Types/Permission.js +40 -0
  142. package/build/dist/Types/Permission.js.map +1 -1
  143. package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
  144. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  145. package/build/dist/UI/Components/Charts/Bar/BarChart.js +1 -1
  146. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  147. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +6 -1
  148. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  149. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +79 -31
  150. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  151. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +7 -1
  152. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  153. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +79 -31
  154. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  155. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +14 -0
  156. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -0
  157. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  158. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  159. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +39 -13
  160. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  161. package/build/dist/UI/Components/Navbar/NavBar.js +44 -9
  162. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  163. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
  164. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  165. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +1 -1
  166. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
  167. package/build/dist/UI/Components/SideMenu/SideMenu.js +9 -6
  168. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  169. package/build/dist/UI/Components/SideMenu/SideMenuItem.js +1 -1
  170. package/build/dist/UI/Components/SideMenu/SideMenuItem.js.map +1 -1
  171. package/build/dist/UI/Components/SideMenu/SideMenuSection.js +7 -2
  172. package/build/dist/UI/Components/SideMenu/SideMenuSection.js.map +1 -1
  173. package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js +42 -0
  174. package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js.map +1 -0
  175. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +74 -0
  176. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -0
  177. package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js +126 -0
  178. package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js.map +1 -0
  179. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +2 -1
  180. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
  181. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +67 -32
  182. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  183. package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js +114 -0
  184. package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js.map +1 -0
  185. package/build/dist/Utils/Rules/RulePatternMatchUtil.js +1 -1
  186. package/build/dist/Utils/Slug.js +31 -1
  187. package/build/dist/Utils/Slug.js.map +1 -1
  188. package/package.json +1 -1
@@ -0,0 +1,174 @@
1
+ import { beforeEach, describe, expect, jest, test } from "@jest/globals";
2
+ import ObjectID from "../../../Types/ObjectID";
3
+ import API from "../../../Utils/API";
4
+ import ClusterKeyAuthorization from "../../../Server/Middleware/ClusterKeyAuthorization";
5
+ import logger from "../../../Server/Utils/Logger";
6
+ import releaseIncomingCallPhoneNumber from "../../../Server/Utils/IncomingCallPhoneNumber";
7
+ import { JSONObject } from "../../../Types/JSON";
8
+
9
+ /*
10
+ * Releasing a provisioned incoming-call number when its config is deleted.
11
+ *
12
+ * The Common layer cannot reach the call provider itself, so this hands the
13
+ * release to the notification app's internal endpoint. It runs during cleanup,
14
+ * behind a delete the user has already asked for, and its whole contract is
15
+ * that it FAILS OPEN: the provider being unreachable, the notification app
16
+ * being mid-restart, or the endpoint answering 500 must all leave the parent
17
+ * delete to finish. A throw escaping here would leave the user's config
18
+ * undeletable for as long as the provider is unwell.
19
+ *
20
+ * The endpoint is internal and authenticated by cluster key rather than by a
21
+ * user, so the header is not optional decoration — a request without it is
22
+ * rejected and the number silently stays provisioned, which is a bill the
23
+ * project keeps paying for something it deleted.
24
+ */
25
+
26
+ jest.mock("../../../Utils/API", () => {
27
+ return { __esModule: true, default: { post: jest.fn() } };
28
+ });
29
+
30
+ jest.mock("../../../Server/Middleware/ClusterKeyAuthorization", () => {
31
+ return {
32
+ __esModule: true,
33
+ default: { getClusterKeyHeaders: jest.fn() },
34
+ };
35
+ });
36
+
37
+ jest.mock("../../../Server/Utils/Logger", () => {
38
+ return {
39
+ __esModule: true,
40
+ default: {
41
+ debug: jest.fn(),
42
+ info: jest.fn(),
43
+ warn: jest.fn(),
44
+ error: jest.fn(),
45
+ },
46
+ };
47
+ });
48
+
49
+ const api: { post: jest.Mock } = API as unknown as { post: jest.Mock };
50
+
51
+ const clusterKey: { getClusterKeyHeaders: jest.Mock } =
52
+ ClusterKeyAuthorization as unknown as {
53
+ getClusterKeyHeaders: jest.Mock;
54
+ };
55
+
56
+ const mockedLogger: { error: jest.Mock } = logger as unknown as {
57
+ error: jest.Mock;
58
+ };
59
+
60
+ const CONFIG_ID: ObjectID = ObjectID.generate();
61
+ const PROVIDER_NUMBER_ID: string = "PN_provider_abc123";
62
+
63
+ function callArgs(): JSONObject {
64
+ return api.post.mock.calls[0]![0] as JSONObject;
65
+ }
66
+
67
+ function release(): Promise<void> {
68
+ return releaseIncomingCallPhoneNumber({
69
+ projectCallSMSConfigId: CONFIG_ID,
70
+ callProviderPhoneNumberId: PROVIDER_NUMBER_ID,
71
+ });
72
+ }
73
+
74
+ describe("releaseIncomingCallPhoneNumber", () => {
75
+ beforeEach(() => {
76
+ jest.clearAllMocks();
77
+ api.post.mockResolvedValue(undefined as never);
78
+ clusterKey.getClusterKeyHeaders.mockReturnValue({
79
+ clusterkey: "test-cluster-key",
80
+ });
81
+ });
82
+
83
+ describe("the request it makes", () => {
84
+ test("posts to the notification app's internal release endpoint", async () => {
85
+ await release();
86
+
87
+ expect(api.post).toHaveBeenCalledTimes(1);
88
+ expect(callArgs()["url"]!.toString()).toContain(
89
+ "/api/notification/phone-number/internal/release",
90
+ );
91
+ });
92
+
93
+ /*
94
+ * Both ids are needed: the provider's own id says which number to hand
95
+ * back, and the config id is what the endpoint authorises the release
96
+ * against. Dropping either silently leaves the number provisioned.
97
+ */
98
+ test("names both the config and the provider's number", async () => {
99
+ await release();
100
+
101
+ const data: JSONObject = callArgs()["data"] as JSONObject;
102
+
103
+ expect(data["projectCallSMSConfigId"]).toBe(CONFIG_ID.toString());
104
+ expect(data["callProviderPhoneNumberId"]).toBe(PROVIDER_NUMBER_ID);
105
+ });
106
+
107
+ /*
108
+ * The id is sent as its string form, not as an ObjectID that would
109
+ * serialize to whatever JSON.stringify makes of the instance.
110
+ */
111
+ test("the config id is sent as a plain string", async () => {
112
+ await release();
113
+
114
+ expect(
115
+ typeof (callArgs()["data"] as JSONObject)["projectCallSMSConfigId"],
116
+ ).toBe("string");
117
+ });
118
+
119
+ /*
120
+ * An internal endpoint with no user behind it. Without the cluster key the
121
+ * request is rejected and the number stays provisioned - and because this
122
+ * function swallows failures, that rejection would be invisible.
123
+ */
124
+ test("carries the cluster key the internal endpoint authenticates on", async () => {
125
+ await release();
126
+
127
+ expect(clusterKey.getClusterKeyHeaders).toHaveBeenCalled();
128
+ expect((callArgs()["headers"] as JSONObject)["clusterkey"]).toBe(
129
+ "test-cluster-key",
130
+ );
131
+ });
132
+ });
133
+
134
+ describe("it fails open, so cleanup never blocks the delete", () => {
135
+ /*
136
+ * The parent delete has already been asked for. A provider or network
137
+ * problem during cleanup must not resurrect the row the user deleted.
138
+ */
139
+ test("a rejected request resolves rather than throwing", async () => {
140
+ api.post.mockRejectedValue(new Error("provider unreachable") as never);
141
+
142
+ await expect(release()).resolves.toBeUndefined();
143
+ });
144
+
145
+ test("the failure is logged rather than swallowed silently", async () => {
146
+ api.post.mockRejectedValue(new Error("provider unreachable") as never);
147
+
148
+ await release();
149
+
150
+ expect(mockedLogger.error).toHaveBeenCalled();
151
+ });
152
+
153
+ /*
154
+ * A synchronous throw while the request is still being assembled - a
155
+ * missing cluster key secret, say - is the same kind of cleanup failure
156
+ * and must not escape either.
157
+ */
158
+ test("a throw while building the request is caught too", async () => {
159
+ clusterKey.getClusterKeyHeaders.mockImplementation((): never => {
160
+ throw new Error("no cluster key configured");
161
+ });
162
+
163
+ await expect(release()).resolves.toBeUndefined();
164
+ expect(mockedLogger.error).toHaveBeenCalled();
165
+ expect(api.post).not.toHaveBeenCalled();
166
+ });
167
+
168
+ test("a successful release logs no error", async () => {
169
+ await release();
170
+
171
+ expect(mockedLogger.error).not.toHaveBeenCalled();
172
+ });
173
+ });
174
+ });
@@ -0,0 +1,417 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import {
3
+ NetworkTopologyDeviceRole,
4
+ NetworkTopologyNode,
5
+ NetworkTopologyNodeShape,
6
+ } from "../../../Types/Monitor/SnmpMonitor/NetworkTopology";
7
+ import DEFAULT_NETWORK_DEVICE_ROLES, {
8
+ DefaultNetworkDeviceRole,
9
+ } from "../../../Types/NetworkDevice/DefaultNetworkDeviceRole";
10
+ import {
11
+ DEVICE_ROLES_IN_LEGEND_ORDER,
12
+ DEVICE_ROLE_LABELS,
13
+ } from "../../../Utils/Monitor/NetworkDeviceRoleUtil";
14
+ import { shapeForNode } from "../../../../App/FeatureSet/Dashboard/src/Components/NetworkDevice/TopologyNodeShape";
15
+
16
+ /*
17
+ * DEFAULT_NETWORK_DEVICE_ROLES is what every project starts life with, and
18
+ * the promise it makes is a conservative one: a project seeded from it must
19
+ * behave EXACTLY as the product behaved when roles were a hardcoded union.
20
+ * Same eleven roles, same labels, same silhouettes, same tiering, same
21
+ * SNMP-versus-monitor default.
22
+ *
23
+ * That promise is only checkable by comparing the seed list against the three
24
+ * modules it replaced — the labels in NetworkDeviceRoleUtil, the SHAPE_BY_ROLE
25
+ * map in the renderer, the CORE_DEVICE_ROLES set in the layout and the
26
+ * MONITOR_BACKED_ROLES set in the adopt flow. So that is what these tests do:
27
+ * every one of them is a cross-check against the old source of truth, not a
28
+ * restatement of the new one.
29
+ *
30
+ * The shape check imports the renderer directly (SHAPE_BY_ROLE itself is not
31
+ * exported, but shapeForNode is, and it is the function the map actually
32
+ * calls). Common's jest maps "Common/*" back to this package and several
33
+ * Common tests already import App/FeatureSet/Dashboard sources this way, so a
34
+ * genuine comparison is available and is worth far more than a second copy of
35
+ * the table. The literal table below is kept anyway, so a reader can see the
36
+ * eleven answers without following the import.
37
+ */
38
+
39
+ type MakeDeviceNodeFunction = (
40
+ role: NetworkTopologyDeviceRole,
41
+ ) => NetworkTopologyNode;
42
+
43
+ /*
44
+ * A node as an OLD payload carried it: the classified role and nothing else.
45
+ * Deliberately no roleShape — that is the per-project stamp, and stamping it
46
+ * would make shapeForNode echo the value back instead of consulting the
47
+ * built-in map this test is comparing against.
48
+ */
49
+ const makeDeviceNode: MakeDeviceNodeFunction = (
50
+ role: NetworkTopologyDeviceRole,
51
+ ): NetworkTopologyNode => {
52
+ return {
53
+ id: `device-${role}`,
54
+ name: role,
55
+ isManaged: true,
56
+ status: "up",
57
+ kind: "device",
58
+ role: role,
59
+ };
60
+ };
61
+
62
+ describe("DEFAULT_NETWORK_DEVICE_ROLES — what a fresh project is given", () => {
63
+ test("seeds exactly eleven roles", () => {
64
+ expect(DEFAULT_NETWORK_DEVICE_ROLES).toHaveLength(11);
65
+ });
66
+
67
+ /*
68
+ * "unknown" is the classifier saying it has no answer, not a job a box
69
+ * does. Offering it as a row an operator could assign would turn "I don't
70
+ * know yet" into "never classify this device again", which is a different
71
+ * and much worse statement.
72
+ */
73
+ test("does not seed 'unknown', which is an absence of an answer rather than a role", () => {
74
+ const keys: Array<string> = DEFAULT_NETWORK_DEVICE_ROLES.map(
75
+ (role: DefaultNetworkDeviceRole): string => {
76
+ return role.key;
77
+ },
78
+ );
79
+
80
+ expect(keys).not.toContain("unknown");
81
+ });
82
+
83
+ test("every key is one of the built-in roles the SNMP classifier can return", () => {
84
+ /*
85
+ * The classifier is evidence-driven and speaks the fixed union; it cannot
86
+ * invent a project's custom role. A seeded key outside that union would be
87
+ * a row nothing could ever match.
88
+ */
89
+ for (const role of DEFAULT_NETWORK_DEVICE_ROLES) {
90
+ expect(DEVICE_ROLES_IN_LEGEND_ORDER).toContain(role.key);
91
+ }
92
+ });
93
+ });
94
+
95
+ describe("DEFAULT_NETWORK_DEVICE_ROLES — order", () => {
96
+ /*
97
+ * The seeded `order` column is this array's index, and the legend reads in
98
+ * DEVICE_ROLES_IN_LEGEND_ORDER. If the two ever diverge, a fresh project's
99
+ * settings page and its map legend list the same eleven roles in two
100
+ * different sequences — the sort of inconsistency nobody files a bug for and
101
+ * everybody trips over.
102
+ */
103
+ test("the keys are DEVICE_ROLES_IN_LEGEND_ORDER, in that exact order", () => {
104
+ const keys: Array<NetworkTopologyDeviceRole> =
105
+ DEFAULT_NETWORK_DEVICE_ROLES.map(
106
+ (role: DefaultNetworkDeviceRole): NetworkTopologyDeviceRole => {
107
+ return role.key;
108
+ },
109
+ );
110
+
111
+ expect(keys).toEqual(Array.from(DEVICE_ROLES_IN_LEGEND_ORDER));
112
+ });
113
+
114
+ test("the two lists are the same SET as well as the same sequence", () => {
115
+ // Guards the case where both lists are edited but only one is reordered.
116
+ const seeded: Set<string> = new Set<string>(
117
+ DEFAULT_NETWORK_DEVICE_ROLES.map(
118
+ (role: DefaultNetworkDeviceRole): string => {
119
+ return role.key;
120
+ },
121
+ ),
122
+ );
123
+
124
+ expect(seeded.size).toBe(DEVICE_ROLES_IN_LEGEND_ORDER.length);
125
+ for (const key of DEVICE_ROLES_IN_LEGEND_ORDER) {
126
+ expect(seeded.has(key)).toBe(true);
127
+ }
128
+ });
129
+ });
130
+
131
+ describe("DEFAULT_NETWORK_DEVICE_ROLES — names reproduce the built-in labels", () => {
132
+ /*
133
+ * The name is what the legend, the detail panel and the accessible label
134
+ * show once a project is seeded, because the topology builder stamps the
135
+ * configured label onto every node. If a seeded name drifted from
136
+ * DEVICE_ROLE_LABELS, a freshly created project's map would be relabelled
137
+ * relative to an old one for no reason the operator asked for.
138
+ */
139
+ test("every default's name is the built-in label for its key", () => {
140
+ for (const role of DEFAULT_NETWORK_DEVICE_ROLES) {
141
+ expect(role.name).toBe(DEVICE_ROLE_LABELS[role.key]);
142
+ }
143
+ });
144
+
145
+ test("the eleven names are exactly the eleven non-unknown labels", () => {
146
+ const names: Array<string> = DEFAULT_NETWORK_DEVICE_ROLES.map(
147
+ (role: DefaultNetworkDeviceRole): string => {
148
+ return role.name;
149
+ },
150
+ );
151
+
152
+ expect(names).toEqual([
153
+ "Router",
154
+ "Switch",
155
+ "Firewall",
156
+ "Wireless AP",
157
+ "Load balancer",
158
+ "Server",
159
+ "Storage",
160
+ "Printer",
161
+ "Camera",
162
+ "IP phone",
163
+ "Host",
164
+ ]);
165
+ expect(names).not.toContain(DEVICE_ROLE_LABELS.unknown);
166
+ });
167
+ });
168
+
169
+ describe("DEFAULT_NETWORK_DEVICE_ROLES — shapes reproduce the renderer's map", () => {
170
+ /*
171
+ * Mirrors the renderer's private SHAPE_BY_ROLE. Written out so the eleven
172
+ * silhouettes are readable here; the test below is the one that proves they
173
+ * still agree with the renderer.
174
+ */
175
+ const EXPECTED_SHAPE_BY_KEY: Readonly<
176
+ Record<string, NetworkTopologyNodeShape>
177
+ > = {
178
+ router: "circle",
179
+ switch: "rounded-square",
180
+ firewall: "diamond",
181
+ wirelessAccessPoint: "triangle",
182
+ loadBalancer: "hexagon",
183
+ server: "tower",
184
+ storage: "cylinder",
185
+ // The four leaf roles deliberately share the endpoint rect.
186
+ printer: "rect",
187
+ camera: "rect",
188
+ phone: "rect",
189
+ host: "rect",
190
+ };
191
+
192
+ test("every default's topologyShape is what the renderer would draw for that role", () => {
193
+ /*
194
+ * The real cross-check, and the regression this whole file exists to
195
+ * prevent: a project seeded from these defaults must draw the map it drew
196
+ * before roles were configurable. shapeForNode on a node carrying only a
197
+ * classified role is exactly the pre-feature code path.
198
+ */
199
+ for (const role of DEFAULT_NETWORK_DEVICE_ROLES) {
200
+ expect(role.topologyShape).toBe(shapeForNode(makeDeviceNode(role.key)));
201
+ }
202
+ });
203
+
204
+ test("and it is the silhouette the table above names", () => {
205
+ for (const role of DEFAULT_NETWORK_DEVICE_ROLES) {
206
+ expect(role.topologyShape).toBe(EXPECTED_SHAPE_BY_KEY[role.key]);
207
+ }
208
+ });
209
+
210
+ test("the seven infrastructure roles are seven distinct silhouettes", () => {
211
+ // A map on which a router, a switch and a firewall look alike is unreadable.
212
+ const infrastructureShapes: Array<NetworkTopologyNodeShape> =
213
+ DEFAULT_NETWORK_DEVICE_ROLES.filter(
214
+ (role: DefaultNetworkDeviceRole): boolean => {
215
+ return role.topologyShape !== "rect";
216
+ },
217
+ ).map((role: DefaultNetworkDeviceRole): NetworkTopologyNodeShape => {
218
+ return role.topologyShape;
219
+ });
220
+
221
+ expect(infrastructureShapes).toHaveLength(7);
222
+ expect(new Set<string>(infrastructureShapes).size).toBe(7);
223
+ });
224
+
225
+ test("every seeded shape is one the renderer has geometry for", () => {
226
+ /*
227
+ * topologyShape is a free-text column and the catalogue drops any value
228
+ * the renderer cannot draw. A seeded value it dropped would leave the
229
+ * node falling back to a circle, silently.
230
+ */
231
+ const drawableShapes: ReadonlyArray<NetworkTopologyNodeShape> = [
232
+ "circle",
233
+ "rounded-square",
234
+ "diamond",
235
+ "triangle",
236
+ "hexagon",
237
+ "tower",
238
+ "cylinder",
239
+ "rect",
240
+ ];
241
+
242
+ for (const role of DEFAULT_NETWORK_DEVICE_ROLES) {
243
+ expect(drawableShapes).toContain(role.topologyShape);
244
+ }
245
+ });
246
+ });
247
+
248
+ describe("DEFAULT_NETWORK_DEVICE_ROLES — isCoreLayer reproduces CORE_DEVICE_ROLES", () => {
249
+ /*
250
+ * Mirrors the private CORE_DEVICE_ROLES set in TopologyLayout. Note what is
251
+ * NOT in it: "switch". A switch is infrastructure, but it hangs off a
252
+ * router, and the top tier is for the devices that hang off nothing.
253
+ *
254
+ * tierForNode now reads node.isCoreLayerRole BEFORE node.role, so these
255
+ * eleven booleans are what decides the tier on every seeded project. Get one
256
+ * wrong and the parent-child tree roots itself on the wrong device.
257
+ */
258
+ const CORE_KEYS: ReadonlyArray<string> = [
259
+ "router",
260
+ "firewall",
261
+ "loadBalancer",
262
+ ];
263
+
264
+ test("exactly router, firewall and load balancer sit at the core", () => {
265
+ const coreKeys: Array<string> = DEFAULT_NETWORK_DEVICE_ROLES.filter(
266
+ (role: DefaultNetworkDeviceRole): boolean => {
267
+ return role.isCoreLayer;
268
+ },
269
+ ).map((role: DefaultNetworkDeviceRole): string => {
270
+ return role.key;
271
+ });
272
+
273
+ expect(coreKeys).toEqual(CORE_KEYS);
274
+ });
275
+
276
+ test("every other default is explicitly not core, never left undefined", () => {
277
+ /*
278
+ * The flag has to be a real boolean on every row: tierForNode branches on
279
+ * `isCoreLayerRole !== undefined`, so an absent value would silently hand
280
+ * the decision back to the FDB heuristic the role was meant to overrule.
281
+ */
282
+ for (const role of DEFAULT_NETWORK_DEVICE_ROLES) {
283
+ expect(typeof role.isCoreLayer).toBe("boolean");
284
+ expect(role.isCoreLayer).toBe(CORE_KEYS.includes(role.key));
285
+ }
286
+ });
287
+
288
+ test("a switch is not core, and neither is an access point", () => {
289
+ // Called out because both are tempting to promote and both would be wrong.
290
+ const byKey: Map<string, DefaultNetworkDeviceRole> = new Map<
291
+ string,
292
+ DefaultNetworkDeviceRole
293
+ >(
294
+ DEFAULT_NETWORK_DEVICE_ROLES.map(
295
+ (
296
+ role: DefaultNetworkDeviceRole,
297
+ ): [string, DefaultNetworkDeviceRole] => {
298
+ return [role.key, role];
299
+ },
300
+ ),
301
+ );
302
+
303
+ expect(byKey.get("switch")?.isCoreLayer).toBe(false);
304
+ expect(byKey.get("wirelessAccessPoint")?.isCoreLayer).toBe(false);
305
+ });
306
+ });
307
+
308
+ describe("DEFAULT_NETWORK_DEVICE_ROLES — isSnmpWalkable reproduces MONITOR_BACKED_ROLES", () => {
309
+ /*
310
+ * Mirrors the private MONITOR_BACKED_ROLES set in AdoptNeighborUtil, which
311
+ * lists the roles that are never SNMP-walkable in practice. Adopting a
312
+ * neighbour with one of them defaults the new device to a monitor rather
313
+ * than to SNMP — defaulting a desk phone to SNMP would queue a walk it can
314
+ * only fail and leave the operator reading "pending" forever.
315
+ *
316
+ * Everything else, including an unclassified peer, defaults to SNMP: an
317
+ * unidentified box on a switch uplink is far more often a switch nobody has
318
+ * added yet than it is a kiosk.
319
+ */
320
+ const MONITOR_BACKED_KEYS: ReadonlyArray<string> = [
321
+ "printer",
322
+ "camera",
323
+ "phone",
324
+ "host",
325
+ ];
326
+
327
+ test("exactly printer, camera, phone and host are not walkable", () => {
328
+ const notWalkable: Array<string> = DEFAULT_NETWORK_DEVICE_ROLES.filter(
329
+ (role: DefaultNetworkDeviceRole): boolean => {
330
+ return !role.isSnmpWalkable;
331
+ },
332
+ ).map((role: DefaultNetworkDeviceRole): string => {
333
+ return role.key;
334
+ });
335
+
336
+ expect(notWalkable).toEqual(MONITOR_BACKED_KEYS);
337
+ });
338
+
339
+ test("every other default is walkable, and the flag is always a real boolean", () => {
340
+ /*
341
+ * buildNeighborAdoptionDraft branches on
342
+ * `isSnmpWalkableRole !== undefined`, so an absent value falls back to the
343
+ * built-in set rather than being read as "not walkable".
344
+ */
345
+ for (const role of DEFAULT_NETWORK_DEVICE_ROLES) {
346
+ expect(typeof role.isSnmpWalkable).toBe("boolean");
347
+ expect(role.isSnmpWalkable).toBe(!MONITOR_BACKED_KEYS.includes(role.key));
348
+ }
349
+ });
350
+
351
+ test("the four non-walkable roles are exactly the four drawn as a leaf rect", () => {
352
+ /*
353
+ * Not a coincidence worth breaking: "something plugged into an access
354
+ * port" is the same set of boxes as "nothing worth walking". If the two
355
+ * ever diverge, one of the two lists was edited without the other.
356
+ */
357
+ const rectKeys: Array<string> = DEFAULT_NETWORK_DEVICE_ROLES.filter(
358
+ (role: DefaultNetworkDeviceRole): boolean => {
359
+ return role.topologyShape === "rect";
360
+ },
361
+ ).map((role: DefaultNetworkDeviceRole): string => {
362
+ return role.key;
363
+ });
364
+
365
+ expect(rectKeys).toEqual(MONITOR_BACKED_KEYS);
366
+ });
367
+ });
368
+
369
+ describe("DEFAULT_NETWORK_DEVICE_ROLES — the rows are storable as they stand", () => {
370
+ test("keys are unique", () => {
371
+ // The column is unique per project; a duplicate would fail the seed.
372
+ const keys: Array<string> = DEFAULT_NETWORK_DEVICE_ROLES.map(
373
+ (role: DefaultNetworkDeviceRole): string => {
374
+ return role.key;
375
+ },
376
+ );
377
+
378
+ expect(new Set<string>(keys).size).toBe(keys.length);
379
+ });
380
+
381
+ test("names are unique", () => {
382
+ /*
383
+ * Also a unique column, and the seeder skips a default whose NAME already
384
+ * exists — two defaults sharing a name would silently seed only one.
385
+ */
386
+ const names: Array<string> = DEFAULT_NETWORK_DEVICE_ROLES.map(
387
+ (role: DefaultNetworkDeviceRole): string => {
388
+ return role.name;
389
+ },
390
+ );
391
+
392
+ expect(new Set<string>(names).size).toBe(names.length);
393
+ });
394
+
395
+ test("every default has a name and a description a human can read", () => {
396
+ /*
397
+ * The description is the only explanation the settings page offers for
398
+ * what a role means, and a blank one there reads as a broken page.
399
+ */
400
+ for (const role of DEFAULT_NETWORK_DEVICE_ROLES) {
401
+ expect(role.name.trim()).not.toBe("");
402
+ expect(role.name).toBe(role.name.trim());
403
+ expect(role.description.trim()).not.toBe("");
404
+ expect(role.description.length).toBeGreaterThan(10);
405
+ }
406
+ });
407
+
408
+ test("descriptions are distinct, so no two roles explain themselves the same way", () => {
409
+ const descriptions: Array<string> = DEFAULT_NETWORK_DEVICE_ROLES.map(
410
+ (role: DefaultNetworkDeviceRole): string => {
411
+ return role.description;
412
+ },
413
+ );
414
+
415
+ expect(new Set<string>(descriptions).size).toBe(descriptions.length);
416
+ });
417
+ });