@oneuptime/common 12.0.14 → 12.0.15

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 (195) hide show
  1. package/Models/DatabaseModels/GlobalOidc.ts +20 -0
  2. package/Models/DatabaseModels/GlobalSso.ts +20 -0
  3. package/Models/DatabaseModels/MarketingConversion.ts +19 -9
  4. package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +46 -0
  5. package/Models/DatabaseModels/StatusPage.ts +7 -4
  6. package/Server/API/StatusPageAPI.ts +29 -4
  7. package/Server/EnvironmentConfig.ts +7 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.ts +19 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.ts +40 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  11. package/Server/Middleware/SlackAuthorization.ts +43 -6
  12. package/Server/Middleware/UserAuthorization.ts +236 -26
  13. package/Server/Middleware/WhatsAppAuthorization.ts +43 -3
  14. package/Server/Services/GlobalOidcProjectService.ts +126 -1
  15. package/Server/Services/GlobalOidcService.ts +132 -0
  16. package/Server/Services/GlobalSsoProjectService.ts +126 -1
  17. package/Server/Services/GlobalSsoService.ts +132 -0
  18. package/Server/Services/UserEmailService.ts +4 -4
  19. package/Server/Utils/CronTab.ts +73 -0
  20. package/Server/Utils/GlobalSsoAuthorization.ts +175 -0
  21. package/Server/Utils/Marketing/ConversionUploadProvider.ts +35 -5
  22. package/Server/Utils/Marketing/Providers/GoogleAds.ts +1 -1
  23. package/Server/Utils/Marketing/Providers/LinkedIn.ts +1 -1
  24. package/Server/Utils/Marketing/Providers/Meta.ts +1 -1
  25. package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +1 -1
  26. package/Server/Utils/Marketing/Providers/Reddit.ts +1 -1
  27. package/Server/Utils/Monitor/Criteria/APIRequestCriteria.ts +48 -33
  28. package/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.ts +35 -30
  29. package/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.ts +33 -28
  30. package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +475 -47
  31. package/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.ts +35 -30
  32. package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +34 -28
  33. package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +33 -28
  34. package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +69 -40
  35. package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +35 -29
  36. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +8 -0
  37. package/Server/Utils/Monitor/MonitorResource.ts +161 -21
  38. package/Server/Utils/StatusPageContentSecurityPolicy.ts +56 -0
  39. package/Server/Utils/StatusPageCustomizationAccess.ts +117 -0
  40. package/Tests/Server/API/StatusPageCustomizationOrigin.test.ts +348 -0
  41. package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +9 -59
  42. package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +227 -0
  43. package/Tests/Server/Middleware/SlackAuthorization.test.ts +149 -0
  44. package/Tests/Server/Middleware/UserAuthorization.test.ts +27 -13
  45. package/Tests/Server/Middleware/UserAuthorizationGlobalSsoGovernance.test.ts +1455 -0
  46. package/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts +91 -2
  47. package/Tests/Server/Middleware/WhatsAppAuthorization.test.ts +233 -0
  48. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +21 -8
  49. package/Tests/Server/Services/GlobalSsoProviderAuthorization.test.ts +1242 -0
  50. package/Tests/Server/Services/MonitorResourceProbeAgreementHydration.test.ts +459 -0
  51. package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +10 -0
  52. package/Tests/Server/Types/AnalyticsDatabase/AggregateUtilBucketTimestamp.test.ts +416 -0
  53. package/Tests/Server/Types/Database/QueryHelperManyToManyAndEmptyValues.test.ts +551 -0
  54. package/Tests/Server/Types/Database/SelectUtil.test.ts +348 -0
  55. package/Tests/Server/Utils/CronTab.test.ts +116 -0
  56. package/Tests/Server/Utils/GlobalSSOToken.test.ts +581 -0
  57. package/Tests/Server/Utils/GlobalSsoAuthorization.test.ts +1401 -0
  58. package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +254 -0
  59. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +1 -1
  60. package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaEvaluateOverTime.test.ts +508 -0
  61. package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaPortTimings.test.ts +30 -9
  62. package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +987 -0
  63. package/Tests/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.test.ts +916 -0
  64. package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +382 -0
  65. package/Tests/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.test.ts +1219 -0
  66. package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +86 -0
  67. package/Tests/Server/Utils/StatusPageContentSecurityPolicy.test.ts +136 -0
  68. package/Tests/Server/Utils/StatusPageCustomizationAccess.test.ts +223 -0
  69. package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +165 -0
  70. package/Tests/Types/NetworkDevice/NetworkDeviceLinkRuleScope.test.ts +215 -0
  71. package/Tests/UI/Components/Forms/FormSubmitAnalytics.test.tsx +206 -0
  72. package/Tests/UI/Utils/AIChatExport/ConversationMarkdown.test.ts +774 -0
  73. package/Tests/UI/Utils/AIChatExport/MarkdownBlocks.test.ts +791 -0
  74. package/Tests/UI/Utils/AIChatExport/MarkdownSafety.test.ts +544 -0
  75. package/Tests/Utils/Analytics.test.ts +187 -0
  76. package/Tests/Utils/Monitor/MonitorMetricType.test.ts +80 -0
  77. package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +1291 -0
  78. package/Tests/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.test.ts +214 -0
  79. package/Types/Marketing/MarketingConversion.ts +12 -0
  80. package/Types/Monitor/CriteriaFilter.ts +9 -0
  81. package/Types/NetworkDevice/NetworkDeviceLinkRuleScope.ts +51 -0
  82. package/UI/Components/FormModal/BasicFormModal.tsx +12 -0
  83. package/UI/Components/Forms/BasicForm.tsx +13 -1
  84. package/UI/Components/Forms/BasicModelForm.tsx +2 -1
  85. package/UI/Components/Forms/ModelForm.tsx +10 -1
  86. package/UI/Components/Forms/Utils/FormAnalyticsName.ts +39 -0
  87. package/UI/Components/ModelFormModal/ModelFormModal.tsx +6 -1
  88. package/Utils/Analytics.ts +55 -2
  89. package/Utils/Monitor/MonitorMetricType.ts +40 -0
  90. package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +587 -37
  91. package/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.ts +115 -0
  92. package/build/dist/Models/DatabaseModels/GlobalOidc.js +21 -0
  93. package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/GlobalSso.js +21 -0
  95. package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/MarketingConversion.js +20 -10
  97. package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +47 -0
  99. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -1
  100. package/build/dist/Models/DatabaseModels/StatusPage.js +4 -4
  101. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  102. package/build/dist/Server/API/StatusPageAPI.js +25 -4
  103. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  104. package/build/dist/Server/EnvironmentConfig.js +6 -0
  105. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js +12 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js +29 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  112. package/build/dist/Server/Middleware/SlackAuthorization.js +28 -4
  113. package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
  114. package/build/dist/Server/Middleware/UserAuthorization.js +197 -22
  115. package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
  116. package/build/dist/Server/Middleware/WhatsAppAuthorization.js +32 -2
  117. package/build/dist/Server/Middleware/WhatsAppAuthorization.js.map +1 -1
  118. package/build/dist/Server/Services/GlobalOidcProjectService.js +105 -0
  119. package/build/dist/Server/Services/GlobalOidcProjectService.js.map +1 -1
  120. package/build/dist/Server/Services/GlobalOidcService.js +127 -0
  121. package/build/dist/Server/Services/GlobalOidcService.js.map +1 -1
  122. package/build/dist/Server/Services/GlobalSsoProjectService.js +105 -0
  123. package/build/dist/Server/Services/GlobalSsoProjectService.js.map +1 -1
  124. package/build/dist/Server/Services/GlobalSsoService.js +127 -0
  125. package/build/dist/Server/Services/GlobalSsoService.js.map +1 -1
  126. package/build/dist/Server/Services/UserEmailService.js +4 -4
  127. package/build/dist/Server/Utils/CronTab.js +66 -0
  128. package/build/dist/Server/Utils/CronTab.js.map +1 -1
  129. package/build/dist/Server/Utils/GlobalSsoAuthorization.js +112 -0
  130. package/build/dist/Server/Utils/GlobalSsoAuthorization.js.map +1 -0
  131. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +17 -1
  132. package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +1 -1
  133. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +1 -1
  134. package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +1 -1
  135. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +1 -1
  136. package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +1 -1
  137. package/build/dist/Server/Utils/Marketing/Providers/Meta.js +1 -1
  138. package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +1 -1
  139. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +1 -1
  140. package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +1 -1
  141. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +1 -1
  142. package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +1 -1
  143. package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js +37 -35
  144. package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js.map +1 -1
  145. package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js +21 -25
  146. package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js.map +1 -1
  147. package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js +20 -22
  148. package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js.map +1 -1
  149. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +370 -42
  150. package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
  151. package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js +21 -25
  152. package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js.map +1 -1
  153. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +21 -25
  154. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
  155. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +21 -25
  156. package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
  157. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +64 -52
  158. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
  159. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +25 -29
  160. package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
  161. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +8 -0
  162. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  163. package/build/dist/Server/Utils/Monitor/MonitorResource.js +123 -23
  164. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  165. package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js +39 -0
  166. package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js.map +1 -0
  167. package/build/dist/Server/Utils/StatusPageCustomizationAccess.js +85 -0
  168. package/build/dist/Server/Utils/StatusPageCustomizationAccess.js.map +1 -0
  169. package/build/dist/Types/Marketing/MarketingConversion.js +10 -0
  170. package/build/dist/Types/Marketing/MarketingConversion.js.map +1 -1
  171. package/build/dist/Types/Monitor/CriteriaFilter.js +1 -0
  172. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  173. package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js +46 -0
  174. package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js.map +1 -0
  175. package/build/dist/UI/Components/FormModal/BasicFormModal.js +2 -1
  176. package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
  177. package/build/dist/UI/Components/Forms/BasicForm.js +10 -1
  178. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  179. package/build/dist/UI/Components/Forms/BasicModelForm.js +2 -1
  180. package/build/dist/UI/Components/Forms/BasicModelForm.js.map +1 -1
  181. package/build/dist/UI/Components/Forms/ModelForm.js +4 -1
  182. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  183. package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js +33 -0
  184. package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js.map +1 -0
  185. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js +2 -1
  186. package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
  187. package/build/dist/Utils/Analytics.js +50 -2
  188. package/build/dist/Utils/Analytics.js.map +1 -1
  189. package/build/dist/Utils/Monitor/MonitorMetricType.js +37 -0
  190. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  191. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +318 -37
  192. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -1
  193. package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js +87 -0
  194. package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js.map +1 -0
  195. package/package.json +1 -1
