@oneuptime/common 12.0.1 → 12.0.2

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 (205) hide show
  1. package/Models/DatabaseModels/Host.ts +12 -4
  2. package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
  3. package/Models/DatabaseModels/KubernetesResource.ts +8 -7
  4. package/Models/DatabaseModels/PodmanResource.ts +6 -6
  5. package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
  6. package/Models/DatabaseModels/StatusPage.ts +95 -0
  7. package/Server/API/TeamMemberAPI.ts +85 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  13. package/Server/Middleware/ProjectAuthorization.ts +66 -4
  14. package/Server/Services/BillingService.ts +600 -51
  15. package/Server/Services/CephClusterService.ts +47 -5
  16. package/Server/Services/CephResourceService.ts +43 -4
  17. package/Server/Services/CloudResourceService.ts +47 -5
  18. package/Server/Services/DatabaseService.ts +87 -6
  19. package/Server/Services/DockerHostService.ts +47 -5
  20. package/Server/Services/DockerResourceService.ts +62 -8
  21. package/Server/Services/DockerSwarmClusterService.ts +48 -5
  22. package/Server/Services/DockerSwarmResourceService.ts +53 -4
  23. package/Server/Services/HostService.ts +48 -5
  24. package/Server/Services/IoTDeviceService.ts +1 -17
  25. package/Server/Services/IoTFleetService.ts +47 -5
  26. package/Server/Services/KubernetesClusterService.ts +48 -5
  27. package/Server/Services/KubernetesContainerService.ts +50 -9
  28. package/Server/Services/KubernetesResourceService.ts +53 -7
  29. package/Server/Services/LabelService.ts +106 -1
  30. package/Server/Services/MonitorService.ts +57 -0
  31. package/Server/Services/PodmanHostService.ts +47 -5
  32. package/Server/Services/PodmanResourceService.ts +62 -8
  33. package/Server/Services/ProjectService.ts +165 -35
  34. package/Server/Services/ProxmoxClusterService.ts +47 -5
  35. package/Server/Services/ProxmoxResourceService.ts +43 -4
  36. package/Server/Services/RumApplicationService.ts +48 -5
  37. package/Server/Services/ServerlessFunctionService.ts +48 -5
  38. package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
  39. package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
  40. package/Server/Services/StatusPageService.ts +95 -36
  41. package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
  42. package/Server/Services/StatusPageSubscriberService.ts +3 -0
  43. package/Server/Types/Database/JSONColumnQuery.ts +621 -0
  44. package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
  45. package/Server/Types/Database/QueryHelper.ts +17 -32
  46. package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
  47. package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
  48. package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
  49. package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
  50. package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
  51. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
  52. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
  53. package/Tests/Server/Services/BillingService.test.ts +78 -3
  54. package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
  55. package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
  56. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
  57. package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
  58. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +510 -0
  59. package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
  60. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
  61. package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
  62. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
  63. package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
  64. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +387 -0
  65. package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
  66. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
  67. package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
  68. package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
  69. package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
  70. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
  71. package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
  72. package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
  73. package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
  74. package/Tests/Types/Events/Recurring.test.ts +157 -0
  75. package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
  76. package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
  77. package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
  78. package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
  79. package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
  80. package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
  81. package/Types/CustomField/CustomFieldDefinition.ts +24 -0
  82. package/Types/Date.ts +112 -0
  83. package/Types/Events/Recurring.ts +99 -1
  84. package/Types/StatusPage/StatusPageReport.ts +11 -0
  85. package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
  86. package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
  87. package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
  88. package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
  89. package/UI/Utils/TeamMembersByUser.ts +302 -0
  90. package/Utils/StatusPage/ReportPeriod.ts +236 -0
  91. package/Utils/Telemetry/HostIpAddresses.ts +98 -0
  92. package/build/dist/Models/DatabaseModels/Host.js +12 -4
  93. package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
  95. package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
  97. package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
  99. package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
  100. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
  101. package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
  102. package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
  103. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  104. package/build/dist/Server/API/TeamMemberAPI.js +57 -2
  105. package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
  112. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
  113. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
  114. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  115. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  116. package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
  117. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  118. package/build/dist/Server/Services/BillingService.js +466 -36
  119. package/build/dist/Server/Services/BillingService.js.map +1 -1
  120. package/build/dist/Server/Services/CephClusterService.js +42 -5
  121. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  122. package/build/dist/Server/Services/CephResourceService.js +28 -3
  123. package/build/dist/Server/Services/CephResourceService.js.map +1 -1
  124. package/build/dist/Server/Services/CloudResourceService.js +42 -5
  125. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  126. package/build/dist/Server/Services/DatabaseService.js +69 -2
  127. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  128. package/build/dist/Server/Services/DockerHostService.js +42 -5
  129. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  130. package/build/dist/Server/Services/DockerResourceService.js +34 -4
  131. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  132. package/build/dist/Server/Services/DockerSwarmClusterService.js +43 -5
  133. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  134. package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
  135. package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
  136. package/build/dist/Server/Services/HostService.js +43 -5
  137. package/build/dist/Server/Services/HostService.js.map +1 -1
  138. package/build/dist/Server/Services/IoTDeviceService.js +1 -8
  139. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  140. package/build/dist/Server/Services/IoTFleetService.js +42 -5
  141. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  142. package/build/dist/Server/Services/KubernetesClusterService.js +43 -5
  143. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  144. package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
  145. package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
  146. package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
  147. package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
  148. package/build/dist/Server/Services/LabelService.js +92 -0
  149. package/build/dist/Server/Services/LabelService.js.map +1 -1
  150. package/build/dist/Server/Services/MonitorService.js +47 -0
  151. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  152. package/build/dist/Server/Services/PodmanHostService.js +42 -5
  153. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  154. package/build/dist/Server/Services/PodmanResourceService.js +34 -4
  155. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  156. package/build/dist/Server/Services/ProjectService.js +135 -21
  157. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  158. package/build/dist/Server/Services/ProxmoxClusterService.js +42 -5
  159. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  160. package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
  161. package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
  162. package/build/dist/Server/Services/RumApplicationService.js +43 -5
  163. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  164. package/build/dist/Server/Services/ServerlessFunctionService.js +43 -5
  165. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  166. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
  167. package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
  168. package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
  169. package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
  170. package/build/dist/Server/Services/StatusPageService.js +69 -46
  171. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  172. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
  173. package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
  174. package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
  175. package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
  176. package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
  177. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
  178. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
  179. package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
  180. package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
  181. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  182. package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
  183. package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
  184. package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
  185. package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
  186. package/build/dist/Types/Date.js +68 -0
  187. package/build/dist/Types/Date.js.map +1 -1
  188. package/build/dist/Types/Events/Recurring.js +63 -0
  189. package/build/dist/Types/Events/Recurring.js.map +1 -1
  190. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
  191. package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
  192. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  193. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
  194. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
  195. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
  196. package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
  197. package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
  198. package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
  199. package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
  200. package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
  201. package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
  202. package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
  203. package/jest.config.json +2 -0
  204. package/package.json +1 -1
  205. package/tsconfig.json +12 -1
