@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,672 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
4
+ import { getMetadataArgsStorage } from "typeorm";
5
+ import { RelationMetadataArgs } from "typeorm/metadata-args/RelationMetadataArgs";
6
+ import AllModelTypes from "../../../Models/DatabaseModels/Index";
7
+ import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
8
+ import NetworkDevice from "../../../Models/DatabaseModels/NetworkDevice";
9
+ import NetworkDeviceRole from "../../../Models/DatabaseModels/NetworkDeviceRole";
10
+ import Project from "../../../Models/DatabaseModels/Project";
11
+ import NetworkDeviceRoleService from "../../../Server/Services/NetworkDeviceRoleService";
12
+ import ProjectService from "../../../Server/Services/ProjectService";
13
+ import Services from "../../../Server/Services/Index";
14
+ import CreateBy from "../../../Server/Types/Database/CreateBy";
15
+ import { ColumnAccessControl } from "../../../Types/BaseDatabase/AccessControl";
16
+ import { TableColumnMetadata } from "../../../Types/Database/TableColumn";
17
+ import TableColumnType from "../../../Types/Database/TableColumnType";
18
+ import { getUniqueColumnBy } from "../../../Types/Database/UniqueColumnBy";
19
+ import GenericFunction from "../../../Types/GenericFunction";
20
+ import DEFAULT_NETWORK_DEVICE_ROLES, {
21
+ DefaultNetworkDeviceRole,
22
+ } from "../../../Types/NetworkDevice/DefaultNetworkDeviceRole";
23
+ import ObjectID from "../../../Types/ObjectID";
24
+ import Permission, {
25
+ PermissionGroup,
26
+ PermissionHelper,
27
+ PermissionProps,
28
+ } from "../../../Types/Permission";
29
+
30
+ /*
31
+ * NetworkDeviceRole: the contracts that live in metadata and in wiring rather
32
+ * than in code, and which therefore no functional test would notice losing.
33
+ *
34
+ * Device roles used to be a fixed TypeScript union with the label, the
35
+ * topology silhouette and the "is this a core device?" flag hardcoded in three
36
+ * different modules. They are now a per-project table an operator edits, and
37
+ * two properties of that table are what keep the rest of the feature standing:
38
+ *
39
+ * 1. THE KEY IS SERVER-OWNED AND IMMUTABLE. The SNMP classifier is
40
+ * evidence-driven: it answers in the built-in vocabulary ("router",
41
+ * "wirelessAccessPoint") and can never invent a project's custom role. The
42
+ * key is what its answer is matched back to, so it has to survive a
43
+ * rename. Grant anybody update on it and renaming "Wireless AP" to
44
+ * "Access Point" — or an over-helpful form that submits every field —
45
+ * silently re-points, or unpoints, every access point already classified.
46
+ *
47
+ * 2. EVERY PROJECT GETS THE SAME SEEDED SET. Both the project-creation hook
48
+ * and the backfill for projects that predate the table seed from the one
49
+ * DEFAULT_NETWORK_DEVICE_ROLES list, through the same public method, so a
50
+ * project created yesterday and one created today cannot disagree about
51
+ * what "Router" means.
52
+ *
53
+ * Nothing here touches a database: the model is instantiated and read for its
54
+ * decorator metadata, the seeder is run against spied services, and the two
55
+ * pieces of wiring that live in another workspace are read off the source.
56
+ */
57
+
58
+ const PROJECT_ID: ObjectID = new ObjectID(
59
+ "22222222-2222-4222-8222-222222222222",
60
+ );
61
+
62
+ const PROJECT_SERVICE_SOURCE: string = fs.readFileSync(
63
+ path.join(__dirname, "../../../Server/Services/ProjectService.ts"),
64
+ "utf8",
65
+ );
66
+
67
+ /*
68
+ * The API router and the backfill both live in the App workspace, which this
69
+ * suite cannot import. They are read as text — the same thing the other
70
+ * wiring tests in this directory do — so the assertion is on the registration
71
+ * itself rather than on a re-export somebody could add to make it pass.
72
+ */
73
+ const BASE_API_INDEX_SOURCE: string = fs.readFileSync(
74
+ path.join(__dirname, "../../../../App/FeatureSet/BaseAPI/Index.ts"),
75
+ "utf8",
76
+ );
77
+
78
+ const BACKFILL_SOURCE: string = fs.readFileSync(
79
+ path.join(
80
+ __dirname,
81
+ "../../../../App/FeatureSet/Workers/DataMigrations/BackfillNetworkDeviceRoles.ts",
82
+ ),
83
+ "utf8",
84
+ );
85
+
86
+ function relationArgs(
87
+ target: GenericFunction,
88
+ propertyName: string,
89
+ ): RelationMetadataArgs | undefined {
90
+ return getMetadataArgsStorage().relations.find(
91
+ (relation: RelationMetadataArgs): boolean => {
92
+ return (
93
+ relation.target === target && relation.propertyName === propertyName
94
+ );
95
+ },
96
+ );
97
+ }
98
+
99
+ function accessControlFor(
100
+ model: BaseModel,
101
+ columnName: string,
102
+ ): ColumnAccessControl {
103
+ const accessControl: ColumnAccessControl | null =
104
+ model.getColumnAccessControlFor(columnName);
105
+
106
+ expect(accessControl).not.toBeNull();
107
+
108
+ return accessControl as ColumnAccessControl;
109
+ }
110
+
111
+ describe("NetworkDeviceRole is a project-scoped, API-exposed lookup table", () => {
112
+ const role: NetworkDeviceRole = new NetworkDeviceRole();
113
+
114
+ /*
115
+ * The tenant column is what makes every read, write and permission check
116
+ * project-scoped by default. Without it a role table is global, and one
117
+ * project's custom roles show up in another's picker.
118
+ */
119
+ test("it is scoped to a project by projectId", () => {
120
+ expect(role.getTenantColumn()).toBe("projectId");
121
+ });
122
+
123
+ test("it is served at /network-device-role", () => {
124
+ expect(role.getCrudApiPath()?.toString()).toBe("/network-device-role");
125
+ });
126
+
127
+ /*
128
+ * A model that is not in AllModelTypes gets no table created for it and no
129
+ * permission catalogue coverage — the settings page would 500 on first
130
+ * load.
131
+ */
132
+ test("the model is registered", () => {
133
+ expect(AllModelTypes).toContain(NetworkDeviceRole);
134
+ });
135
+
136
+ test("the service is registered", () => {
137
+ expect(Services).toContain(NetworkDeviceRoleService);
138
+ });
139
+
140
+ /*
141
+ * ...and the CRUD router is mounted. The settings page is entirely
142
+ * model-backed — it does nothing but list, create and edit through this
143
+ * endpoint — so an unmounted router is a page that 404s on every action.
144
+ */
145
+ test("the CRUD router is mounted in the API", () => {
146
+ expect(BASE_API_INDEX_SOURCE).toContain(
147
+ 'import NetworkDeviceRole from "Common/Models/DatabaseModels/NetworkDeviceRole";',
148
+ );
149
+ expect(BASE_API_INDEX_SOURCE).toContain(
150
+ "new BaseAPI<NetworkDeviceRole, NetworkDeviceRoleServiceType>(",
151
+ );
152
+ expect(BASE_API_INDEX_SOURCE).toContain("NetworkDeviceRoleService,");
153
+ });
154
+ });
155
+
156
+ describe("the key column is server-derived and immutable", () => {
157
+ const role: NetworkDeviceRole = new NetworkDeviceRole();
158
+
159
+ /*
160
+ * THE INVARIANT THIS FILE EXISTS FOR.
161
+ *
162
+ * An operator names a role; they never name its identifier. The key is
163
+ * derived once, by the service, from the name at create time — and it is
164
+ * what the SNMP classifier's answer and every stored topology payload are
165
+ * matched against. Empty create and empty update mean the API cannot set it
166
+ * on the way in or move it afterwards, so classification keeps matching
167
+ * across a rename. Granting update to even ProjectOwner would be enough:
168
+ * the settings form posts the whole row.
169
+ */
170
+ test("nobody, at any permission level, may create or update the key", () => {
171
+ const accessControl: ColumnAccessControl = accessControlFor(role, "key");
172
+
173
+ expect(accessControl.create).toEqual([]);
174
+ expect(accessControl.update).toEqual([]);
175
+ });
176
+
177
+ /*
178
+ * Read is a different matter and must stay open: the topology API selects
179
+ * networkDeviceRole.key on every map load to stamp nodes with their role,
180
+ * and a Viewer looking at the map is the least-privileged caller that does
181
+ * it.
182
+ */
183
+ test("but it stays readable, because the topology map matches on it", () => {
184
+ const accessControl: ColumnAccessControl = accessControlFor(role, "key");
185
+
186
+ expect(accessControl.read).toContain(Permission.ReadNetworkDeviceRole);
187
+ expect(accessControl.read).toContain(Permission.Viewer);
188
+ });
189
+
190
+ test("it is selectable through the device relation, which is how the map reads it", () => {
191
+ const metadata: TableColumnMetadata = role.getTableColumnMetadata("key");
192
+
193
+ expect(metadata.canReadOnRelationQuery).toBe(true);
194
+ });
195
+
196
+ /*
197
+ * The contrast is the point. The name is the label and is meant to be
198
+ * edited; the key is the identity and is not. If a future edit ever makes
199
+ * the two look alike, one of these two assertions fails.
200
+ */
201
+ test("the name, by contrast, is editable — label and identity are separate columns", () => {
202
+ const accessControl: ColumnAccessControl = accessControlFor(role, "name");
203
+
204
+ expect(accessControl.update).toContain(Permission.EditNetworkDeviceRole);
205
+ expect(accessControl.update.length).toBeGreaterThan(0);
206
+ });
207
+ });
208
+
209
+ describe("a role's name is unique within its project, not globally", () => {
210
+ const role: NetworkDeviceRole = new NetworkDeviceRole();
211
+
212
+ /*
213
+ * Global uniqueness would mean the first project to create "Router" stops
214
+ * every other project from having one. Scoping to projectId is also what
215
+ * the seeder's idempotency guard leans on.
216
+ */
217
+ test("name is unique per project", () => {
218
+ expect(getUniqueColumnBy(role, "name")).toBe("projectId");
219
+ });
220
+
221
+ /*
222
+ * The key is scoped the same way and for a stronger reason: two rows in one
223
+ * project sharing a key would make the classifier's lookup ambiguous, so a
224
+ * device classified as a switch could land on either row.
225
+ */
226
+ test("key is unique per project too, so classification never has two answers", () => {
227
+ expect(getUniqueColumnBy(role, "key")).toBe("projectId");
228
+ });
229
+ });
230
+
231
+ describe("NetworkDevice points at a role without depending on one", () => {
232
+ const device: NetworkDevice = new NetworkDevice();
233
+
234
+ test("it has a networkDeviceRoleId column and the relation behind it", () => {
235
+ const idMetadata: TableColumnMetadata = device.getTableColumnMetadata(
236
+ "networkDeviceRoleId",
237
+ );
238
+
239
+ expect(idMetadata).toBeDefined();
240
+ expect(idMetadata.type).toBe(TableColumnType.ObjectID);
241
+ expect(idMetadata.required).toBe(false);
242
+
243
+ const relationMetadata: TableColumnMetadata =
244
+ device.getTableColumnMetadata("networkDeviceRole");
245
+
246
+ expect(relationMetadata.type).toBe(TableColumnType.Entity);
247
+ expect(relationMetadata.modelType).toBe(NetworkDeviceRole);
248
+ expect(relationMetadata.manyToOneRelationColumn).toBe(
249
+ "networkDeviceRoleId",
250
+ );
251
+ });
252
+
253
+ /*
254
+ * Configuration must never delete inventory: removing a role from the
255
+ * settings page puts its devices back to being classified from their own
256
+ * SNMP identity, which is exactly what an unset role has always meant. A
257
+ * CASCADE here — the action every other NetworkDevice relation uses — would
258
+ * mean deleting "Printer" deletes the printers.
259
+ */
260
+ test("deleting the role detaches the devices rather than deleting them", () => {
261
+ expect(
262
+ relationArgs(NetworkDevice, "networkDeviceRole")?.options.onDelete,
263
+ ).toBe("SET NULL");
264
+ });
265
+
266
+ /*
267
+ * The deprecated inline string is retained on purpose — the backfill reads
268
+ * it to point each device at its new role row — but it has to be labelled
269
+ * loudly enough that nobody writes new code against it, because the plan is
270
+ * to drop it once the backfill has run everywhere.
271
+ */
272
+ test("the old deviceRole string survives, marked deprecated for the backfill", () => {
273
+ const metadata: TableColumnMetadata =
274
+ device.getTableColumnMetadata("deviceRole");
275
+
276
+ expect(metadata).toBeDefined();
277
+ expect(metadata.title).toContain("Deprecated");
278
+ expect(metadata.description?.toLowerCase()).toContain("deprecated");
279
+ });
280
+ });
281
+
282
+ describe("the seeded defaults", () => {
283
+ /*
284
+ * The seeder's idempotency guard is a lookup by name and by key, so a
285
+ * duplicate in this list would make it seed one row and then skip the
286
+ * other — silently shipping a project one role short.
287
+ */
288
+ test("carry no duplicate names or keys", () => {
289
+ const names: Array<string> = DEFAULT_NETWORK_DEVICE_ROLES.map(
290
+ (role: DefaultNetworkDeviceRole): string => {
291
+ return role.name;
292
+ },
293
+ );
294
+ const keys: Array<string> = DEFAULT_NETWORK_DEVICE_ROLES.map(
295
+ (role: DefaultNetworkDeviceRole): string => {
296
+ return role.key;
297
+ },
298
+ );
299
+
300
+ expect(new Set(names).size).toBe(names.length);
301
+ expect(new Set(keys).size).toBe(keys.length);
302
+ });
303
+
304
+ /*
305
+ * "unknown" is the classifier saying it has no answer, not a role an
306
+ * operator assigns. Seeding it would put "Unknown" in the role picker,
307
+ * where choosing it would mean "stop classifying this device forever"
308
+ * rather than "I don't know yet".
309
+ */
310
+ test("do not include the classifier's own no-answer value", () => {
311
+ expect(
312
+ DEFAULT_NETWORK_DEVICE_ROLES.map(
313
+ (role: DefaultNetworkDeviceRole): string => {
314
+ return role.key;
315
+ },
316
+ ),
317
+ ).not.toContain("unknown");
318
+ });
319
+ });
320
+
321
+ describe("every project is seeded with the default device roles", () => {
322
+ let createSpy: jest.SpyInstance;
323
+
324
+ beforeEach(() => {
325
+ /*
326
+ * getExistingProjectScopedNames reads the project's roles through
327
+ * findBy; getKeysInProject is the second half of the idempotency guard.
328
+ * Both are stubbed empty here so this describe block sees a brand new
329
+ * project, and overridden per-test where the guard itself is the subject.
330
+ */
331
+ jest.spyOn(NetworkDeviceRoleService, "findBy").mockResolvedValue([]);
332
+ jest
333
+ .spyOn(NetworkDeviceRoleService, "getKeysInProject")
334
+ .mockResolvedValue(new Set<string>());
335
+
336
+ createSpy = jest
337
+ .spyOn(NetworkDeviceRoleService, "create")
338
+ .mockImplementation(
339
+ async (
340
+ createBy: CreateBy<NetworkDeviceRole>,
341
+ ): Promise<NetworkDeviceRole> => {
342
+ return createBy.data;
343
+ },
344
+ );
345
+ });
346
+
347
+ afterEach(() => {
348
+ jest.restoreAllMocks();
349
+ });
350
+
351
+ function newProject(): Project {
352
+ const project: Project = new Project();
353
+ project._id = PROJECT_ID.toString();
354
+
355
+ return project;
356
+ }
357
+
358
+ function seededRoles(): Array<NetworkDeviceRole> {
359
+ return createSpy.mock.calls.map(
360
+ (call: Array<unknown>): NetworkDeviceRole => {
361
+ return (call[0] as CreateBy<NetworkDeviceRole>).data;
362
+ },
363
+ );
364
+ }
365
+
366
+ /*
367
+ * Calling this method from a test at all is the assertion that it is
368
+ * public: it is `await`ed from BackfillNetworkDeviceRoles in another
369
+ * workspace, and making it private would fail ts-jest's compile here as
370
+ * well as breaking that migration.
371
+ */
372
+ test("the seeder is public, and the backfill for older projects calls it", async () => {
373
+ await ProjectService.addDefaultNetworkDeviceRoles(newProject());
374
+
375
+ expect(createSpy).toHaveBeenCalled();
376
+ expect(BACKFILL_SOURCE).toContain(
377
+ "ProjectService.addDefaultNetworkDeviceRoles(project)",
378
+ );
379
+ });
380
+
381
+ /*
382
+ * The role seeder runs in the same parallel batch as the site-type seeder
383
+ * on the project-creation hook. Alongside, not after: a project whose roles
384
+ * are seeded by some later job would render its first topology map with no
385
+ * roles at all. This is read off the source because the hook itself is a
386
+ * private method behind a full project create.
387
+ */
388
+ test("it runs from the project-creation hook, next to the site-type seeder", () => {
389
+ const start: number = PROJECT_SERVICE_SOURCE.indexOf("await Promise.all([");
390
+
391
+ expect(start).toBeGreaterThan(-1);
392
+
393
+ const promiseAllBlock: string = PROJECT_SERVICE_SOURCE.slice(
394
+ start,
395
+ PROJECT_SERVICE_SOURCE.indexOf("]);", start),
396
+ );
397
+
398
+ expect(promiseAllBlock).toContain(
399
+ "this.addDefaultNetworkSiteTypes(createdItem)",
400
+ );
401
+ expect(promiseAllBlock).toContain(
402
+ "this.addDefaultNetworkDeviceRoles(createdItem)",
403
+ );
404
+ });
405
+
406
+ test("it seeds one role per default, named and ordered as the list is", async () => {
407
+ await ProjectService.addDefaultNetworkDeviceRoles(newProject());
408
+
409
+ const seeded: Array<NetworkDeviceRole> = seededRoles();
410
+
411
+ expect(seeded.length).toBe(DEFAULT_NETWORK_DEVICE_ROLES.length);
412
+ expect(
413
+ seeded.map((role: NetworkDeviceRole): string | undefined => {
414
+ return role.name;
415
+ }),
416
+ ).toEqual(
417
+ DEFAULT_NETWORK_DEVICE_ROLES.map(
418
+ (role: DefaultNetworkDeviceRole): string => {
419
+ return role.name;
420
+ },
421
+ ),
422
+ );
423
+ });
424
+
425
+ /*
426
+ * order is the array index PLUS ONE, and the +1 is load-bearing: order is
427
+ * an optional numeric column, and a leading zero sorts and reads as "unset"
428
+ * everywhere a falsy check is made. Starting at 1 also lines the settings
429
+ * page up with the map legend, which is why the defaults are listed in
430
+ * legend order in the first place.
431
+ */
432
+ test("it numbers the roles from 1, matching the map legend's order", async () => {
433
+ await ProjectService.addDefaultNetworkDeviceRoles(newProject());
434
+
435
+ expect(
436
+ seededRoles().map((role: NetworkDeviceRole): number | undefined => {
437
+ return role.order;
438
+ }),
439
+ ).toEqual(
440
+ DEFAULT_NETWORK_DEVICE_ROLES.map(
441
+ (_role: DefaultNetworkDeviceRole, index: number): number => {
442
+ return index + 1;
443
+ },
444
+ ),
445
+ );
446
+ });
447
+
448
+ /*
449
+ * The key is passed EXPLICITLY rather than left for the service to derive.
450
+ * "Wireless AP" is the case that proves why: no derivation from that name
451
+ * produces "wirelessAccessPoint", which is the value the SNMP classifier
452
+ * actually emits — so a seeded AP would match no row and every access point
453
+ * on the map would fall back to unclassified.
454
+ */
455
+ test("it passes the classifier's own key rather than letting one be derived", async () => {
456
+ await ProjectService.addDefaultNetworkDeviceRoles(newProject());
457
+
458
+ const seeded: Array<NetworkDeviceRole> = seededRoles();
459
+
460
+ expect(
461
+ seeded.map((role: NetworkDeviceRole): string | undefined => {
462
+ return role.key;
463
+ }),
464
+ ).toEqual(
465
+ DEFAULT_NETWORK_DEVICE_ROLES.map(
466
+ (role: DefaultNetworkDeviceRole): string => {
467
+ return role.key;
468
+ },
469
+ ),
470
+ );
471
+
472
+ const wirelessAccessPoint: NetworkDeviceRole | undefined = seeded.find(
473
+ (role: NetworkDeviceRole): boolean => {
474
+ return role.key === "wirelessAccessPoint";
475
+ },
476
+ );
477
+
478
+ expect(wirelessAccessPoint?.name).toBe("Wireless AP");
479
+ });
480
+
481
+ /*
482
+ * The shape and the two flags are the whole of what used to be hardcoded in
483
+ * the renderer and the layout module. A seeded project has to draw exactly
484
+ * the map it drew before roles were configurable, which means these three
485
+ * come across untouched.
486
+ */
487
+ test("it carries the shape and both behaviour flags across unchanged", async () => {
488
+ await ProjectService.addDefaultNetworkDeviceRoles(newProject());
489
+
490
+ const seeded: Array<NetworkDeviceRole> = seededRoles();
491
+
492
+ for (let index: number = 0; index < seeded.length; index++) {
493
+ const expectedRole: DefaultNetworkDeviceRole =
494
+ DEFAULT_NETWORK_DEVICE_ROLES[index]!;
495
+ const seededRole: NetworkDeviceRole = seeded[index]!;
496
+
497
+ expect(seededRole.topologyShape).toBe(expectedRole.topologyShape);
498
+ expect(seededRole.isCoreLayer).toBe(expectedRole.isCoreLayer);
499
+ expect(seededRole.isSnmpWalkable).toBe(expectedRole.isSnmpWalkable);
500
+ }
501
+ });
502
+
503
+ test("every seeded role belongs to the project being created, and is written as root", async () => {
504
+ await ProjectService.addDefaultNetworkDeviceRoles(newProject());
505
+
506
+ for (const call of createSpy.mock.calls as Array<Array<unknown>>) {
507
+ const createBy: CreateBy<NetworkDeviceRole> =
508
+ call[0] as CreateBy<NetworkDeviceRole>;
509
+
510
+ expect(createBy.data.projectId?.toString()).toBe(PROJECT_ID.toString());
511
+ /*
512
+ * Root, because this runs inside the project-creation hook: the owner
513
+ * does not have permissions on a project that does not exist yet.
514
+ */
515
+ expect(createBy.props.isRoot).toBe(true);
516
+ }
517
+ });
518
+
519
+ /*
520
+ * The backfill re-runs the seeder against projects that may already have
521
+ * been seeded, so re-running it must be a no-op rather than a second copy of
522
+ * every role.
523
+ */
524
+ test("re-running it against an already-seeded project creates nothing", async () => {
525
+ jest.spyOn(NetworkDeviceRoleService, "getKeysInProject").mockResolvedValue(
526
+ new Set<string>(
527
+ DEFAULT_NETWORK_DEVICE_ROLES.map(
528
+ (role: DefaultNetworkDeviceRole): string => {
529
+ return role.key;
530
+ },
531
+ ),
532
+ ),
533
+ );
534
+
535
+ await ProjectService.addDefaultNetworkDeviceRoles(newProject());
536
+
537
+ expect(createSpy).not.toHaveBeenCalled();
538
+ });
539
+
540
+ /*
541
+ * ...including when the project has RENAMED a seeded role. The name guard
542
+ * alone would miss this and hand the project a second "Router" beside its
543
+ * "Edge Router"; the key is the identity that survives the rename, which is
544
+ * why the guard checks both.
545
+ */
546
+ test("a renamed role is still recognised, by its key rather than its name", async () => {
547
+ jest
548
+ .spyOn(NetworkDeviceRoleService, "getKeysInProject")
549
+ .mockResolvedValue(new Set<string>(["router"]));
550
+
551
+ await ProjectService.addDefaultNetworkDeviceRoles(newProject());
552
+
553
+ const seeded: Array<NetworkDeviceRole> = seededRoles();
554
+
555
+ expect(seeded.length).toBe(DEFAULT_NETWORK_DEVICE_ROLES.length - 1);
556
+ expect(
557
+ seeded.map((role: NetworkDeviceRole): string | undefined => {
558
+ return role.key;
559
+ }),
560
+ ).not.toContain("router");
561
+ });
562
+
563
+ /*
564
+ * The other half of the same guard: a project that already holds a role by
565
+ * name — seeded before the key column carried anything useful, say — does
566
+ * not get a duplicate either.
567
+ */
568
+ test("a role that already exists by name is not seeded twice", async () => {
569
+ const existing: NetworkDeviceRole = new NetworkDeviceRole();
570
+ existing.name = "Router";
571
+
572
+ jest
573
+ .spyOn(NetworkDeviceRoleService, "findBy")
574
+ .mockResolvedValue([existing]);
575
+
576
+ await ProjectService.addDefaultNetworkDeviceRoles(newProject());
577
+
578
+ expect(seededRoles().length).toBe(DEFAULT_NETWORK_DEVICE_ROLES.length - 1);
579
+ });
580
+ });
581
+
582
+ describe("the four device-role permissions", () => {
583
+ interface RolePermissionUnderTest {
584
+ name: string;
585
+ permission: Permission;
586
+ }
587
+
588
+ /*
589
+ * The names are written out rather than derived from the enum, because the
590
+ * stored value is the half that matters: a team's permission row persists
591
+ * the string, so a value that ever drifts from its member name orphans
592
+ * every grant already handed out.
593
+ */
594
+ const ROLE_PERMISSIONS: Array<RolePermissionUnderTest> = [
595
+ {
596
+ name: "CreateNetworkDeviceRole",
597
+ permission: Permission.CreateNetworkDeviceRole,
598
+ },
599
+ {
600
+ name: "ReadNetworkDeviceRole",
601
+ permission: Permission.ReadNetworkDeviceRole,
602
+ },
603
+ {
604
+ name: "EditNetworkDeviceRole",
605
+ permission: Permission.EditNetworkDeviceRole,
606
+ },
607
+ {
608
+ name: "DeleteNetworkDeviceRole",
609
+ permission: Permission.DeleteNetworkDeviceRole,
610
+ },
611
+ ];
612
+
613
+ /*
614
+ * A permission the enum declares but the catalogue does not describe cannot
615
+ * be granted: the team-permission editor renders from getAllPermissionProps,
616
+ * so a missing descriptor means the permission exists, gates the model, and
617
+ * can never be handed to anybody.
618
+ */
619
+ test.each(ROLE_PERMISSIONS)(
620
+ "$name has a catalogue entry in the Monitor group",
621
+ (entry: RolePermissionUnderTest) => {
622
+ const descriptor: PermissionProps | undefined =
623
+ PermissionHelper.getAllPermissionProps().find(
624
+ (props: PermissionProps): boolean => {
625
+ return props.permission === entry.permission;
626
+ },
627
+ );
628
+
629
+ expect(descriptor).toBeDefined();
630
+ /*
631
+ * Monitor, not Settings: device roles are configured under Network, and
632
+ * the group is what decides which section of the permission editor they
633
+ * appear in.
634
+ */
635
+ expect(descriptor?.group).toBe(PermissionGroup.Monitor);
636
+ /*
637
+ * ...and assignable to a project team, which is the only way a
638
+ * non-owner ever gets one.
639
+ */
640
+ expect(descriptor?.isAssignableToTenant).toBe(true);
641
+ },
642
+ );
643
+
644
+ test.each(ROLE_PERMISSIONS)(
645
+ "$name is stored under exactly its own name",
646
+ (entry: RolePermissionUnderTest) => {
647
+ expect(entry.permission.toString()).toBe(entry.name);
648
+ },
649
+ );
650
+
651
+ /*
652
+ * The table's own access control has to accept the granular permissions, or
653
+ * granting "Create Network Device Role" to a team would grant nothing at
654
+ * all — the catalogue entry would exist and the gate would still refuse.
655
+ */
656
+ test("the model's table access control honours each of them", () => {
657
+ const role: NetworkDeviceRole = new NetworkDeviceRole();
658
+
659
+ expect(role.createRecordPermissions).toContain(
660
+ Permission.CreateNetworkDeviceRole,
661
+ );
662
+ expect(role.readRecordPermissions).toContain(
663
+ Permission.ReadNetworkDeviceRole,
664
+ );
665
+ expect(role.updateRecordPermissions).toContain(
666
+ Permission.EditNetworkDeviceRole,
667
+ );
668
+ expect(role.deleteRecordPermissions).toContain(
669
+ Permission.DeleteNetworkDeviceRole,
670
+ );
671
+ });
672
+ });
@@ -20,7 +20,7 @@ import { describe, expect, it, afterEach } from "@jest/globals";
20
20
  * the write-time validation in front of them.
21
21
  *
22
22
  * The pattern criteria are documented as case-insensitive regexes, but the
23
- * assignment rules right next to them in Network > Automation take '*'
23
+ * assignment rules right next to them in Network > Rules take '*'
24
24
  * wildcard globs. A rule written with the glob syntax - `*0664*` - used to
25
25
  * throw inside `new RegExp`, get swallowed, and silently label nothing
26
26
  * (OneUptime/oneuptime#2940). Both engines now accept either syntax, and a