@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.
- package/Models/DatabaseModels/Host.ts +12 -4
- package/Models/DatabaseModels/KubernetesContainer.ts +5 -4
- package/Models/DatabaseModels/KubernetesResource.ts +8 -7
- package/Models/DatabaseModels/PodmanResource.ts +6 -6
- package/Models/DatabaseModels/ServiceLevelObjective.ts +91 -0
- package/Models/DatabaseModels/StatusPage.ts +95 -0
- package/Server/API/TeamMemberAPI.ts +85 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.ts +84 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.ts +131 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Middleware/ProjectAuthorization.ts +66 -4
- package/Server/Services/BillingService.ts +600 -51
- package/Server/Services/CephClusterService.ts +47 -5
- package/Server/Services/CephResourceService.ts +43 -4
- package/Server/Services/CloudResourceService.ts +47 -5
- package/Server/Services/DatabaseService.ts +87 -6
- package/Server/Services/DockerHostService.ts +47 -5
- package/Server/Services/DockerResourceService.ts +62 -8
- package/Server/Services/DockerSwarmClusterService.ts +48 -5
- package/Server/Services/DockerSwarmResourceService.ts +53 -4
- package/Server/Services/HostService.ts +48 -5
- package/Server/Services/IoTDeviceService.ts +1 -17
- package/Server/Services/IoTFleetService.ts +47 -5
- package/Server/Services/KubernetesClusterService.ts +48 -5
- package/Server/Services/KubernetesContainerService.ts +50 -9
- package/Server/Services/KubernetesResourceService.ts +53 -7
- package/Server/Services/LabelService.ts +106 -1
- package/Server/Services/MonitorService.ts +57 -0
- package/Server/Services/PodmanHostService.ts +47 -5
- package/Server/Services/PodmanResourceService.ts +62 -8
- package/Server/Services/ProjectService.ts +165 -35
- package/Server/Services/ProxmoxClusterService.ts +47 -5
- package/Server/Services/ProxmoxResourceService.ts +43 -4
- package/Server/Services/RumApplicationService.ts +48 -5
- package/Server/Services/ServerlessFunctionService.ts +48 -5
- package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +426 -0
- package/Server/Services/ServiceLevelObjectiveService.ts +39 -0
- package/Server/Services/StatusPageService.ts +95 -36
- package/Server/Services/StatusPageSubscriberNotificationTemplateService.ts +18 -1
- package/Server/Services/StatusPageSubscriberService.ts +3 -0
- package/Server/Types/Database/JSONColumnQuery.ts +621 -0
- package/Server/Types/Database/Permissions/PublicPermission.ts +9 -2
- package/Server/Types/Database/QueryHelper.ts +17 -32
- package/Server/Utils/Database/TruncateColumnValue.ts +84 -0
- package/Tests/App/Dashboard/UsersTableGroupsByPerson.test.tsx +448 -0
- package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +704 -0
- package/Tests/Server/API/TeamMemberRemoveUserFromProjectAPI.test.ts +303 -0
- package/Tests/Server/Middleware/MasterAdminAuthorization.test.ts +350 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyHeader.test.ts +362 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +687 -0
- package/Tests/Server/Services/BillingService.test.ts +78 -3
- package/Tests/Server/Services/BillingServiceChangePlanCancel.test.ts +645 -0
- package/Tests/Server/Services/BillingServiceTrialPlanChange.test.ts +1005 -0
- package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +503 -0
- package/Tests/Server/Services/HostIpAddressesColumnWidth.test.ts +198 -0
- package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +510 -0
- package/Tests/Server/Services/InventoryResourceNameColumnWidth.test.ts +500 -0
- package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +586 -0
- package/Tests/Server/Services/InventoryUpsertTruncationGuard.test.ts +1242 -0
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +650 -0
- package/Tests/Server/Services/ProjectServiceExtendTrial.test.ts +1 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +387 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +770 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +130 -0
- package/Tests/Server/Services/SloMonitorLabelRuleHooks.test.ts +352 -0
- package/Tests/Server/Services/StatusPageSubscriberReport.test.ts +29 -13
- package/Tests/Server/TestingUtils/Services/BillingServiceHelper.ts +69 -8
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +593 -0
- package/Tests/Server/Types/Database/Permissions/PublicPermission.test.ts +495 -0
- package/Tests/Server/Types/Database/QueryUtil.test.ts +113 -0
- package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
- package/Tests/Types/Events/Recurring.test.ts +157 -0
- package/Tests/UI/Components/ModelTable/ModelTableGroupsProjectUsers.test.tsx +375 -0
- package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +196 -0
- package/Tests/UI/Utils/ProjectUsersModelAPI.test.ts +739 -0
- package/Tests/UI/Utils/TeamMembersByUser.test.ts +633 -0
- package/Tests/Utils/StatusPage/ReportPeriod.test.ts +218 -0
- package/Tests/Utils/Telemetry/HostIpAddresses.test.ts +291 -0
- package/Types/CustomField/CustomFieldDefinition.ts +24 -0
- package/Types/Date.ts +112 -0
- package/Types/Events/Recurring.ts +99 -1
- package/Types/StatusPage/StatusPageReport.ts +11 -0
- package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +2 -6
- package/UI/Components/ModelTable/useCustomFieldColumns.ts +21 -2
- package/UI/Utils/ModelAPI/ProjectUsersModelAPI.ts +305 -0
- package/UI/Utils/TeamMembersByUser.ts +302 -0
- package/Utils/StatusPage/ReportPeriod.ts +236 -0
- package/Utils/Telemetry/HostIpAddresses.ts +98 -0
- package/build/dist/Models/DatabaseModels/Host.js +12 -4
- package/build/dist/Models/DatabaseModels/Host.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesContainer.js +4 -4
- package/build/dist/Models/DatabaseModels/KubernetesContainer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/KubernetesResource.js +7 -7
- package/build/dist/Models/DatabaseModels/KubernetesResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/PodmanResource.js +6 -6
- package/build/dist/Models/DatabaseModels/PodmanResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js +87 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjective.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +98 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/TeamMemberAPI.js +57 -2
- package/build/dist/Server/API/TeamMemberAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js +45 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785900000000-AddSloMonitorLabelRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js +31 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785915638551-IncreaseHostIpAddressesLength.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js +90 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930000000-WidenInventoryResourceNameColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785930709405-AddStatusPageReportPeriod.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js +59 -4
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/BillingService.js +466 -36
- package/build/dist/Server/Services/BillingService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +42 -5
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/CephResourceService.js +28 -3
- package/build/dist/Server/Services/CephResourceService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceService.js +42 -5
- package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +69 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +42 -5
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerResourceService.js +34 -4
- package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +43 -5
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmResourceService.js +28 -3
- package/build/dist/Server/Services/DockerSwarmResourceService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +43 -5
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceService.js +1 -8
- package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetService.js +42 -5
- package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +43 -5
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesContainerService.js +29 -3
- package/build/dist/Server/Services/KubernetesContainerService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesResourceService.js +32 -4
- package/build/dist/Server/Services/KubernetesResourceService.js.map +1 -1
- package/build/dist/Server/Services/LabelService.js +92 -0
- package/build/dist/Server/Services/LabelService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +47 -0
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +42 -5
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/PodmanResourceService.js +34 -4
- package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +135 -21
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +42 -5
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxResourceService.js +28 -3
- package/build/dist/Server/Services/ProxmoxResourceService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationService.js +43 -5
- package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionService.js +43 -5
- package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +347 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +33 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +69 -46
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js +17 -1
- package/build/dist/Server/Services/StatusPageSubscriberNotificationTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +3 -0
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +402 -0
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -0
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js +9 -2
- package/build/dist/Server/Types/Database/Permissions/PublicPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +17 -27
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js +32 -0
- package/build/dist/Server/Utils/Database/TruncateColumnValue.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldDefinition.js +2 -0
- package/build/dist/Types/CustomField/CustomFieldDefinition.js.map +1 -0
- package/build/dist/Types/Date.js +68 -0
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Events/Recurring.js +63 -0
- package/build/dist/Types/Events/Recurring.js.map +1 -1
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +18 -2
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js +213 -0
- package/build/dist/UI/Utils/ModelAPI/ProjectUsersModelAPI.js.map +1 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js +195 -0
- package/build/dist/UI/Utils/TeamMembersByUser.js.map +1 -0
- package/build/dist/Utils/StatusPage/ReportPeriod.js +131 -0
- package/build/dist/Utils/StatusPage/ReportPeriod.js.map +1 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js +82 -0
- package/build/dist/Utils/Telemetry/HostIpAddresses.js.map +1 -0
- package/jest.config.json +2 -0
- package/package.json +1 -1
- package/tsconfig.json +12 -1
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import ModelPermission from "../../../../../Server/Types/AnalyticsDatabase/ModelPermission";
|
|
2
|
+
import DatabaseRequestType from "../../../../../Server/Types/BaseDatabase/DatabaseRequestType";
|
|
3
|
+
import PublicPermission from "../../../../../Server/Types/Database/Permissions/PublicPermission";
|
|
4
|
+
import Log from "../../../../../Models/AnalyticsModels/Log";
|
|
5
|
+
import LogPipeline from "../../../../../Models/DatabaseModels/LogPipeline";
|
|
6
|
+
import Probe from "../../../../../Models/DatabaseModels/Probe";
|
|
7
|
+
import StatusPageSubscriber from "../../../../../Models/DatabaseModels/StatusPageSubscriber";
|
|
8
|
+
import DatabaseCommonInteractionProps from "../../../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
9
|
+
import Exception from "../../../../../Types/Exception/Exception";
|
|
10
|
+
import ExceptionCode from "../../../../../Types/Exception/ExceptionCode";
|
|
11
|
+
import NotAuthenticatedException from "../../../../../Types/Exception/NotAuthenticatedException";
|
|
12
|
+
import ObjectID from "../../../../../Types/ObjectID";
|
|
13
|
+
import Permission from "../../../../../Types/Permission";
|
|
14
|
+
import UserType from "../../../../../Types/UserType";
|
|
15
|
+
import { describe, expect, it, jest } from "@jest/globals";
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* Every throw below is a deliberate, asserted-on throw, but @CaptureSpan on
|
|
19
|
+
* these methods hands each one to Telemetry, which logs the whole stack via
|
|
20
|
+
* Server/Utils/Logger. Auto-mocking Logger (the same thing
|
|
21
|
+
* Tests/Server/Middleware/UserAuthorization.test.ts does) keeps the run
|
|
22
|
+
* readable. Nothing under test is mocked: PublicPermission,
|
|
23
|
+
* AnalyticsDatabase/ModelPermission, TablePermission and the real models all
|
|
24
|
+
* run for real.
|
|
25
|
+
*/
|
|
26
|
+
jest.mock("../../../../../Server/Utils/Logger");
|
|
27
|
+
|
|
28
|
+
/*
|
|
29
|
+
* PublicPermission is the very first gate every Postgres-backed read/write
|
|
30
|
+
* passes through, and until now nothing tested it directly — the only test
|
|
31
|
+
* that touched checkIfUserIsLoggedIn (BasePermission.test.ts) stubs it out.
|
|
32
|
+
*
|
|
33
|
+
* It is also the function that produced the 401 in GitHub issue #3004: a
|
|
34
|
+
* caller with a perfectly good project API key was told "A user should be
|
|
35
|
+
* logged in to read record of Log Pipeline." Their key had never reached the
|
|
36
|
+
* server, but the sentence talked only about sessions and named the model, so
|
|
37
|
+
* it read as an RBAC bug on Log Pipeline specifically. Issue #1754 was the
|
|
38
|
+
* same sentence on a different model (axios dropping the headers object).
|
|
39
|
+
* The wording is therefore load-bearing, and so is the API early return the
|
|
40
|
+
* reporter believed was broken. Both are pinned here.
|
|
41
|
+
*/
|
|
42
|
+
describe("PublicPermission", () => {
|
|
43
|
+
const userId: ObjectID = ObjectID.generate();
|
|
44
|
+
const projectId: ObjectID = ObjectID.generate();
|
|
45
|
+
|
|
46
|
+
const allRequestTypes: Array<DatabaseRequestType> = [
|
|
47
|
+
DatabaseRequestType.Create,
|
|
48
|
+
DatabaseRequestType.Read,
|
|
49
|
+
DatabaseRequestType.Update,
|
|
50
|
+
DatabaseRequestType.Delete,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// A caller carrying neither a session nor an API key.
|
|
54
|
+
function anonymousProps(): DatabaseCommonInteractionProps {
|
|
55
|
+
return {
|
|
56
|
+
tenantId: projectId,
|
|
57
|
+
userType: UserType.Public,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
* A caller authenticated by API key. This is exactly what
|
|
63
|
+
* ProjectAuthorization.isValidProjectIdAndApiKeyMiddleware builds once it
|
|
64
|
+
* resolves a key: userType = API, tenantId from the key's project, and NO
|
|
65
|
+
* userId, because an API key is not a user.
|
|
66
|
+
*/
|
|
67
|
+
function apiKeyProps(): DatabaseCommonInteractionProps {
|
|
68
|
+
return {
|
|
69
|
+
tenantId: projectId,
|
|
70
|
+
userType: UserType.API,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Runs the check and returns the message it threw with.
|
|
75
|
+
function messageFromCheck(runCheck: () => void): string {
|
|
76
|
+
try {
|
|
77
|
+
runCheck();
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return (err as Exception).message;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
throw new Error(
|
|
83
|
+
"Expected checkIfUserIsLoggedIn to throw, but it returned normally.",
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("isPublicPermissionAllowed", () => {
|
|
88
|
+
/*
|
|
89
|
+
* LogPipeline is the issue-3004 model. Its read list is project-scoped
|
|
90
|
+
* only — no Permission.Public — which is why an anonymous read of it
|
|
91
|
+
* reaches the throw below rather than being waved through.
|
|
92
|
+
*/
|
|
93
|
+
it("is false for a model whose read list has no Permission.Public", () => {
|
|
94
|
+
/*
|
|
95
|
+
* Stated against the model config as well as the function, so that if
|
|
96
|
+
* someone ever adds Permission.Public to LogPipeline the failure here
|
|
97
|
+
* reads as "the fixture changed" rather than "the function broke" —
|
|
98
|
+
* that would be a real security change and this file would need
|
|
99
|
+
* updating, not the production code.
|
|
100
|
+
*/
|
|
101
|
+
expect(new LogPipeline().readRecordPermissions).not.toContain(
|
|
102
|
+
Permission.Public,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
expect(
|
|
106
|
+
PublicPermission.isPublicPermissionAllowed(
|
|
107
|
+
LogPipeline,
|
|
108
|
+
DatabaseRequestType.Read,
|
|
109
|
+
),
|
|
110
|
+
).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/*
|
|
114
|
+
* Probe genuinely carries Permission.Public in its TableAccessControl
|
|
115
|
+
* read list (status pages render probe names to logged-out visitors).
|
|
116
|
+
*/
|
|
117
|
+
it("is true for a model whose read list contains Permission.Public", () => {
|
|
118
|
+
expect(new Probe().readRecordPermissions).toContain(Permission.Public);
|
|
119
|
+
|
|
120
|
+
expect(
|
|
121
|
+
PublicPermission.isPublicPermissionAllowed(
|
|
122
|
+
Probe,
|
|
123
|
+
DatabaseRequestType.Read,
|
|
124
|
+
),
|
|
125
|
+
).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/*
|
|
129
|
+
* The lookup is per-operation, not per-model. StatusPageSubscriber is the
|
|
130
|
+
* clearest case in the codebase: anyone may subscribe to a status page
|
|
131
|
+
* (Public in `create`) but the subscriber list itself is not public
|
|
132
|
+
* (no Public in `read`). A model-level answer here would leak the list.
|
|
133
|
+
*/
|
|
134
|
+
it("answers per operation, not per model", () => {
|
|
135
|
+
expect(
|
|
136
|
+
PublicPermission.isPublicPermissionAllowed(
|
|
137
|
+
StatusPageSubscriber,
|
|
138
|
+
DatabaseRequestType.Create,
|
|
139
|
+
),
|
|
140
|
+
).toBe(true);
|
|
141
|
+
|
|
142
|
+
expect(
|
|
143
|
+
PublicPermission.isPublicPermissionAllowed(
|
|
144
|
+
StatusPageSubscriber,
|
|
145
|
+
DatabaseRequestType.Read,
|
|
146
|
+
),
|
|
147
|
+
).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("is false for every non-public operation of the issue-3004 model", () => {
|
|
151
|
+
for (const type of allRequestTypes) {
|
|
152
|
+
expect(
|
|
153
|
+
PublicPermission.isPublicPermissionAllowed(LogPipeline, type),
|
|
154
|
+
).toBe(false);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("checkIfUserIsLoggedIn", () => {
|
|
160
|
+
/*
|
|
161
|
+
* ===== THE MESSAGE =====
|
|
162
|
+
*
|
|
163
|
+
* This is the sentence the reporter of #3004 saw. The old wording — "A
|
|
164
|
+
* user should be logged in to read record of Log Pipeline." — described
|
|
165
|
+
* only one of the two ways to authenticate and named the model, so two
|
|
166
|
+
* separate reporters (#3004 and #1754) spent their time auditing that
|
|
167
|
+
* model's RBAC config when the real cause was an API key that never
|
|
168
|
+
* arrived. The replacement names both credentials. If anyone shortens it
|
|
169
|
+
* back to a login-only phrasing, this fails.
|
|
170
|
+
*/
|
|
171
|
+
it("names both a session and an API key in the anonymous-read message", () => {
|
|
172
|
+
const message: string = messageFromCheck(() => {
|
|
173
|
+
PublicPermission.checkIfUserIsLoggedIn(
|
|
174
|
+
LogPipeline,
|
|
175
|
+
anonymousProps(),
|
|
176
|
+
DatabaseRequestType.Read,
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
expect(message).toBe(
|
|
181
|
+
"Authenticated user or a valid API key is needed to read record of Log Pipeline.",
|
|
182
|
+
);
|
|
183
|
+
expect(message).not.toContain("should be logged in");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
/*
|
|
187
|
+
* ===== THE API EARLY RETURN =====
|
|
188
|
+
*
|
|
189
|
+
* This is the invariant #3004 claimed was broken: a request authenticated
|
|
190
|
+
* by API key has userType API and NO userId, and must sail straight past
|
|
191
|
+
* this gate for every operation even on a model that is not public. If
|
|
192
|
+
* this ever regressed, every single API-key call in the product would 401
|
|
193
|
+
* with the sentence above, and the report would look exactly like #3004.
|
|
194
|
+
*
|
|
195
|
+
* The early return sits inside the not-public/no-userId branch and is
|
|
196
|
+
* keyed on `props.userType === UserType.API`; no userId is consulted,
|
|
197
|
+
* which is exactly the props shape ProjectAuthorization produces once it
|
|
198
|
+
* resolves a key.
|
|
199
|
+
*/
|
|
200
|
+
it("lets an API-key caller with no userId through for every operation", () => {
|
|
201
|
+
for (const type of allRequestTypes) {
|
|
202
|
+
const props: DatabaseCommonInteractionProps = apiKeyProps();
|
|
203
|
+
expect(props.userId).toBeUndefined();
|
|
204
|
+
|
|
205
|
+
expect(() => {
|
|
206
|
+
PublicPermission.checkIfUserIsLoggedIn(LogPipeline, props, type);
|
|
207
|
+
}).not.toThrow();
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("lets a logged-in user through for every operation", () => {
|
|
212
|
+
for (const type of allRequestTypes) {
|
|
213
|
+
expect(() => {
|
|
214
|
+
PublicPermission.checkIfUserIsLoggedIn(
|
|
215
|
+
LogPipeline,
|
|
216
|
+
{
|
|
217
|
+
userId: userId,
|
|
218
|
+
tenantId: projectId,
|
|
219
|
+
userType: UserType.User,
|
|
220
|
+
},
|
|
221
|
+
type,
|
|
222
|
+
);
|
|
223
|
+
}).not.toThrow();
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
/*
|
|
228
|
+
* The gate has exactly two halves — `!isPublicPermissionAllowed` and
|
|
229
|
+
* `!props.userId` — and it is the *presence of a userId* that lifts it,
|
|
230
|
+
* not the userType label. A session established before userType was
|
|
231
|
+
* stamped on the props (SSO callback, older cached tokens) still carries
|
|
232
|
+
* a userId, and must not be treated as anonymous. If the condition were
|
|
233
|
+
* ever rewritten as `props.userType !== UserType.User`, this fails.
|
|
234
|
+
*/
|
|
235
|
+
it("lets any caller carrying a userId through, whatever the userType says", () => {
|
|
236
|
+
expect(() => {
|
|
237
|
+
PublicPermission.checkIfUserIsLoggedIn(
|
|
238
|
+
LogPipeline,
|
|
239
|
+
{
|
|
240
|
+
userId: userId,
|
|
241
|
+
tenantId: projectId,
|
|
242
|
+
userType: UserType.Public,
|
|
243
|
+
},
|
|
244
|
+
DatabaseRequestType.Read,
|
|
245
|
+
);
|
|
246
|
+
}).not.toThrow();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
/*
|
|
250
|
+
* The 401 has to stay a 401. Response.sendErrorResponse reads
|
|
251
|
+
* exception.code straight through to the HTTP status, so a wrong
|
|
252
|
+
* exception class here would silently change the status callers see and
|
|
253
|
+
* break client retry/re-auth logic.
|
|
254
|
+
*/
|
|
255
|
+
it("throws NotAuthenticatedException carrying status 401 for an anonymous caller", () => {
|
|
256
|
+
let thrown: Exception | null = null;
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
PublicPermission.checkIfUserIsLoggedIn(
|
|
260
|
+
LogPipeline,
|
|
261
|
+
anonymousProps(),
|
|
262
|
+
DatabaseRequestType.Read,
|
|
263
|
+
);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
thrown = err as Exception;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
expect(thrown).toBeInstanceOf(NotAuthenticatedException);
|
|
269
|
+
expect(thrown?.code).toBe(ExceptionCode.NotAuthenticatedException);
|
|
270
|
+
expect(thrown?.code).toBe(401);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
/*
|
|
274
|
+
* The operation verb is interpolated from DatabaseRequestType and the
|
|
275
|
+
* model name from singularName. Support triages these reports by the
|
|
276
|
+
* sentence, so both halves have to be the real values — "Log Pipeline"
|
|
277
|
+
* with its space, not the class name "LogPipeline".
|
|
278
|
+
*/
|
|
279
|
+
it("interpolates the operation and the model's singular name", () => {
|
|
280
|
+
const expectedByType: Array<[DatabaseRequestType, string]> = [
|
|
281
|
+
[
|
|
282
|
+
DatabaseRequestType.Create,
|
|
283
|
+
"Authenticated user or a valid API key is needed to create record of Log Pipeline.",
|
|
284
|
+
],
|
|
285
|
+
[
|
|
286
|
+
DatabaseRequestType.Read,
|
|
287
|
+
"Authenticated user or a valid API key is needed to read record of Log Pipeline.",
|
|
288
|
+
],
|
|
289
|
+
[
|
|
290
|
+
DatabaseRequestType.Update,
|
|
291
|
+
"Authenticated user or a valid API key is needed to update record of Log Pipeline.",
|
|
292
|
+
],
|
|
293
|
+
[
|
|
294
|
+
DatabaseRequestType.Delete,
|
|
295
|
+
"Authenticated user or a valid API key is needed to delete record of Log Pipeline.",
|
|
296
|
+
],
|
|
297
|
+
];
|
|
298
|
+
|
|
299
|
+
for (const [type, expected] of expectedByType) {
|
|
300
|
+
const message: string = messageFromCheck(() => {
|
|
301
|
+
PublicPermission.checkIfUserIsLoggedIn(
|
|
302
|
+
LogPipeline,
|
|
303
|
+
anonymousProps(),
|
|
304
|
+
type,
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(message).toBe(expected);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// The human-readable name, not the class name.
|
|
312
|
+
expect(new LogPipeline().singularName).toBe("Log Pipeline");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
/*
|
|
316
|
+
* A model that IS public for the operation lets an anonymous caller
|
|
317
|
+
* through — that is what keeps logged-out status pages working — but only
|
|
318
|
+
* for that operation. Probe is public to read and not public to create.
|
|
319
|
+
*/
|
|
320
|
+
it("lets an anonymous caller read a publicly-readable model but not create it", () => {
|
|
321
|
+
expect(() => {
|
|
322
|
+
PublicPermission.checkIfUserIsLoggedIn(
|
|
323
|
+
Probe,
|
|
324
|
+
anonymousProps(),
|
|
325
|
+
DatabaseRequestType.Read,
|
|
326
|
+
);
|
|
327
|
+
}).not.toThrow();
|
|
328
|
+
|
|
329
|
+
expect(() => {
|
|
330
|
+
PublicPermission.checkIfUserIsLoggedIn(
|
|
331
|
+
Probe,
|
|
332
|
+
anonymousProps(),
|
|
333
|
+
DatabaseRequestType.Create,
|
|
334
|
+
);
|
|
335
|
+
}).toThrow(NotAuthenticatedException);
|
|
336
|
+
|
|
337
|
+
/*
|
|
338
|
+
* Every other message assertion in this file uses LogPipeline, so this
|
|
339
|
+
* is the one that proves the sentence is built from the modelType
|
|
340
|
+
* argument — `new modelType().singularName` — rather than from a name
|
|
341
|
+
* captured anywhere else.
|
|
342
|
+
*/
|
|
343
|
+
const message: string = messageFromCheck(() => {
|
|
344
|
+
PublicPermission.checkIfUserIsLoggedIn(
|
|
345
|
+
Probe,
|
|
346
|
+
anonymousProps(),
|
|
347
|
+
DatabaseRequestType.Create,
|
|
348
|
+
);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
expect(message).toBe(
|
|
352
|
+
"Authenticated user or a valid API key is needed to create record of Probe.",
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
/*
|
|
357
|
+
* An entirely absent userType behaves the same as UserType.Public: only
|
|
358
|
+
* UserType.API short-circuits. Callers that forget to set userType must
|
|
359
|
+
* not accidentally get the API bypass.
|
|
360
|
+
*/
|
|
361
|
+
it("treats props with no userType as anonymous", () => {
|
|
362
|
+
expect(() => {
|
|
363
|
+
PublicPermission.checkIfUserIsLoggedIn(
|
|
364
|
+
LogPipeline,
|
|
365
|
+
{ tenantId: projectId },
|
|
366
|
+
DatabaseRequestType.Read,
|
|
367
|
+
);
|
|
368
|
+
}).toThrow(NotAuthenticatedException);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
/*
|
|
372
|
+
* ===== MasterAdmin =====
|
|
373
|
+
*
|
|
374
|
+
* Documenting what the code actually does rather than what one might
|
|
375
|
+
* assume: the early return checks `props.userType === UserType.API` and
|
|
376
|
+
* nothing else, so a MasterAdmin (or an isMasterAdmin flag) with no
|
|
377
|
+
* userId is NOT waved through here — it throws the same 401. In practice
|
|
378
|
+
* the master-admin path always carries a userId, and the actual
|
|
379
|
+
* master-key bypass lives upstream in the middleware, not in this gate.
|
|
380
|
+
* If someone later adds a MasterAdmin short-circuit here, this test will
|
|
381
|
+
* fail and force the decision to be explicit.
|
|
382
|
+
*/
|
|
383
|
+
it("does not grant MasterAdmin an early return when there is no userId", () => {
|
|
384
|
+
expect(() => {
|
|
385
|
+
PublicPermission.checkIfUserIsLoggedIn(
|
|
386
|
+
LogPipeline,
|
|
387
|
+
{
|
|
388
|
+
tenantId: projectId,
|
|
389
|
+
userType: UserType.MasterAdmin,
|
|
390
|
+
isMasterAdmin: true,
|
|
391
|
+
},
|
|
392
|
+
DatabaseRequestType.Read,
|
|
393
|
+
);
|
|
394
|
+
}).toThrow(NotAuthenticatedException);
|
|
395
|
+
|
|
396
|
+
// ...but a MasterAdmin with a session passes, like any logged-in user.
|
|
397
|
+
expect(() => {
|
|
398
|
+
PublicPermission.checkIfUserIsLoggedIn(
|
|
399
|
+
LogPipeline,
|
|
400
|
+
{
|
|
401
|
+
userId: userId,
|
|
402
|
+
tenantId: projectId,
|
|
403
|
+
userType: UserType.MasterAdmin,
|
|
404
|
+
isMasterAdmin: true,
|
|
405
|
+
},
|
|
406
|
+
DatabaseRequestType.Read,
|
|
407
|
+
);
|
|
408
|
+
}).not.toThrow();
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
/*
|
|
413
|
+
* ===== DRIFT GUARD: Postgres message vs analytics message =====
|
|
414
|
+
*
|
|
415
|
+
* There are two copies of this check — PublicPermission (Postgres tables)
|
|
416
|
+
* and AnalyticsDatabase/ModelPermission (ClickHouse tables). They are meant
|
|
417
|
+
* to be the same gate, and a caller has no idea which storage engine backs
|
|
418
|
+
* the model they just called. The two sentences silently diverged for
|
|
419
|
+
* years: the analytics side already said "Authenticated user or a valid API
|
|
420
|
+
* key is needed to ...", while the Postgres side still said "A user should
|
|
421
|
+
* be logged in to ...". Searching the codebase for the string a user
|
|
422
|
+
* pasted therefore turned up only one of the two call sites, which is a
|
|
423
|
+
* large part of why #3004 was hard to diagnose.
|
|
424
|
+
*
|
|
425
|
+
* This drives both real functions with equivalent props and compares the
|
|
426
|
+
* sentences they actually produce, rather than grepping the source, so it
|
|
427
|
+
* still catches a divergence introduced by refactoring either one.
|
|
428
|
+
*/
|
|
429
|
+
describe("message parity with the analytics-side checkIfUserIsLoggedIn", () => {
|
|
430
|
+
// Replace the only part that legitimately differs: the model's own name.
|
|
431
|
+
function sentenceTemplate(message: string, singularName: string): string {
|
|
432
|
+
return message.replace(
|
|
433
|
+
`record of ${singularName}.`,
|
|
434
|
+
"record of <singularName>.",
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
it("produces the same sentence on both storage engines for every operation", () => {
|
|
439
|
+
const postgresSingularName: string = new LogPipeline().singularName!;
|
|
440
|
+
const analyticsSingularName: string = new Log().singularName!;
|
|
441
|
+
|
|
442
|
+
// Different names, so the comparison below is not trivially true.
|
|
443
|
+
expect(postgresSingularName).not.toBe(analyticsSingularName);
|
|
444
|
+
|
|
445
|
+
for (const type of allRequestTypes) {
|
|
446
|
+
const postgresMessage: string = messageFromCheck(() => {
|
|
447
|
+
PublicPermission.checkIfUserIsLoggedIn(
|
|
448
|
+
LogPipeline,
|
|
449
|
+
anonymousProps(),
|
|
450
|
+
type,
|
|
451
|
+
);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const analyticsMessage: string = messageFromCheck(() => {
|
|
455
|
+
ModelPermission.checkIfUserIsLoggedIn(Log, anonymousProps(), type);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const postgresTemplate: string = sentenceTemplate(
|
|
459
|
+
postgresMessage,
|
|
460
|
+
postgresSingularName,
|
|
461
|
+
);
|
|
462
|
+
const analyticsTemplate: string = sentenceTemplate(
|
|
463
|
+
analyticsMessage,
|
|
464
|
+
analyticsSingularName,
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
// The normalisation actually fired on both sides.
|
|
468
|
+
expect(postgresTemplate).toContain("<singularName>");
|
|
469
|
+
expect(analyticsTemplate).toContain("<singularName>");
|
|
470
|
+
|
|
471
|
+
expect(postgresTemplate).toBe(analyticsTemplate);
|
|
472
|
+
expect(postgresTemplate).toBe(
|
|
473
|
+
`Authenticated user or a valid API key is needed to ${type} record of <singularName>.`,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// The API early return has to agree across both engines too.
|
|
479
|
+
it("lets an API-key caller with no userId through on both storage engines", () => {
|
|
480
|
+
for (const type of allRequestTypes) {
|
|
481
|
+
expect(() => {
|
|
482
|
+
PublicPermission.checkIfUserIsLoggedIn(
|
|
483
|
+
LogPipeline,
|
|
484
|
+
apiKeyProps(),
|
|
485
|
+
type,
|
|
486
|
+
);
|
|
487
|
+
}).not.toThrow();
|
|
488
|
+
|
|
489
|
+
expect(() => {
|
|
490
|
+
ModelPermission.checkIfUserIsLoggedIn(Log, apiKeyProps(), type);
|
|
491
|
+
}).not.toThrow();
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import QueryUtil from "../../../../Server/Types/Database/QueryUtil";
|
|
2
|
+
import Incident from "../../../../Models/DatabaseModels/Incident";
|
|
2
3
|
import TeamMember from "../../../../Models/DatabaseModels/TeamMember";
|
|
4
|
+
import Includes from "../../../../Types/BaseDatabase/Includes";
|
|
5
|
+
import IsNull from "../../../../Types/BaseDatabase/IsNull";
|
|
3
6
|
import Search from "../../../../Types/BaseDatabase/Search";
|
|
4
7
|
import ObjectID from "../../../../Types/ObjectID";
|
|
5
8
|
import { FindOperator } from "typeorm";
|
|
@@ -72,3 +75,113 @@ describe("QueryUtil.serializeQuery — nested relation operators", () => {
|
|
|
72
75
|
expect(result["user"]._id).toBe(userId.toString());
|
|
73
76
|
});
|
|
74
77
|
});
|
|
78
|
+
|
|
79
|
+
/*
|
|
80
|
+
* Custom field filters ride the `customFields` jsonb column, so they reach the
|
|
81
|
+
* database through serializeQuery's JSON branch. These pin the routing: the
|
|
82
|
+
* branch has to fire for an operator-valued object (the new custom-field
|
|
83
|
+
* chips) exactly as it always has for a plain one, and the branches above it
|
|
84
|
+
* have to keep winning for a whole-column IsNull.
|
|
85
|
+
*/
|
|
86
|
+
describe("QueryUtil.serializeQuery — customFields jsonb column", () => {
|
|
87
|
+
type RawOperatorSqlFunction = (operator: unknown) => string;
|
|
88
|
+
|
|
89
|
+
const rawOperatorSql: RawOperatorSqlFunction = (
|
|
90
|
+
operator: unknown,
|
|
91
|
+
): string => {
|
|
92
|
+
const getSql: ((aliasPath: string) => string) | undefined = (
|
|
93
|
+
operator as FindOperator<unknown>
|
|
94
|
+
).getSql;
|
|
95
|
+
|
|
96
|
+
if (!getSql) {
|
|
97
|
+
throw new Error("Expected a Raw FindOperator with a SQL generator.");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return getSql("Incident.customFields");
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
it("compiles a plain key/value object into a raw jsonb predicate", () => {
|
|
104
|
+
const query: Record<string, unknown> = {
|
|
105
|
+
projectId: ObjectID.generate(),
|
|
106
|
+
customFields: { Team: "Payments" },
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const result: Record<string, any> = QueryUtil.serializeQuery(
|
|
110
|
+
Incident,
|
|
111
|
+
query as any,
|
|
112
|
+
) as unknown as Record<string, any>;
|
|
113
|
+
|
|
114
|
+
expect(result["customFields"]).toBeInstanceOf(FindOperator);
|
|
115
|
+
expect(result["customFields"].type).toBe("raw");
|
|
116
|
+
expect(rawOperatorSql(result["customFields"])).toContain(
|
|
117
|
+
'"Incident"."customFields" ->>',
|
|
118
|
+
);
|
|
119
|
+
expect(
|
|
120
|
+
Object.values(result["customFields"].objectLiteralParameters),
|
|
121
|
+
).toContain("Payments");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("compiles an operator nested under a key into the same raw predicate", () => {
|
|
125
|
+
const query: Record<string, unknown> = {
|
|
126
|
+
projectId: ObjectID.generate(),
|
|
127
|
+
customFields: { Team: new Includes(["Payments", "Billing"]) },
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const result: Record<string, any> = QueryUtil.serializeQuery(
|
|
131
|
+
Incident,
|
|
132
|
+
query as any,
|
|
133
|
+
) as unknown as Record<string, any>;
|
|
134
|
+
|
|
135
|
+
const sql: string = rawOperatorSql(result["customFields"]);
|
|
136
|
+
|
|
137
|
+
// Two values means two OR-ed equality arms, each with a containment check.
|
|
138
|
+
expect(sql).toContain(" OR ");
|
|
139
|
+
expect(sql).toContain("@> CAST(");
|
|
140
|
+
expect(
|
|
141
|
+
Object.values(result["customFields"].objectLiteralParameters),
|
|
142
|
+
).toEqual(expect.arrayContaining(["Team", "Payments", "Billing"]));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("never leaves a custom field name in the statement text", () => {
|
|
146
|
+
const query: Record<string, unknown> = {
|
|
147
|
+
projectId: ObjectID.generate(),
|
|
148
|
+
customFields: { "x' OR 1=1 --": "v" },
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const result: Record<string, any> = QueryUtil.serializeQuery(
|
|
152
|
+
Incident,
|
|
153
|
+
query as any,
|
|
154
|
+
) as unknown as Record<string, any>;
|
|
155
|
+
|
|
156
|
+
expect(rawOperatorSql(result["customFields"])).not.toContain("OR 1=1");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("still routes a whole-column IsNull to the branch above the JSON one", () => {
|
|
160
|
+
const query: Record<string, unknown> = {
|
|
161
|
+
projectId: ObjectID.generate(),
|
|
162
|
+
customFields: new IsNull(),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const result: Record<string, any> = QueryUtil.serializeQuery(
|
|
166
|
+
Incident,
|
|
167
|
+
query as any,
|
|
168
|
+
) as unknown as Record<string, any>;
|
|
169
|
+
|
|
170
|
+
expect(rawOperatorSql(result["customFields"])).toContain("IS NULL");
|
|
171
|
+
expect(rawOperatorSql(result["customFields"])).not.toContain("->>");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("still routes an explicit null to isNull", () => {
|
|
175
|
+
const query: Record<string, unknown> = {
|
|
176
|
+
projectId: ObjectID.generate(),
|
|
177
|
+
customFields: null,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const result: Record<string, any> = QueryUtil.serializeQuery(
|
|
181
|
+
Incident,
|
|
182
|
+
query as any,
|
|
183
|
+
) as unknown as Record<string, any>;
|
|
184
|
+
|
|
185
|
+
expect(rawOperatorSql(result["customFields"])).toContain("IS NULL");
|
|
186
|
+
});
|
|
187
|
+
});
|