@@ -0,0 +1,704 @@
1
+ import BaseAPI from "../../../Server/API/BaseAPI";
2
+ import ApiKeyPermissionService from "../../../Server/Services/ApiKeyPermissionService";
3
+ import ApiKeyService from "../../../Server/Services/ApiKeyService";
4
+ import DatabaseService from "../../../Server/Services/DatabaseService";
5
+ import GlobalConfigService from "../../../Server/Services/GlobalConfigService";
6
+ import ProjectService from "../../../Server/Services/ProjectService";
7
+ import JSONWebToken from "../../../Server/Utils/JsonWebToken";
8
+ import {
9
+ ExpressResponse,
10
+ NextFunction,
11
+ OneUptimeRequest,
12
+ } from "../../../Server/Utils/Express";
13
+ import Response from "../../../Server/Utils/Response";
14
+ import { mockRouter } from "./Helpers";
15
+ import ApiKeyPermission from "../../../Models/DatabaseModels/ApiKeyPermission";
16
+ import LogPipeline from "../../../Models/DatabaseModels/LogPipeline";
17
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
18
+ import BadDataException from "../../../Types/Exception/BadDataException";
19
+ import Exception from "../../../Types/Exception/Exception";
20
+ import NotAuthenticatedException from "../../../Types/Exception/NotAuthenticatedException";
21
+ import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
22
+ import ObjectID from "../../../Types/ObjectID";
23
+ import Permission from "../../../Types/Permission";
24
+ import UserType from "../../../Types/UserType";
25
+ import { getJestSpyOn } from "../../Spy";
26
+ import { beforeEach, describe, expect, it, jest } from "@jest/globals";
27
+
28
+ jest.mock("../../../Server/Utils/Express", () => {
29
+ return {
30
+ getRouter: () => {
31
+ return mockRouter;
32
+ },
33
+ };
34
+ });
35
+
36
+ jest.mock("../../../Server/Utils/Response", () => {
37
+ return {
38
+ sendEmptySuccessResponse: jest.fn(),
39
+ sendEntityResponse: jest.fn(),
40
+ sendEntityArrayResponse: jest.fn(),
41
+ sendJsonArrayResponse: jest.fn(),
42
+ sendJsonObjectResponse: jest.fn(),
43
+ sendErrorResponse: jest.fn(),
44
+ };
45
+ });
46
+
47
+ /*
48
+ * Issue #3004: a caller sent a valid project API key to
49
+ * POST /api/log-pipeline/get-list and got
50
+ * 401 {"error":"A user should be logged in to read record of Log Pipeline."}
51
+ * They read that as a broken RBAC path on Log Pipeline specifically.
52
+ *
53
+ * It was neither Log-Pipeline-specific nor an RBAC failure. That message was
54
+ * what the codebase produced for ANY request it had already decided was
55
+ * anonymous - and a request whose `ApiKey` header arrived empty or malformed
56
+ * was decided to be anonymous, because the "does this caller present a key"
57
+ * test asked whether the key was USABLE rather than whether it was PRESENT.
58
+ * The caller was then told to log in (they never tried to) about a model
59
+ * (which was never the problem). Issue #1754 is the same string on a
60
+ * different model.
61
+ *
62
+ * Nothing below is Log-Pipeline-specific. This is the generic CRUD auth
63
+ * contract of every model registered through BaseAPI; LogPipeline is here
64
+ * only because it is the resource named in the report, and the control block
65
+ * at the bottom re-runs the two decisive cases against Monitor to show the
66
+ * outcome does not depend on the model.
67
+ *
68
+ * The whole real chain runs: the route's own middleware is
69
+ * UserMiddleware.getUserMiddleware, which routes to
70
+ * ProjectMiddleware.isValidProjectIdAndApiKeyMiddleware, and the handler runs
71
+ * the real DatabaseService read with the real permission pipeline. Only the
72
+ * TypeORM repository and the three lookup services are stubbed.
73
+ *
74
+ * On the status codes asserted here: every failure below leaves via
75
+ * `next(err)`, and Express's error handler in Server/Utils/StartServer.ts
76
+ * answers `res.status(err.code).send({ error: err.message })`. So the
77
+ * exception's `code` IS the HTTP status the caller sees -
78
+ * BadDataException=400, NotAuthenticatedException=401,
79
+ * NotAuthorizedException=422 (Types/Exception/ExceptionCode.ts).
80
+ */
81
+
82
+ /*
83
+ * Any hex UUID is accepted by ObjectID.isValidUUID (any version,
84
+ * case-insensitive), so these only have to be well formed.
85
+ */
86
+ const API_KEY_ID: ObjectID = new ObjectID(
87
+ "11111111-1111-4111-8111-111111111111",
88
+ );
89
+ const PROJECT_ID: ObjectID = new ObjectID(
90
+ "22222222-2222-4222-8222-222222222222",
91
+ );
92
+ const KNOWN_API_KEY: string = "33333333-3333-4333-8333-333333333333";
93
+ const UNKNOWN_API_KEY: string = "44444444-4444-4444-8444-444444444444";
94
+
95
+ /*
96
+ * A project the key does NOT belong to, used only to prove a caller-supplied
97
+ * `tenantid` header cannot redirect a key at another project.
98
+ */
99
+ const OTHER_PROJECT_ID: string = "55555555-5555-4555-8555-555555555555";
100
+
101
+ /*
102
+ * Registered once, at module load, because mockRouter.match returns the FIRST
103
+ * route with a given method+uri and that route closes over the instance that
104
+ * registered it. Re-registering per test would leave the matched route
105
+ * pointing at a stale service that the per-test spies never touch.
106
+ */
107
+ const logPipelineService: DatabaseService<LogPipeline> =
108
+ new DatabaseService<LogPipeline>(LogPipeline);
109
+ const logPipelineApi: BaseAPI<
110
+ LogPipeline,
111
+ DatabaseService<LogPipeline>
112
+ > = new BaseAPI<LogPipeline, DatabaseService<LogPipeline>>(
113
+ LogPipeline,
114
+ logPipelineService,
115
+ );
116
+
117
+ const monitorService: DatabaseService<Monitor> = new DatabaseService<Monitor>(
118
+ Monitor,
119
+ );
120
+ const monitorApi: BaseAPI<Monitor, DatabaseService<Monitor>> = new BaseAPI<
121
+ Monitor,
122
+ DatabaseService<Monitor>
123
+ >(Monitor, monitorService);
124
+
125
+ const LOG_PIPELINE_PATH: string = new logPipelineApi.entityType()
126
+ .getCrudApiPath()!
127
+ .toString();
128
+
129
+ const MONITOR_PATH: string = new monitorApi.entityType()
130
+ .getCrudApiPath()!
131
+ .toString();
132
+
133
+ /*
134
+ * Stands in for the TypeORM repository so the read stops at the DB boundary
135
+ * and everything above it - permission checks included - is the real thing.
136
+ * Mocking service.findBy instead would delete the very checks under test.
137
+ */
138
+ type RepositoryStub = {
139
+ find: () => Promise<Array<never>>;
140
+ count: () => Promise<number>;
141
+ };
142
+
143
+ const repository: RepositoryStub = {
144
+ find: (): Promise<Array<never>> => {
145
+ return Promise.resolve([]);
146
+ },
147
+ count: (): Promise<number> => {
148
+ return Promise.resolve(0);
149
+ },
150
+ };
151
+
152
+ type HeaderBag = Record<string, string | Array<string> | undefined>;
153
+
154
+ type RouteOutcome = {
155
+ error: Exception | null;
156
+ request: OneUptimeRequest;
157
+ };
158
+
159
+ type CallGetListFunction = (data: {
160
+ crudApiPath: string;
161
+ headers: HeaderBag;
162
+ }) => Promise<RouteOutcome>;
163
+
164
+ /*
165
+ * Drives the registered POST /<model>/get-list route the way Express would:
166
+ * middleware first, then - only if the middleware did not hand an error to
167
+ * next() - the handler. Both report failure through next(err).
168
+ */
169
+ const callGetList: CallGetListFunction = async (data: {
170
+ crudApiPath: string;
171
+ headers: HeaderBag;
172
+ }): Promise<RouteOutcome> => {
173
+ const route: ReturnType<typeof mockRouter.match> = mockRouter.match(
174
+ "POST",
175
+ `${data.crudApiPath}/get-list`,
176
+ );
177
+
178
+ const request: OneUptimeRequest = {
179
+ headers: data.headers,
180
+ params: {},
181
+ query: {},
182
+ body: { query: {}, select: { _id: true }, sort: {} },
183
+ } as unknown as OneUptimeRequest;
184
+
185
+ const response: ExpressResponse = {
186
+ status: jest.fn().mockReturnThis(),
187
+ json: jest.fn().mockReturnThis(),
188
+ send: jest.fn().mockReturnThis(),
189
+ set: jest.fn().mockReturnThis(),
190
+ } as unknown as ExpressResponse;
191
+
192
+ const captured: { error: Exception | null } = { error: null };
193
+
194
+ const next: NextFunction = ((error?: unknown): void => {
195
+ if (error) {
196
+ captured.error = error as Exception;
197
+ }
198
+ }) as unknown as NextFunction;
199
+
200
+ await route.middleware(request, response, next);
201
+
202
+ if (!captured.error) {
203
+ await route.handlerFunction(request, response, next);
204
+ }
205
+
206
+ return { error: captured.error, request: request };
207
+ };
208
+
209
+ describe("BaseAPI CRUD auth contract for the ApiKey header (issue #3004)", () => {
210
+ let repositoryFind: ReturnType<typeof getJestSpyOn>;
211
+ let findApiKey: ReturnType<typeof getJestSpyOn>;
212
+ let findApiKeyPermissions: ReturnType<typeof getJestSpyOn>;
213
+ let decodeAccessToken: ReturnType<typeof getJestSpyOn>;
214
+
215
+ type GrantPermissionFunction = (permission: Permission) => void;
216
+
217
+ /*
218
+ * Makes KNOWN_API_KEY resolve to a key on PROJECT_ID carrying exactly one
219
+ * permission. isBlockPermission has to be an explicit false: getUserPermissions
220
+ * keeps a row only when `isBlockPermission === (permissionType === Block)`, so
221
+ * an undefined here silently drops the grant and turns a 200 into a 422.
222
+ */
223
+ const grantApiKeyPermission: GrantPermissionFunction = (
224
+ permission: Permission,
225
+ ): void => {
226
+ findApiKey.mockResolvedValue({ id: API_KEY_ID, projectId: PROJECT_ID });
227
+
228
+ const apiKeyPermission: ApiKeyPermission = new ApiKeyPermission();
229
+ apiKeyPermission.permission = permission;
230
+ apiKeyPermission.labels = [];
231
+ apiKeyPermission.isBlockPermission = false;
232
+
233
+ findApiKeyPermissions.mockResolvedValue([apiKeyPermission]);
234
+ };
235
+
236
+ beforeEach(() => {
237
+ jest.restoreAllMocks();
238
+ jest.clearAllMocks();
239
+
240
+ repositoryFind = getJestSpyOn(repository, "find");
241
+
242
+ getJestSpyOn(logPipelineService, "getRepository").mockReturnValue(
243
+ repository,
244
+ );
245
+ getJestSpyOn(monitorService, "getRepository").mockReturnValue(repository);
246
+
247
+ // Default: no key in the table, no permissions, and no master key configured.
248
+ findApiKey = getJestSpyOn(ApiKeyService, "findApiKey").mockResolvedValue(
249
+ null,
250
+ );
251
+ findApiKeyPermissions = getJestSpyOn(
252
+ ApiKeyPermissionService,
253
+ "findBy",
254
+ ).mockResolvedValue([]);
255
+ getJestSpyOn(GlobalConfigService, "findOneBy").mockResolvedValue(null);
256
+
257
+ /*
258
+ * getUserMiddleware fires this off (unawaited) whenever a `tenantid`
259
+ * header is present. It is a write we never assert on; stubbing it keeps
260
+ * a stray Postgres round-trip from landing after the test has finished.
261
+ */
262
+ getJestSpyOn(ProjectService, "updateLastActive").mockResolvedValue(
263
+ undefined,
264
+ );
265
+
266
+ /*
267
+ * getDatabaseCommonInteractionProps reads the project's billing plan on
268
+ * every request that resolved a tenant, but ONLY when BILLING_ENABLED is
269
+ * set - and the Common Test CI job sets it while a bare local jest run
270
+ * does not. Unstubbed, that is a real ProjectService.findOneById on a
271
+ * service this file never stubs, so the five cases that get as far as a
272
+ * tenant died with "Database not connected" in CI and passed locally.
273
+ *
274
+ * Answering as the no-plan project keeps the auth contract below reading
275
+ * the same either way: BillingPermissions short-circuits on an undefined
276
+ * currentPlan, which is exactly the billing-disabled path.
277
+ */
278
+ getJestSpyOn(ProjectService, "getCurrentPlan").mockResolvedValue({
279
+ plan: null,
280
+ isSubscriptionUnpaid: false,
281
+ });
282
+
283
+ /*
284
+ * Never stubbed to succeed - it exists so tests can assert whether the
285
+ * session branch of getUserMiddleware was entered at all. Reaching it is
286
+ * always the regression, never the expectation.
287
+ */
288
+ decodeAccessToken = getJestSpyOn(JSONWebToken, "decode");
289
+ });
290
+
291
+ it("registers the route the issue reported against", () => {
292
+ expect(LOG_PIPELINE_PATH).toBe("/log-pipeline");
293
+ expect(
294
+ mockRouter.match("POST", `${LOG_PIPELINE_PATH}/get-list`),
295
+ ).toBeTruthy();
296
+ });
297
+
298
+ describe("A) no ApiKey header at all", () => {
299
+ /*
300
+ * The genuinely anonymous request - no key, no session. This is the one
301
+ * case where the "authenticate first" answer is correct, and it must name
302
+ * BOTH accepted credentials so an API caller who reads it is not sent off
303
+ * to audit session login or the model's roles. The old wording
304
+ * ("A user should be logged in to read record of Log Pipeline.") is what
305
+ * made #3004 and #1754 look like per-model RBAC bugs.
306
+ */
307
+ it("answers 401 naming both a session and an API key", async () => {
308
+ const outcome: RouteOutcome = await callGetList({
309
+ crudApiPath: LOG_PIPELINE_PATH,
310
+ headers: {},
311
+ });
312
+
313
+ expect(outcome.error).toBeInstanceOf(NotAuthenticatedException);
314
+ expect(outcome.error?.message).toBe(
315
+ "Authenticated user or a valid API key is needed to read record of Log Pipeline.",
316
+ );
317
+ expect(outcome.error?.code).toBe(401);
318
+ expect(outcome.error?.message).not.toContain("should be logged in");
319
+ expect(repositoryFind).not.toHaveBeenCalled();
320
+ expect(Response.sendEntityArrayResponse).not.toHaveBeenCalled();
321
+ });
322
+ });
323
+
324
+ describe("B) ApiKey header present but empty", () => {
325
+ /*
326
+ * THE #3004 FIX, stated as an assertion.
327
+ *
328
+ * Before it, an empty header was "no usable key" and therefore treated as
329
+ * "no key", so the request fell through to the anonymous path and produced
330
+ * case A above: a 401 about logging in, naming Log Pipeline. An unset shell
331
+ * variable, a client that serializes a missing credential as "", and our own
332
+ * CLI's partial-credential path all land here.
333
+ *
334
+ * Presence now claims the request for the API-key branch, so it fails where
335
+ * the caller actually went wrong: 400 Invalid API Key.
336
+ */
337
+ it("answers 400 Invalid API Key instead of the anonymous 401", async () => {
338
+ const outcome: RouteOutcome = await callGetList({
339
+ crudApiPath: LOG_PIPELINE_PATH,
340
+ headers: { apikey: "" },
341
+ });
342
+
343
+ expect(outcome.error).toBeInstanceOf(BadDataException);
344
+ expect(outcome.error?.message).toBe("Invalid API Key");
345
+ expect(outcome.error?.code).toBe(400);
346
+ });
347
+
348
+ it("does not mention logging in, or the model, for a key problem", async () => {
349
+ const outcome: RouteOutcome = await callGetList({
350
+ crudApiPath: LOG_PIPELINE_PATH,
351
+ headers: { apikey: "" },
352
+ });
353
+
354
+ expect(outcome.error?.message).not.toContain("logged in");
355
+ expect(outcome.error?.message).not.toContain("Log Pipeline");
356
+ expect(outcome.error).not.toBeInstanceOf(NotAuthenticatedException);
357
+ });
358
+
359
+ /*
360
+ * Where the request is decided, not just what it is told.
361
+ *
362
+ * The empty key must be claimed by the API-key branch: the request is
363
+ * never classified as an anonymous `Public` caller (which is exactly what
364
+ * the pre-fix `hasApiKey` did to it, and the only reason the permission
365
+ * layer ever saw it), and it never reaches the key table either, because
366
+ * "" is not a lookup-able value. Nothing is disclosed and no response is
367
+ * written - the failure leaves via next(err).
368
+ *
369
+ * Stopping at "no rows were read" would not have pinned this down: the
370
+ * old anonymous path did not read any rows either, it just failed later
371
+ * and in the wrong words.
372
+ */
373
+ it("is claimed by the API key branch rather than treated as an anonymous caller", async () => {
374
+ const outcome: RouteOutcome = await callGetList({
375
+ crudApiPath: LOG_PIPELINE_PATH,
376
+ headers: { apikey: "" },
377
+ });
378
+
379
+ expect(outcome.request.userType).not.toBe(UserType.Public);
380
+ expect(findApiKey).not.toHaveBeenCalled();
381
+ expect(repositoryFind).not.toHaveBeenCalled();
382
+ expect(Response.sendEntityArrayResponse).not.toHaveBeenCalled();
383
+ expect(Response.sendErrorResponse).not.toHaveBeenCalled();
384
+ });
385
+
386
+ /*
387
+ * Which credential wins when the caller presents two.
388
+ *
389
+ * This is the one behaviour the presence-vs-usability change alters for
390
+ * callers who were NOT broken before: a request carrying a session token
391
+ * AND a blank `ApiKey` header used to ignore the blank header and
392
+ * authenticate by session; now the key branch claims it and it fails 400.
393
+ * That is the intended reading - a credential that was sent and is
394
+ * unusable is an error, not something to silently paper over with another
395
+ * credential, because the alternative is a caller whose key has quietly
396
+ * stopped being used and who has no way to find out.
397
+ *
398
+ * It is asserted rather than left implicit because it is the change most
399
+ * likely to be "fixed" back by someone who sees only half the picture
400
+ * (e.g. re-gating line ~303 of UserAuthorization on getApiKey, or on the
401
+ * absence of an access token). Both of those rewrites reach
402
+ * JSONWebToken.decode, which is exactly what this pins shut.
403
+ */
404
+ it("prefers the malformed key over a session token rather than silently falling back", async () => {
405
+ const outcome: RouteOutcome = await callGetList({
406
+ crudApiPath: LOG_PIPELINE_PATH,
407
+ headers: { apikey: "", authorization: "Bearer some.session.token" },
408
+ });
409
+
410
+ expect(outcome.error).toBeInstanceOf(BadDataException);
411
+ expect(outcome.error?.message).toBe("Invalid API Key");
412
+ expect(outcome.error?.code).toBe(400);
413
+
414
+ // The session branch was never entered, so no token was ever decoded.
415
+ expect(decodeAccessToken).not.toHaveBeenCalled();
416
+ expect(outcome.request.userType).not.toBe(UserType.Public);
417
+ expect(repositoryFind).not.toHaveBeenCalled();
418
+ });
419
+ });
420
+
421
+ describe("C) whitespace-only ApiKey header", () => {
422
+ /*
423
+ * `ApiKey: $ONEUPTIME_API_KEY` with the variable unset arrives as spaces,
424
+ * not as an absent header. Trimming makes it empty, so it is the same
425
+ * malformed-key failure as B - not the anonymous 401.
426
+ */
427
+ it("answers 400 Invalid API Key", async () => {
428
+ const outcome: RouteOutcome = await callGetList({
429
+ crudApiPath: LOG_PIPELINE_PATH,
430
+ headers: { apikey: " " },
431
+ });
432
+
433
+ expect(outcome.error).toBeInstanceOf(BadDataException);
434
+ expect(outcome.error?.message).toBe("Invalid API Key");
435
+ expect(outcome.error?.code).toBe(400);
436
+ expect(repositoryFind).not.toHaveBeenCalled();
437
+ });
438
+ });
439
+
440
+ describe("D) non-UUID ApiKey header", () => {
441
+ /*
442
+ * ApiKey.apiKey is a Postgres `uuid` column. A non-UUID value used to be
443
+ * handed straight to the lookup, which raised 22P02 "invalid input syntax
444
+ * for type uuid" as a raw QueryFailedError - not a OneUptime Exception, so
445
+ * it slipped past the error translator and answered 500 to what is really a
446
+ * malformed request. The shape is rejected before the query now, which is
447
+ * why findApiKey must not be called at all.
448
+ */
449
+ it("answers 400 Invalid API Key without ever querying the key table", async () => {
450
+ const outcome: RouteOutcome = await callGetList({
451
+ crudApiPath: LOG_PIPELINE_PATH,
452
+ headers: { apikey: "not-a-uuid" },
453
+ });
454
+
455
+ expect(outcome.error).toBeInstanceOf(BadDataException);
456
+ expect(outcome.error?.message).toBe("Invalid API Key");
457
+ expect(outcome.error?.code).toBe(400);
458
+ expect(findApiKey).not.toHaveBeenCalled();
459
+ expect(outcome.error?.code).not.toBe(500);
460
+ });
461
+
462
+ /*
463
+ * A caller that sets the header twice. Node normally collapses repeated
464
+ * headers into one comma-separated value, and IncomingHttpHeaders also
465
+ * types a header as string[]; the middleware handles both by joining
466
+ * before it validates. Either way it is never a usable key - but it is
467
+ * unmistakably someone authenticating by key, so it has to be claimed as a
468
+ * key problem rather than fall through to the anonymous 401.
469
+ */
470
+ it("treats a duplicated ApiKey header as a malformed key, not as an absent one", async () => {
471
+ const outcome: RouteOutcome = await callGetList({
472
+ crudApiPath: LOG_PIPELINE_PATH,
473
+ headers: { apikey: [KNOWN_API_KEY, UNKNOWN_API_KEY] },
474
+ });
475
+
476
+ expect(outcome.error).toBeInstanceOf(BadDataException);
477
+ expect(outcome.error?.message).toBe("Invalid API Key");
478
+ expect(outcome.error?.code).toBe(400);
479
+ expect(findApiKey).not.toHaveBeenCalled();
480
+ });
481
+ });
482
+
483
+ describe("E) well-formed but unknown API key", () => {
484
+ /*
485
+ * A key that is the right shape but is not in the table (revoked, expired,
486
+ * or from another install) and is not the instance master key. Same 400 as
487
+ * a malformed one: the API deliberately does not distinguish "wrong shape"
488
+ * from "unknown key" to a caller.
489
+ */
490
+ it("answers 400 Invalid API Key after the lookup misses", async () => {
491
+ const outcome: RouteOutcome = await callGetList({
492
+ crudApiPath: LOG_PIPELINE_PATH,
493
+ headers: { apikey: UNKNOWN_API_KEY },
494
+ });
495
+
496
+ expect(findApiKey).toHaveBeenCalledTimes(1);
497
+ expect(outcome.error).toBeInstanceOf(BadDataException);
498
+ expect(outcome.error?.message).toBe("Invalid API Key");
499
+ expect(outcome.error?.code).toBe(400);
500
+ expect(repositoryFind).not.toHaveBeenCalled();
501
+ });
502
+ });
503
+
504
+ describe("F) valid key holding a permission the model accepts", () => {
505
+ /*
506
+ * TelemetryAdmin is in LogPipeline's read list. The request must reach the
507
+ * handler and complete: this is the case the reporter believed was broken,
508
+ * and it is what proves the 401 they saw was about the header never
509
+ * arriving, not about the key's roles.
510
+ */
511
+ it("reaches the handler and returns the list", async () => {
512
+ grantApiKeyPermission(Permission.TelemetryAdmin);
513
+
514
+ const outcome: RouteOutcome = await callGetList({
515
+ crudApiPath: LOG_PIPELINE_PATH,
516
+ headers: { apikey: KNOWN_API_KEY },
517
+ });
518
+
519
+ expect(outcome.error).toBeNull();
520
+ expect(repositoryFind).toHaveBeenCalledTimes(1);
521
+ expect(Response.sendEntityArrayResponse).toHaveBeenCalledTimes(1);
522
+ });
523
+
524
+ /*
525
+ * The tenant is taken from the KEY, and the caller is typed as an API
526
+ * principal - that UserType.API is what lets the logged-in check in
527
+ * PublicPermission return early instead of throwing case A at a caller
528
+ * who has no userId and never will.
529
+ */
530
+ it("scopes the request to the key's own project and marks it an API caller", async () => {
531
+ grantApiKeyPermission(Permission.TelemetryAdmin);
532
+
533
+ const outcome: RouteOutcome = await callGetList({
534
+ crudApiPath: LOG_PIPELINE_PATH,
535
+ headers: { apikey: KNOWN_API_KEY },
536
+ });
537
+
538
+ expect(outcome.request.userType).toBe(UserType.API);
539
+ expect(outcome.request.tenantId?.toString()).toBe(PROJECT_ID.toString());
540
+ });
541
+
542
+ /*
543
+ * ...and never from a caller-supplied header. getUserMiddleware sets
544
+ * req.tenantId from the `tenantid` header before it has looked at the key
545
+ * at all, so the key's own projectId has to overwrite it - otherwise a key
546
+ * for project A would be evaluated against project B's id just because the
547
+ * caller said so, and the key's permissions (registered under its real
548
+ * project) would not be found for the claimed one.
549
+ */
550
+ it("ignores a tenantid header that points at a different project", async () => {
551
+ grantApiKeyPermission(Permission.TelemetryAdmin);
552
+
553
+ const outcome: RouteOutcome = await callGetList({
554
+ crudApiPath: LOG_PIPELINE_PATH,
555
+ headers: { apikey: KNOWN_API_KEY, tenantid: OTHER_PROJECT_ID },
556
+ });
557
+
558
+ expect(outcome.request.tenantId?.toString()).toBe(PROJECT_ID.toString());
559
+ expect(outcome.request.tenantId?.toString()).not.toBe(OTHER_PROJECT_ID);
560
+ expect(outcome.error).toBeNull();
561
+ expect(repositoryFind).toHaveBeenCalledTimes(1);
562
+ });
563
+
564
+ /*
565
+ * A padded value is a usable key, not a malformed one - trimming happens
566
+ * before validation, so a header that picked up whitespace still works.
567
+ * The lookup has to receive the TRIMMED value: the padded string is not a
568
+ * valid uuid literal, so passing it through would raise 22P02 in the query
569
+ * layer and answer 500 to a request that was only untidy.
570
+ */
571
+ it("trims a key padded with whitespace instead of querying the padded value", async () => {
572
+ grantApiKeyPermission(Permission.TelemetryAdmin);
573
+
574
+ const outcome: RouteOutcome = await callGetList({
575
+ crudApiPath: LOG_PIPELINE_PATH,
576
+ headers: { apikey: ` ${KNOWN_API_KEY} ` },
577
+ });
578
+
579
+ expect(findApiKey).toHaveBeenCalledWith(new ObjectID(KNOWN_API_KEY));
580
+ expect(outcome.error).toBeNull();
581
+ expect(Response.sendEntityArrayResponse).toHaveBeenCalledTimes(1);
582
+ });
583
+ });
584
+
585
+ describe("G) valid key holding ProjectAdmin", () => {
586
+ // A second accepted permission, so F is not passing off one lucky enum.
587
+ it("reaches the handler and returns the list", async () => {
588
+ grantApiKeyPermission(Permission.ProjectAdmin);
589
+
590
+ const outcome: RouteOutcome = await callGetList({
591
+ crudApiPath: LOG_PIPELINE_PATH,
592
+ headers: { apikey: KNOWN_API_KEY },
593
+ });
594
+
595
+ expect(outcome.error).toBeNull();
596
+ expect(repositoryFind).toHaveBeenCalledTimes(1);
597
+ expect(Response.sendEntityArrayResponse).toHaveBeenCalledTimes(1);
598
+ });
599
+ });
600
+
601
+ describe("H) valid key holding an unrelated permission", () => {
602
+ /*
603
+ * This is what a real per-model RBAC failure looks like, and it is nothing
604
+ * like what #3004 saw: 422, not 401; it names the operation and the model
605
+ * AND lists the permissions that would have worked, so the key's owner can
606
+ * fix it. If the reported bug had been what it appeared to be, this is the
607
+ * response they would have received.
608
+ */
609
+ it("answers 422 listing the permissions that would have worked", async () => {
610
+ grantApiKeyPermission(Permission.CreateProjectApiKey);
611
+
612
+ const outcome: RouteOutcome = await callGetList({
613
+ crudApiPath: LOG_PIPELINE_PATH,
614
+ headers: { apikey: KNOWN_API_KEY },
615
+ });
616
+
617
+ expect(outcome.error).toBeInstanceOf(NotAuthorizedException);
618
+ expect(outcome.error?.code).toBe(422);
619
+ expect(outcome.error?.message).toContain(
620
+ "You do not have permissions to read Log Pipeline",
621
+ );
622
+ expect(outcome.error?.message).toContain(
623
+ "You need one of these permissions:",
624
+ );
625
+ expect(outcome.error?.message).toContain("Telemetry Admin");
626
+ expect(Response.sendEntityArrayResponse).not.toHaveBeenCalled();
627
+ });
628
+
629
+ /*
630
+ * An authenticated key that lacks a role is never reported as
631
+ * unauthenticated. The request must still be refused - asserting only the
632
+ * two negatives would pass if the read had been allowed through, so the
633
+ * refusal itself is asserted first.
634
+ */
635
+ it("does not answer the anonymous 401 for an authenticated key", async () => {
636
+ grantApiKeyPermission(Permission.CreateProjectApiKey);
637
+
638
+ const outcome: RouteOutcome = await callGetList({
639
+ crudApiPath: LOG_PIPELINE_PATH,
640
+ headers: { apikey: KNOWN_API_KEY },
641
+ });
642
+
643
+ expect(outcome.error).not.toBeNull();
644
+ expect(repositoryFind).not.toHaveBeenCalled();
645
+ expect(outcome.error).not.toBeInstanceOf(NotAuthenticatedException);
646
+ expect(outcome.error?.code).not.toBe(401);
647
+ expect(outcome.error?.message).not.toContain("needed to read record of");
648
+ });
649
+ });
650
+
651
+ /*
652
+ * I) CONTROL.
653
+ *
654
+ * The issue premise was that the fault was "specific to the Log Pipeline CRUD
655
+ * authentication path". Monitor is registered through the same BaseAPI in the
656
+ * same way and behaves identically - same exception, same status, same
657
+ * wording apart from the model name. One run of this block refutes that
658
+ * premise: nothing above belongs to Log Pipeline.
659
+ */
660
+ describe("I) control: a different model registered the same way", () => {
661
+ it("answers the same 401 with no ApiKey header, differing only in the model name", async () => {
662
+ const logPipeline: RouteOutcome = await callGetList({
663
+ crudApiPath: LOG_PIPELINE_PATH,
664
+ headers: {},
665
+ });
666
+
667
+ const monitor: RouteOutcome = await callGetList({
668
+ crudApiPath: MONITOR_PATH,
669
+ headers: {},
670
+ });
671
+
672
+ expect(monitor.error).toBeInstanceOf(NotAuthenticatedException);
673
+ expect(monitor.error?.code).toBe(401);
674
+ expect(monitor.error?.message).toBe(
675
+ "Authenticated user or a valid API key is needed to read record of Monitor.",
676
+ );
677
+
678
+ expect(monitor.error?.message.replace("Monitor", "Log Pipeline")).toBe(
679
+ logPipeline.error?.message,
680
+ );
681
+ expect(monitor.error?.code).toBe(logPipeline.error?.code);
682
+ });
683
+
684
+ it("answers the same 400 Invalid API Key for an empty ApiKey header", async () => {
685
+ const logPipeline: RouteOutcome = await callGetList({
686
+ crudApiPath: LOG_PIPELINE_PATH,
687
+ headers: { apikey: "" },
688
+ });
689
+
690
+ const monitor: RouteOutcome = await callGetList({
691
+ crudApiPath: MONITOR_PATH,
692
+ headers: { apikey: "" },
693
+ });
694
+
695
+ expect(monitor.error).toBeInstanceOf(BadDataException);
696
+ expect(monitor.error?.code).toBe(400);
697
+ expect(monitor.error?.message).toBe("Invalid API Key");
698
+
699
+ // Byte for byte the same answer, because the model plays no part in it.
700
+ expect(monitor.error?.message).toBe(logPipeline.error?.message);
701
+ expect(monitor.error?.code).toBe(logPipeline.error?.code);
702
+ });
703
+ });
704
+ });