@@ -0,0 +1,215 @@
1
+ import NetworkDeviceLinkRuleScope, {
2
+ NetworkDeviceLinkRuleScopeUtil,
3
+ } from "../../../Types/NetworkDevice/NetworkDeviceLinkRuleScope";
4
+
5
+ /*
6
+ * `scope` was added to a table that already had rules in it, so every rule
7
+ * written before the column existed holds NULL — and each of those rules is
8
+ * currently drawing a topology somebody has already looked at and trusted.
9
+ * NULL therefore has to read as Project, because Project is exactly what
10
+ * those rules have always meant: one parent across whatever device set the
11
+ * query handed the resolver. Read NULL as Site instead and every legacy rule
12
+ * quietly starts asking a different question, re-pointing cables on maps
13
+ * nobody re-opened. That is why the default lives in `parse` rather than in a
14
+ * `=== "Site"` comparison scattered over the call sites: one place to be
15
+ * right, and it is pinned here.
16
+ *
17
+ * The mirror of the same worry is the unrecognised value. The column is free
18
+ * text (the NetworkDeviceMonitoringMethod precedent), so a typo, an older
19
+ * client, or a future scope name that this build has never heard of can all
20
+ * land in it. Every one of them has to fall back to the behaviour that was
21
+ * already on screen rather than to the new one.
22
+ */
23
+ describe("NetworkDeviceLinkRuleScope enum", () => {
24
+ /*
25
+ * These strings are persisted values, not display labels. Renaming one is
26
+ * a silent data migration: every row already holding the old spelling stops
27
+ * being recognised and — by the fallback above — quietly reverts to
28
+ * Project, with no error anywhere to say so. Change these only alongside a
29
+ * migration that rewrites the column.
30
+ */
31
+ test("Project is persisted as the string Project", () => {
32
+ expect(NetworkDeviceLinkRuleScope.Project).toBe("Project");
33
+ });
34
+
35
+ test("Site is persisted as the string Site", () => {
36
+ expect(NetworkDeviceLinkRuleScope.Site).toBe("Site");
37
+ });
38
+
39
+ test("has exactly the two known scopes", () => {
40
+ expect(Object.values(NetworkDeviceLinkRuleScope).sort()).toEqual([
41
+ "Project",
42
+ "Site",
43
+ ]);
44
+ });
45
+ });
46
+
47
+ /*
48
+ * One table, two consumers. `parse` and `isSiteScoped` are asserted against
49
+ * the same rows further down so the pair cannot drift apart: a caller that
50
+ * branches on `isSiteScoped` and a caller that switches on `parse` must never
51
+ * disagree about the same stored string.
52
+ */
53
+ const PROJECT_INPUTS: Array<string | null | undefined> = [
54
+ // The pre-column rows, and the shapes an empty form field arrives as.
55
+ undefined,
56
+ null,
57
+ "",
58
+ " ",
59
+ // The value written by anything that does set the column explicitly.
60
+ "Project",
61
+ "project",
62
+ "PROJECT",
63
+ " Project ",
64
+ /*
65
+ * Near-misses that must NOT be read as site scope. "Sites" and
66
+ * "site-scoped" are the plausible typo and the plausible rename; matching
67
+ * them loosely would turn a mistyped rule into one that silently redraws.
68
+ */
69
+ "Sites",
70
+ "site-scoped",
71
+ "anything",
72
+ // Falsy-looking text is still just unrecognised text, not a flag.
73
+ "0",
74
+ "false",
75
+ ];
76
+
77
+ const SITE_INPUTS: Array<string> = [
78
+ "Site",
79
+ "site",
80
+ "SITE",
81
+ " site ",
82
+ " SiTe ",
83
+ ];
84
+
85
+ interface ScopeCase {
86
+ value: string | null | undefined;
87
+ expected: NetworkDeviceLinkRuleScope;
88
+ }
89
+
90
+ const ALL_CASES: Array<ScopeCase> = [
91
+ ...PROJECT_INPUTS.map((value: string | null | undefined): ScopeCase => {
92
+ return { value: value, expected: NetworkDeviceLinkRuleScope.Project };
93
+ }),
94
+ ...SITE_INPUTS.map((value: string): ScopeCase => {
95
+ return { value: value, expected: NetworkDeviceLinkRuleScope.Site };
96
+ }),
97
+ ];
98
+
99
+ describe("NetworkDeviceLinkRuleScopeUtil.parse", () => {
100
+ test.each(PROJECT_INPUTS)(
101
+ "reads %p as Project",
102
+ (value: string | null | undefined) => {
103
+ expect(NetworkDeviceLinkRuleScopeUtil.parse(value)).toBe(
104
+ NetworkDeviceLinkRuleScope.Project,
105
+ );
106
+ },
107
+ );
108
+
109
+ test.each(SITE_INPUTS)(
110
+ "reads %p as Site, case- and whitespace-insensitively",
111
+ (value: string) => {
112
+ expect(NetworkDeviceLinkRuleScopeUtil.parse(value)).toBe(
113
+ NetworkDeviceLinkRuleScope.Site,
114
+ );
115
+ },
116
+ );
117
+
118
+ /*
119
+ * Only an exact (trimmed, lowercased) "site" opts in. Substring matching
120
+ * would make "not site" or "site (deprecated)" flip the rule's meaning,
121
+ * which is the one direction of mistake this type refuses to make.
122
+ */
123
+ test("does not treat a string merely containing 'site' as site-scoped", () => {
124
+ expect(NetworkDeviceLinkRuleScopeUtil.parse("per site")).toBe(
125
+ NetworkDeviceLinkRuleScope.Project,
126
+ );
127
+ expect(NetworkDeviceLinkRuleScopeUtil.parse("not site")).toBe(
128
+ NetworkDeviceLinkRuleScope.Project,
129
+ );
130
+ });
131
+
132
+ /*
133
+ * The return value is always one of the two enum members — never the raw
134
+ * column text passed straight back. Callers switch on it, and a passthrough
135
+ * would send an unrecognised spelling into a branch that does not exist.
136
+ */
137
+ test("returns an enum member for every case in the table, never the raw input", () => {
138
+ for (const testCase of ALL_CASES) {
139
+ const parsed: NetworkDeviceLinkRuleScope =
140
+ NetworkDeviceLinkRuleScopeUtil.parse(testCase.value);
141
+
142
+ expect(Object.values(NetworkDeviceLinkRuleScope)).toContain(parsed);
143
+ }
144
+ });
145
+
146
+ /*
147
+ * The parser sits in the middle of rendering a topology map: throwing here
148
+ * would take out the whole map over one bad cell, when the safe answer
149
+ * (Project, what the rule drew yesterday) is right there. So nothing in the
150
+ * declared signature may throw — and neither may the falsy non-string
151
+ * shapes a loosely typed caller or a hand-rolled JSON body can smuggle past
152
+ * TypeScript, which the `|| ""` guard absorbs.
153
+ */
154
+ test("never throws for any value in or near its declared signature", () => {
155
+ const inputs: Array<string | null | undefined> = [
156
+ ...ALL_CASES.map((testCase: ScopeCase): string | null | undefined => {
157
+ return testCase.value;
158
+ }),
159
+ "\tSITE\n",
160
+ "\n project \t",
161
+ "Site\u0000",
162
+ "🙂",
163
+ ];
164
+
165
+ for (const input of inputs) {
166
+ expect((): NetworkDeviceLinkRuleScope => {
167
+ return NetworkDeviceLinkRuleScopeUtil.parse(input);
168
+ }).not.toThrow();
169
+ }
170
+
171
+ /*
172
+ * Not strings at all. 0/false/NaN are what a JSON body or a permissive
173
+ * ORM can hand over in place of "no scope set", and they have to land on
174
+ * the same safe default rather than blow up.
175
+ */
176
+ const nonStrings: Array<unknown> = [0, false, NaN];
177
+
178
+ for (const input of nonStrings) {
179
+ expect(
180
+ NetworkDeviceLinkRuleScopeUtil.parse(input as unknown as string),
181
+ ).toBe(NetworkDeviceLinkRuleScope.Project);
182
+ }
183
+ });
184
+ });
185
+
186
+ describe("NetworkDeviceLinkRuleScopeUtil.isSiteScoped", () => {
187
+ test.each(SITE_INPUTS)("is true for %p", (value: string) => {
188
+ expect(NetworkDeviceLinkRuleScopeUtil.isSiteScoped(value)).toBe(true);
189
+ });
190
+
191
+ test.each(PROJECT_INPUTS)(
192
+ "is false for %p",
193
+ (value: string | null | undefined) => {
194
+ expect(NetworkDeviceLinkRuleScopeUtil.isSiteScoped(value)).toBe(false);
195
+ },
196
+ );
197
+
198
+ /*
199
+ * The two entry points are asserted against the same table in the same
200
+ * loop, so neither can be extended (a new alias, a new scope) without the
201
+ * other. `NetworkDeviceLinkRuleUtil` branches on `isSiteScoped` while the
202
+ * dashboard's rule list renders off it too — if they ever disagreed, a rule
203
+ * would be labelled one way and resolved the other.
204
+ */
205
+ test("agrees with parse on every case in the table", () => {
206
+ for (const testCase of ALL_CASES) {
207
+ expect(NetworkDeviceLinkRuleScopeUtil.parse(testCase.value)).toBe(
208
+ testCase.expected,
209
+ );
210
+ expect(NetworkDeviceLinkRuleScopeUtil.isSiteScoped(testCase.value)).toBe(
211
+ testCase.expected === NetworkDeviceLinkRuleScope.Site,
212
+ );
213
+ }
214
+ });
215
+ });
@@ -0,0 +1,206 @@
1
+ import "@testing-library/jest-dom";
2
+ import { beforeEach, describe, expect, jest, test } from "@jest/globals";
3
+ import { render, screen, waitFor } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import { UserEvent } from "@testing-library/user-event/dist/types/setup/setup";
6
+ import React from "react";
7
+ import getJestMockFunction, { MockFunction } from "../../../MockType";
8
+
9
+ /*
10
+ * The "FORM SUBMIT" event is what names a conversion in PostHog and in the GTM
11
+ * dataLayer. Every wrapper around BasicForm has to hand it something
12
+ * meaningful, and BasicForm has to stay silent when nothing meaningful exists.
13
+ * An event named after a missing prop reads as "FORM SUBMIT: undefined" and
14
+ * collapses every form in the product into one indistinguishable conversion.
15
+ */
16
+
17
+ const captureMock: MockFunction = getJestMockFunction();
18
+
19
+ jest.mock("../../../../UI/Utils/Analytics", () => {
20
+ return {
21
+ __esModule: true,
22
+ default: {
23
+ capture: (...args: Array<any>) => {
24
+ return captureMock(...args);
25
+ },
26
+ },
27
+ };
28
+ });
29
+
30
+ import BasicForm from "../../../../UI/Components/Forms/BasicForm";
31
+ import BasicFormModal from "../../../../UI/Components/FormModal/BasicFormModal";
32
+ import Fields from "../../../../UI/Components/Forms/Types/Fields";
33
+ import FormFieldSchemaType from "../../../../UI/Components/Forms/Types/FormFieldSchemaType";
34
+ import FormValues from "../../../../UI/Components/Forms/Types/FormValues";
35
+ import FormAnalyticsName from "../../../../UI/Components/Forms/Utils/FormAnalyticsName";
36
+
37
+ const fields: Fields<FormValues<any>> = [
38
+ {
39
+ field: {
40
+ label: true,
41
+ },
42
+ title: "Label",
43
+ fieldType: FormFieldSchemaType.Text,
44
+ dataTestId: "label",
45
+ },
46
+ ];
47
+
48
+ function setupUser(): UserEvent {
49
+ return userEvent.setup({ delay: null });
50
+ }
51
+
52
+ type CapturedEventNamesFunction = () => Array<string>;
53
+
54
+ const capturedEventNames: CapturedEventNamesFunction = (): Array<string> => {
55
+ return captureMock.mock.calls.map((call: Array<any>) => {
56
+ return call[0] as string;
57
+ });
58
+ };
59
+
60
+ describe("FORM SUBMIT analytics naming", () => {
61
+ beforeEach(() => {
62
+ captureMock.mockClear();
63
+ });
64
+
65
+ describe("FormAnalyticsName.resolve", () => {
66
+ test("takes the first usable candidate", () => {
67
+ expect(FormAnalyticsName.resolve("Create Monitor", "Fallback")).toBe(
68
+ "Create Monitor",
69
+ );
70
+ expect(FormAnalyticsName.resolve(undefined, "Fallback")).toBe("Fallback");
71
+ expect(FormAnalyticsName.resolve(null, " Trimmed ")).toBe("Trimmed");
72
+ });
73
+
74
+ test("rejects candidates that are, or embed, a stringified empty value", () => {
75
+ expect(FormAnalyticsName.resolve("undefined")).toBeUndefined();
76
+ expect(FormAnalyticsName.resolve("null")).toBeUndefined();
77
+ expect(FormAnalyticsName.resolve("Duplicate undefined")).toBeUndefined();
78
+ expect(FormAnalyticsName.resolve("Edit null")).toBeUndefined();
79
+
80
+ // A rejected candidate falls through to the next one.
81
+ expect(
82
+ FormAnalyticsName.resolve("Duplicate undefined", "Duplicate Monitor"),
83
+ ).toBe("Duplicate Monitor");
84
+ });
85
+
86
+ test("returns undefined when no candidate is usable", () => {
87
+ expect(FormAnalyticsName.resolve()).toBeUndefined();
88
+ expect(
89
+ FormAnalyticsName.resolve(undefined, null, "", " "),
90
+ ).toBeUndefined();
91
+ });
92
+
93
+ test("leaves real words that merely look like empty values alone", () => {
94
+ expect(FormAnalyticsName.resolve("Set Nullable Columns")).toBe(
95
+ "Set Nullable Columns",
96
+ );
97
+ });
98
+ });
99
+
100
+ describe("BasicForm", () => {
101
+ test("captures the form name on submit", async () => {
102
+ const onSubmit: MockFunction = getJestMockFunction();
103
+
104
+ render(
105
+ <BasicForm
106
+ fields={fields}
107
+ id="named-form"
108
+ name="Create Widget"
109
+ initialValues={{ label: "" }}
110
+ onSubmit={onSubmit}
111
+ submitButtonText="Submit"
112
+ />,
113
+ );
114
+
115
+ const user: UserEvent = setupUser();
116
+ await user.click(screen.getByTestId("Submit"));
117
+
118
+ await waitFor(() => {
119
+ expect(onSubmit).toHaveBeenCalled();
120
+ });
121
+
122
+ expect(capturedEventNames()).toEqual(["FORM SUBMIT: Create Widget"]);
123
+ }, 30000);
124
+
125
+ test("stays silent when the form has no name", async () => {
126
+ const onSubmit: MockFunction = getJestMockFunction();
127
+
128
+ render(
129
+ <BasicForm
130
+ fields={fields}
131
+ id="unnamed-form"
132
+ initialValues={{ label: "" }}
133
+ onSubmit={onSubmit}
134
+ submitButtonText="Submit"
135
+ />,
136
+ );
137
+
138
+ const user: UserEvent = setupUser();
139
+ await user.click(screen.getByTestId("Submit"));
140
+
141
+ // The form still submits -- only the analytics event is skipped.
142
+ await waitFor(() => {
143
+ expect(onSubmit).toHaveBeenCalled();
144
+ });
145
+
146
+ expect(captureMock).not.toHaveBeenCalled();
147
+ }, 30000);
148
+ });
149
+
150
+ describe("BasicFormModal", () => {
151
+ test("falls back to the modal title when the caller passes no name", async () => {
152
+ const onSubmit: MockFunction = getJestMockFunction();
153
+
154
+ render(
155
+ <BasicFormModal<FormValues<any>>
156
+ title="Add Probe to Monitors"
157
+ submitButtonText="Save"
158
+ onSubmit={onSubmit}
159
+ formProps={{
160
+ fields: fields,
161
+ initialValues: { label: "" },
162
+ }}
163
+ />,
164
+ );
165
+
166
+ const user: UserEvent = setupUser();
167
+ await user.click(screen.getByTestId("modal-footer-submit-button"));
168
+
169
+ await waitFor(() => {
170
+ expect(onSubmit).toHaveBeenCalled();
171
+ });
172
+
173
+ expect(capturedEventNames()).toEqual([
174
+ "FORM SUBMIT: Add Probe to Monitors",
175
+ ]);
176
+ }, 30000);
177
+
178
+ test("prefers a name the caller supplied over the modal title", async () => {
179
+ const onSubmit: MockFunction = getJestMockFunction();
180
+
181
+ render(
182
+ <BasicFormModal<FormValues<any>>
183
+ title="Add Probe to Monitors"
184
+ name="Monitor > Bulk Add Probe"
185
+ submitButtonText="Save"
186
+ onSubmit={onSubmit}
187
+ formProps={{
188
+ fields: fields,
189
+ initialValues: { label: "" },
190
+ }}
191
+ />,
192
+ );
193
+
194
+ const user: UserEvent = setupUser();
195
+ await user.click(screen.getByTestId("modal-footer-submit-button"));
196
+
197
+ await waitFor(() => {
198
+ expect(onSubmit).toHaveBeenCalled();
199
+ });
200
+
201
+ expect(capturedEventNames()).toEqual([
202
+ "FORM SUBMIT: Monitor > Bulk Add Probe",
203
+ ]);
204
+ }, 30000);
205
+ });
206
+ });