@oneuptime/common 11.5.13 → 11.6.0
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/EnterpriseLicense.ts +21 -0
- package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
- package/Models/DatabaseModels/GlobalConfig.ts +76 -0
- package/Server/API/EnterpriseLicenseAPI.ts +57 -0
- package/Server/API/GlobalConfigAPI.ts +62 -1
- package/Server/EnvironmentConfig.ts +22 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.ts +38 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/AnalyticsDatabaseService.ts +48 -45
- package/Server/Services/DatabaseService.ts +66 -1
- package/Server/Services/WorkflowService.ts +100 -39
- package/Server/Types/Workflow/Components/Schedule.ts +57 -32
- package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +59 -0
- package/Server/Utils/Express.ts +13 -0
- package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +799 -0
- package/Server/Utils/Telemetry/TelemetryWriterClient.ts +263 -0
- package/Server/Utils/Telemetry/TelemetryWriterServer.ts +240 -0
- package/Server/Utils/Telemetry/TelemetryWriterShedMetrics.ts +93 -0
- package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
- package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
- package/Tests/Server/Services/WorkflowService.test.ts +308 -0
- package/Tests/Server/Utils/Attribution.test.ts +203 -0
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +183 -0
- package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +340 -0
- package/Tests/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.test.ts +87 -0
- package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +165 -0
- package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +86 -0
- package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +1565 -0
- package/Tests/Server/Utils/Telemetry/TelemetryWriterClient.test.ts +313 -0
- package/Tests/Server/Utils/Telemetry/TelemetryWriterServer.test.ts +270 -0
- package/Tests/Server/Utils/Telemetry/TelemetryWriterShedMetrics.test.ts +127 -0
- package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -0
- package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +228 -0
- package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +169 -0
- package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +289 -0
- package/Tests/Types/Monitor/MonitorStepLogMonitor.test.ts +231 -0
- package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +245 -0
- package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +155 -0
- package/Tests/Types/Monitor/MonitorStepTelemetrySerialization.test.ts +141 -0
- package/Tests/Types/Monitor/MonitorStepTraceMonitor.test.ts +137 -0
- package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +311 -0
- package/Tests/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.test.ts +461 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +2852 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +758 -0
- package/Tests/UI/Utils/NotificationMethodUtil.test.ts +564 -0
- package/Tests/Utils/Array.test.ts +156 -0
- package/Tests/Utils/CronTab.test.ts +199 -0
- package/Tests/Utils/ModelImportExport.test.ts +101 -0
- package/Tests/Utils/Number.test.ts +179 -0
- package/Tests/Utils/VersionUtil.test.ts +348 -0
- package/Tests/jest.setup.ts +1 -0
- package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
- package/Types/Monitor/MonitorStep.ts +24 -4
- package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
- package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
- package/UI/Components/Workflow/ArgumentsForm.tsx +68 -30
- package/UI/Components/Workflow/CronScheduleField.tsx +411 -0
- package/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.ts +176 -0
- package/UI/Utils/NotificationMethodUtil.ts +310 -0
- package/Utils/CronTab.ts +823 -0
- package/Utils/ModelImportExport.ts +21 -6
- package/Utils/VersionUtil.ts +260 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +22 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
- package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +26 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +81 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +47 -0
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/API/GlobalConfigAPI.js +52 -1
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +17 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +44 -16
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +53 -1
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/WorkflowService.js +80 -30
- package/build/dist/Server/Services/WorkflowService.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Schedule.js +44 -19
- package/build/dist/Server/Types/Workflow/Components/Schedule.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js +24 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -0
- package/build/dist/Server/Utils/Express.js +12 -0
- package/build/dist/Server/Utils/Express.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +496 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js +165 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js +136 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js +76 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorStep.js +20 -4
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +73 -0
- package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -0
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +261 -13
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +30 -6
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/CronScheduleField.js +209 -0
- package/build/dist/UI/Components/Workflow/CronScheduleField.js.map +1 -0
- package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js +129 -0
- package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js.map +1 -0
- package/build/dist/UI/Utils/NotificationMethodUtil.js +189 -0
- package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -0
- package/build/dist/Utils/CronTab.js +609 -0
- package/build/dist/Utils/CronTab.js.map +1 -0
- package/build/dist/Utils/ModelImportExport.js +20 -6
- package/build/dist/Utils/ModelImportExport.js.map +1 -1
- package/build/dist/Utils/VersionUtil.js +193 -0
- package/build/dist/Utils/VersionUtil.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import NetworkDeviceDiscoveryScanService from "../../../Server/Services/NetworkDeviceDiscoveryScanService";
|
|
2
|
+
import ModelPermission from "../../../Server/Types/Database/Permissions/Index";
|
|
3
|
+
import UpdateBy from "../../../Server/Types/Database/UpdateBy";
|
|
4
|
+
import NetworkDeviceDiscoveryScan from "../../../Models/DatabaseModels/NetworkDeviceDiscoveryScan";
|
|
5
|
+
import DatabaseBaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
6
|
+
import Probe from "../../../Models/DatabaseModels/Probe";
|
|
7
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
8
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
9
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
10
|
+
import Select from "../../../Server/Types/Database/Select";
|
|
11
|
+
import { afterEach, describe, expect, jest, test } from "@jest/globals";
|
|
12
|
+
|
|
13
|
+
type ScanUpdateData = UpdateBy<NetworkDeviceDiscoveryScan>["data"];
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* Regression tests for the bug that left every SNMP discovery scan stuck in
|
|
17
|
+
* "Pending": update `data` passed as a full model instance carries the
|
|
18
|
+
* non-column own properties of DatabaseBaseModel (column initializers plus
|
|
19
|
+
* `isPermissionIf = {}`). _updateBy used to turn every data key into a select
|
|
20
|
+
* column for its internal find, and the unknown `isPermissionIf` column made
|
|
21
|
+
* that find throw `TableColumnMetadata not found for isPermissionIf column`,
|
|
22
|
+
* failing the whole update — so the probe-ingest list endpoint 400'd before
|
|
23
|
+
* it could claim a scan.
|
|
24
|
+
*
|
|
25
|
+
* sanitizeUpdateData is the fix: model instances are stripped down to their
|
|
26
|
+
* set table columns before the update pipeline runs.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
describe("DatabaseService.sanitizeUpdateData — model instances become plain column data", () => {
|
|
30
|
+
test("a fresh model instance really does carry non-column own properties (the trap this guards against)", () => {
|
|
31
|
+
const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
* If this stops holding, the base model changed shape and these tests
|
|
35
|
+
* (and sanitizeUpdateData itself) should be revisited.
|
|
36
|
+
*/
|
|
37
|
+
expect(Object.keys(scan)).toContain("isPermissionIf");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("keeps only the columns that were actually set", () => {
|
|
41
|
+
const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
|
|
42
|
+
scan.status = "In Progress";
|
|
43
|
+
scan.startedAt = OneUptimeDate.getCurrentDate();
|
|
44
|
+
|
|
45
|
+
const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
|
|
46
|
+
"sanitizeUpdateData"
|
|
47
|
+
](scan as unknown as ScanUpdateData);
|
|
48
|
+
|
|
49
|
+
expect(Object.keys(data as JSONObject).sort()).toEqual([
|
|
50
|
+
"startedAt",
|
|
51
|
+
"status",
|
|
52
|
+
]);
|
|
53
|
+
expect((data as JSONObject)["status"]).toBe("In Progress");
|
|
54
|
+
expect((data as JSONObject)["startedAt"]).toBeInstanceOf(Date);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("the result is a plain object, not a model instance", () => {
|
|
58
|
+
const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
|
|
59
|
+
scan.status = "Completed";
|
|
60
|
+
|
|
61
|
+
const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
|
|
62
|
+
"sanitizeUpdateData"
|
|
63
|
+
](scan as unknown as ScanUpdateData);
|
|
64
|
+
|
|
65
|
+
expect(data).not.toBeInstanceOf(DatabaseBaseModel);
|
|
66
|
+
expect(Object.getPrototypeOf(data)).toBe(Object.prototype);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("drops isPermissionIf and every other non-column own property", () => {
|
|
70
|
+
const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
|
|
71
|
+
scan.status = "Completed";
|
|
72
|
+
|
|
73
|
+
const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
|
|
74
|
+
"sanitizeUpdateData"
|
|
75
|
+
](scan as unknown as ScanUpdateData);
|
|
76
|
+
|
|
77
|
+
expect(Object.keys(data as JSONObject)).not.toContain("isPermissionIf");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("drops columns left undefined so they cannot become writes", () => {
|
|
81
|
+
const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
|
|
82
|
+
scan.status = "Failed";
|
|
83
|
+
// cidr, snmpVersion, statusMessage etc. stay undefined.
|
|
84
|
+
|
|
85
|
+
const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
|
|
86
|
+
"sanitizeUpdateData"
|
|
87
|
+
](scan as unknown as ScanUpdateData);
|
|
88
|
+
|
|
89
|
+
expect(Object.keys(data as JSONObject)).toEqual(["status"]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("preserves explicit null so columns can still be cleared", () => {
|
|
93
|
+
const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
|
|
94
|
+
scan.status = "Pending";
|
|
95
|
+
(scan as any).statusMessage = null;
|
|
96
|
+
(scan as any).nextScanAt = null;
|
|
97
|
+
|
|
98
|
+
const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
|
|
99
|
+
"sanitizeUpdateData"
|
|
100
|
+
](scan as unknown as ScanUpdateData);
|
|
101
|
+
|
|
102
|
+
expect((data as JSONObject)["statusMessage"]).toBeNull();
|
|
103
|
+
expect((data as JSONObject)["nextScanAt"]).toBeNull();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("drops _id, createdAt, updatedAt and version even when set (loaded-model updates must not redirect the write or forge timestamps)", () => {
|
|
107
|
+
const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan(
|
|
108
|
+
ObjectID.generate(),
|
|
109
|
+
);
|
|
110
|
+
scan.status = "Completed";
|
|
111
|
+
(scan as any).createdAt = OneUptimeDate.getCurrentDate();
|
|
112
|
+
(scan as any).updatedAt = OneUptimeDate.getCurrentDate();
|
|
113
|
+
(scan as any).version = 7;
|
|
114
|
+
|
|
115
|
+
const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
|
|
116
|
+
"sanitizeUpdateData"
|
|
117
|
+
](scan as unknown as ScanUpdateData);
|
|
118
|
+
|
|
119
|
+
expect(Object.keys(data as JSONObject)).toEqual(["status"]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("keeps entity relation columns that were set", () => {
|
|
123
|
+
const probeId: ObjectID = ObjectID.generate();
|
|
124
|
+
const scan: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
|
|
125
|
+
scan.status = "Pending";
|
|
126
|
+
scan.probe = new Probe(probeId);
|
|
127
|
+
|
|
128
|
+
const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
|
|
129
|
+
"sanitizeUpdateData"
|
|
130
|
+
](scan as unknown as ScanUpdateData);
|
|
131
|
+
|
|
132
|
+
expect((data as JSONObject)["probe"]).toBeInstanceOf(Probe);
|
|
133
|
+
expect(((data as JSONObject)["probe"] as Probe).id?.toString()).toBe(
|
|
134
|
+
probeId.toString(),
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("plain objects pass through by reference, unknown keys and all (typos must keep failing loudly)", () => {
|
|
139
|
+
const plain: JSONObject = {
|
|
140
|
+
status: "Completed",
|
|
141
|
+
someTypoColumn: 1,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const data: ScanUpdateData = NetworkDeviceDiscoveryScanService[
|
|
145
|
+
"sanitizeUpdateData"
|
|
146
|
+
](plain as unknown as ScanUpdateData);
|
|
147
|
+
|
|
148
|
+
expect(data).toBe(plain);
|
|
149
|
+
expect(Object.keys(data as JSONObject)).toContain("someTypoColumn");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("DatabaseService._updateBy — updateOneById with a model instance no longer poisons the internal find", () => {
|
|
154
|
+
afterEach(() => {
|
|
155
|
+
jest.restoreAllMocks();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("select columns for the internal find contain only real columns from the data", async () => {
|
|
159
|
+
const scanId: ObjectID = ObjectID.generate();
|
|
160
|
+
|
|
161
|
+
const foundItem: NetworkDeviceDiscoveryScan =
|
|
162
|
+
new NetworkDeviceDiscoveryScan();
|
|
163
|
+
foundItem._id = scanId.toString();
|
|
164
|
+
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/typedef
|
|
166
|
+
const findBySpy = jest
|
|
167
|
+
.spyOn(NetworkDeviceDiscoveryScanService as any, "_findBy")
|
|
168
|
+
.mockResolvedValue([foundItem] as never);
|
|
169
|
+
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/typedef
|
|
171
|
+
const saveMock = jest.fn((item: unknown) => {
|
|
172
|
+
return Promise.resolve(item);
|
|
173
|
+
});
|
|
174
|
+
jest
|
|
175
|
+
.spyOn(NetworkDeviceDiscoveryScanService, "getRepository")
|
|
176
|
+
.mockReturnValue({ save: saveMock } as never);
|
|
177
|
+
|
|
178
|
+
// Permission layer is not under test here and needs no DB.
|
|
179
|
+
jest
|
|
180
|
+
.spyOn(ModelPermission, "checkUpdatePermissionByModel")
|
|
181
|
+
.mockResolvedValue(undefined as never);
|
|
182
|
+
jest
|
|
183
|
+
.spyOn(ModelPermission, "checkUpdateQueryPermissions")
|
|
184
|
+
.mockImplementation(((
|
|
185
|
+
_modelType: unknown,
|
|
186
|
+
query: unknown,
|
|
187
|
+
): Promise<unknown> => {
|
|
188
|
+
return Promise.resolve(query);
|
|
189
|
+
}) as never);
|
|
190
|
+
|
|
191
|
+
const inProgress: NetworkDeviceDiscoveryScan =
|
|
192
|
+
new NetworkDeviceDiscoveryScan();
|
|
193
|
+
inProgress.status = "In Progress";
|
|
194
|
+
inProgress.startedAt = OneUptimeDate.getCurrentDate();
|
|
195
|
+
|
|
196
|
+
/*
|
|
197
|
+
* Before the fix this call rejected with "TableColumnMetadata not found
|
|
198
|
+
* for isPermissionIf column" — reproducing exactly why discovery scans
|
|
199
|
+
* never left Pending.
|
|
200
|
+
*/
|
|
201
|
+
await expect(
|
|
202
|
+
NetworkDeviceDiscoveryScanService.updateOneById({
|
|
203
|
+
id: scanId,
|
|
204
|
+
data: inProgress as unknown as UpdateBy<NetworkDeviceDiscoveryScan>["data"],
|
|
205
|
+
props: {
|
|
206
|
+
isRoot: true,
|
|
207
|
+
},
|
|
208
|
+
}),
|
|
209
|
+
).resolves.toBeUndefined();
|
|
210
|
+
|
|
211
|
+
expect(findBySpy).toHaveBeenCalledTimes(1);
|
|
212
|
+
const findArgs: { select: Select<NetworkDeviceDiscoveryScan> } = (
|
|
213
|
+
findBySpy.mock.calls[0] as Array<any>
|
|
214
|
+
)[0];
|
|
215
|
+
const selectedColumns: Array<string> = Object.keys(findArgs.select);
|
|
216
|
+
|
|
217
|
+
// The data columns plus _id and the tenant column — nothing else.
|
|
218
|
+
expect(selectedColumns.sort()).toEqual(
|
|
219
|
+
["_id", "projectId", "startedAt", "status"].sort(),
|
|
220
|
+
);
|
|
221
|
+
expect(selectedColumns).not.toContain("isPermissionIf");
|
|
222
|
+
|
|
223
|
+
// The persisted payload is the plain data plus the located row's _id.
|
|
224
|
+
expect(saveMock).toHaveBeenCalledTimes(1);
|
|
225
|
+
const savedItem: JSONObject = saveMock.mock.calls[0]![0] as JSONObject;
|
|
226
|
+
expect(Object.keys(savedItem).sort()).toEqual(
|
|
227
|
+
["_id", "startedAt", "status"].sort(),
|
|
228
|
+
);
|
|
229
|
+
expect(savedItem["status"]).toBe("In Progress");
|
|
230
|
+
expect(savedItem["_id"]).toBe(scanId.toString());
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import NetworkDeviceDiscoveryScan from "../../../Models/DatabaseModels/NetworkDeviceDiscoveryScan";
|
|
2
|
+
import NetworkDeviceDiscoveryScanService from "../../../Server/Services/NetworkDeviceDiscoveryScanService";
|
|
3
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
4
|
+
import { afterEach, describe, expect, test } from "@jest/globals";
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* Regression tests for the second layer of the discovery-scan "stuck
|
|
8
|
+
* Pending" bug (the first layer — model instances poisoning the internal
|
|
9
|
+
* find's select columns — is covered in
|
|
10
|
+
* DatabaseServiceSanitizeUpdateData.test.ts).
|
|
11
|
+
*
|
|
12
|
+
* DatabaseService._updateBy builds the row it saves as a merge of the
|
|
13
|
+
* located row's _id and the caller's update data. With the old merge order
|
|
14
|
+
* `{ _id: item._id, ...data }`, update data carrying an own `_id: undefined`
|
|
15
|
+
* property (a fresh model instance before sanitizeUpdateData existed, or a
|
|
16
|
+
* plain object that spells it out) clobbered the located row's id — TypeORM
|
|
17
|
+
* save() saw no primary key, INSERTed a new row instead of updating, and
|
|
18
|
+
* died on the first NOT NULL column.
|
|
19
|
+
*
|
|
20
|
+
* These tests pin the fixed merge order: the located row's _id must win no
|
|
21
|
+
* matter what shape the update data takes. _findBy is mocked out, so this
|
|
22
|
+
* exercises the save-payload merge in isolation.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
type SavedRow = { _id?: string; status?: string; startedAt?: Date };
|
|
26
|
+
|
|
27
|
+
function mockPersistence(existingRowId: string): jest.Mock {
|
|
28
|
+
const existing: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
|
|
29
|
+
existing._id = existingRowId;
|
|
30
|
+
|
|
31
|
+
jest
|
|
32
|
+
.spyOn(NetworkDeviceDiscoveryScanService as any, "_findBy")
|
|
33
|
+
.mockResolvedValue([existing] as never);
|
|
34
|
+
|
|
35
|
+
const save: jest.Mock = jest
|
|
36
|
+
.fn()
|
|
37
|
+
.mockImplementation(async (row: unknown): Promise<unknown> => {
|
|
38
|
+
return row;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
jest
|
|
42
|
+
.spyOn(NetworkDeviceDiscoveryScanService, "getRepository")
|
|
43
|
+
.mockReturnValue({ save } as any);
|
|
44
|
+
|
|
45
|
+
return save;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("DatabaseService.updateOneById — saved row keeps the located row's _id", () => {
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
jest.restoreAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("when data is a fresh model instance (own undefined _id property)", async () => {
|
|
54
|
+
const rowId: string = ObjectID.generate().toString();
|
|
55
|
+
const save: jest.Mock = mockPersistence(rowId);
|
|
56
|
+
|
|
57
|
+
const data: NetworkDeviceDiscoveryScan = new NetworkDeviceDiscoveryScan();
|
|
58
|
+
data.status = "In Progress";
|
|
59
|
+
data.startedAt = new Date();
|
|
60
|
+
|
|
61
|
+
// The bug's precondition: the instance really does carry _id: undefined.
|
|
62
|
+
expect(Object.keys(data)).toContain("_id");
|
|
63
|
+
|
|
64
|
+
await NetworkDeviceDiscoveryScanService.updateOneById({
|
|
65
|
+
id: new ObjectID(rowId),
|
|
66
|
+
data: data as never,
|
|
67
|
+
props: { isRoot: true },
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(save).toHaveBeenCalledTimes(1);
|
|
71
|
+
const saved: SavedRow = save.mock.calls[0]![0] as SavedRow;
|
|
72
|
+
expect(saved._id).toBe(rowId);
|
|
73
|
+
expect(saved.status).toBe("In Progress");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("when data is a plain partial object", async () => {
|
|
77
|
+
const rowId: string = ObjectID.generate().toString();
|
|
78
|
+
const save: jest.Mock = mockPersistence(rowId);
|
|
79
|
+
|
|
80
|
+
await NetworkDeviceDiscoveryScanService.updateOneById({
|
|
81
|
+
id: new ObjectID(rowId),
|
|
82
|
+
data: { status: "In Progress" } as never,
|
|
83
|
+
props: { isRoot: true },
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(save).toHaveBeenCalledTimes(1);
|
|
87
|
+
const saved: SavedRow = save.mock.calls[0]![0] as SavedRow;
|
|
88
|
+
expect(saved._id).toBe(rowId);
|
|
89
|
+
expect(saved.status).toBe("In Progress");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("when data is a plain object that spells out _id: undefined", async () => {
|
|
93
|
+
const rowId: string = ObjectID.generate().toString();
|
|
94
|
+
const save: jest.Mock = mockPersistence(rowId);
|
|
95
|
+
|
|
96
|
+
await NetworkDeviceDiscoveryScanService.updateOneById({
|
|
97
|
+
id: new ObjectID(rowId),
|
|
98
|
+
data: { _id: undefined, status: "In Progress" } as never,
|
|
99
|
+
props: { isRoot: true },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(save).toHaveBeenCalledTimes(1);
|
|
103
|
+
const saved: SavedRow = save.mock.calls[0]![0] as SavedRow;
|
|
104
|
+
expect(saved._id).toBe(rowId);
|
|
105
|
+
expect(saved.status).toBe("In Progress");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkflowService trigger denormalization tests.
|
|
3
|
+
*
|
|
4
|
+
* The runner never parses workflow graphs. It finds workflows by the
|
|
5
|
+
* triggerId / triggerArguments columns, which are derived from the graph and
|
|
6
|
+
* written onto the row. Anything that puts a graph on a workflow without
|
|
7
|
+
* populating those columns produces a workflow that looks correct in the
|
|
8
|
+
* builder and silently never fires.
|
|
9
|
+
*
|
|
10
|
+
* That is exactly what creating a workflow with a graph already attached does
|
|
11
|
+
* - importing a JSON export, or duplicating an existing workflow - so the
|
|
12
|
+
* create path has to denormalize the trigger just like the update path does.
|
|
13
|
+
*
|
|
14
|
+
* updateOneById and the outbound call to the workflow service are both spied
|
|
15
|
+
* on, so no database or network is touched.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import WorkflowService from "../../../Server/Services/WorkflowService";
|
|
19
|
+
import Workflow from "../../../Models/DatabaseModels/Workflow";
|
|
20
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
21
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
22
|
+
import API from "../../../Utils/API";
|
|
23
|
+
import { afterEach, describe, expect, test, jest } from "@jest/globals";
|
|
24
|
+
|
|
25
|
+
const WORKFLOW_ID: string = "550e8400-e29b-41d4-a716-446655440000";
|
|
26
|
+
|
|
27
|
+
function scheduleGraph(): JSONObject {
|
|
28
|
+
return {
|
|
29
|
+
nodes: [
|
|
30
|
+
{
|
|
31
|
+
id: "node-1",
|
|
32
|
+
data: {
|
|
33
|
+
metadataId: "Schedule",
|
|
34
|
+
componentType: "Trigger",
|
|
35
|
+
nodeType: "Node",
|
|
36
|
+
arguments: { "schedule-cron": "0 0 * * *" },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "node-2",
|
|
41
|
+
data: {
|
|
42
|
+
metadataId: "Webhook",
|
|
43
|
+
componentType: "Component",
|
|
44
|
+
nodeType: "Node",
|
|
45
|
+
arguments: {},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
edges: [],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function createdWorkflow(graph: JSONObject | undefined): Workflow {
|
|
54
|
+
/*
|
|
55
|
+
* webhookSecretKey is set so onCreateSuccess skips its own key-generation
|
|
56
|
+
* update, leaving updateOneById calls attributable to trigger derivation.
|
|
57
|
+
* projectId is left undefined so the fire-and-forget label/owner rule
|
|
58
|
+
* engines are skipped.
|
|
59
|
+
*/
|
|
60
|
+
return {
|
|
61
|
+
_id: WORKFLOW_ID,
|
|
62
|
+
id: new ObjectID(WORKFLOW_ID),
|
|
63
|
+
graph: graph,
|
|
64
|
+
webhookSecretKey: "already-set",
|
|
65
|
+
} as unknown as Workflow;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function spyOnUpdateOneById(): jest.SpyInstance {
|
|
69
|
+
return jest
|
|
70
|
+
.spyOn(WorkflowService, "updateOneById")
|
|
71
|
+
.mockResolvedValue(undefined as never) as unknown as jest.SpyInstance;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function spyOnApiPost(): jest.SpyInstance {
|
|
75
|
+
return jest
|
|
76
|
+
.spyOn(API, "post")
|
|
77
|
+
.mockResolvedValue({} as never) as unknown as jest.SpyInstance;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type OnCreateSuccessFunction = (
|
|
81
|
+
onCreate: unknown,
|
|
82
|
+
createdItem: Workflow,
|
|
83
|
+
) => Promise<Workflow>;
|
|
84
|
+
|
|
85
|
+
function onCreateSuccess(createdItem: Workflow): Promise<Workflow> {
|
|
86
|
+
const hook: OnCreateSuccessFunction = (
|
|
87
|
+
WorkflowService as unknown as { onCreateSuccess: OnCreateSuccessFunction }
|
|
88
|
+
).onCreateSuccess.bind(WorkflowService);
|
|
89
|
+
|
|
90
|
+
return hook({ createBy: { data: createdItem }, carryForward: null }, {
|
|
91
|
+
...createdItem,
|
|
92
|
+
} as Workflow);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type OnUpdateSuccessFunction = (
|
|
96
|
+
onUpdate: unknown,
|
|
97
|
+
updatedItemIds: Array<ObjectID>,
|
|
98
|
+
) => Promise<unknown>;
|
|
99
|
+
|
|
100
|
+
function onUpdateSuccess(graph: JSONObject | undefined): Promise<unknown> {
|
|
101
|
+
const hook: OnUpdateSuccessFunction = (
|
|
102
|
+
WorkflowService as unknown as { onUpdateSuccess: OnUpdateSuccessFunction }
|
|
103
|
+
).onUpdateSuccess.bind(WorkflowService);
|
|
104
|
+
|
|
105
|
+
return hook(
|
|
106
|
+
{
|
|
107
|
+
updateBy: {
|
|
108
|
+
query: { _id: WORKFLOW_ID },
|
|
109
|
+
data: graph === undefined ? {} : { graph: graph },
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
[new ObjectID(WORKFLOW_ID)],
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
describe("WorkflowService trigger denormalization on create", () => {
|
|
117
|
+
afterEach(() => {
|
|
118
|
+
jest.restoreAllMocks();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("writes the trigger from the graph when a workflow is created with one", async () => {
|
|
122
|
+
const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
|
|
123
|
+
spyOnApiPost();
|
|
124
|
+
|
|
125
|
+
await onCreateSuccess(createdWorkflow(scheduleGraph()));
|
|
126
|
+
|
|
127
|
+
expect(updateSpy).toHaveBeenCalledTimes(1);
|
|
128
|
+
|
|
129
|
+
const updateArgs: JSONObject = updateSpy.mock.calls[0]![0] as JSONObject;
|
|
130
|
+
const data: JSONObject = updateArgs["data"] as JSONObject;
|
|
131
|
+
|
|
132
|
+
expect(data["triggerId"]).toBe("Schedule");
|
|
133
|
+
expect(data["triggerArguments"]).toEqual({ "schedule-cron": "0 0 * * *" });
|
|
134
|
+
|
|
135
|
+
// must not recurse back through the hooks it is running inside of.
|
|
136
|
+
expect(updateArgs["props"]).toEqual({ isRoot: true, ignoreHooks: true });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("notifies the workflow service so schedule triggers get registered", async () => {
|
|
140
|
+
spyOnUpdateOneById();
|
|
141
|
+
const postSpy: jest.SpyInstance = spyOnApiPost();
|
|
142
|
+
|
|
143
|
+
await onCreateSuccess(createdWorkflow(scheduleGraph()));
|
|
144
|
+
|
|
145
|
+
expect(postSpy).toHaveBeenCalledTimes(1);
|
|
146
|
+
|
|
147
|
+
const postArgs: JSONObject = postSpy.mock.calls[0]![0] as JSONObject;
|
|
148
|
+
|
|
149
|
+
expect(postArgs["url"]!.toString()).toContain(
|
|
150
|
+
`/workflow/update/${WORKFLOW_ID}`,
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("clears the trigger when the graph has no trigger node", async () => {
|
|
155
|
+
const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
|
|
156
|
+
spyOnApiPost();
|
|
157
|
+
|
|
158
|
+
await onCreateSuccess(
|
|
159
|
+
createdWorkflow({
|
|
160
|
+
nodes: [
|
|
161
|
+
{
|
|
162
|
+
id: "node-1",
|
|
163
|
+
data: {
|
|
164
|
+
metadataId: "Webhook",
|
|
165
|
+
componentType: "Component",
|
|
166
|
+
nodeType: "Node",
|
|
167
|
+
arguments: {},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
edges: [],
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const data: JSONObject = (updateSpy.mock.calls[0]![0] as JSONObject)[
|
|
176
|
+
"data"
|
|
177
|
+
] as JSONObject;
|
|
178
|
+
|
|
179
|
+
expect(data["triggerId"]).toBeNull();
|
|
180
|
+
expect(data["triggerArguments"]).toEqual({});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("does no trigger work for a workflow created without a graph", async () => {
|
|
184
|
+
const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
|
|
185
|
+
const postSpy: jest.SpyInstance = spyOnApiPost();
|
|
186
|
+
|
|
187
|
+
await onCreateSuccess(createdWorkflow(undefined));
|
|
188
|
+
|
|
189
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
190
|
+
expect(postSpy).not.toHaveBeenCalled();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("does not fail the create when the workflow service is unreachable", async () => {
|
|
194
|
+
const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
|
|
195
|
+
|
|
196
|
+
jest
|
|
197
|
+
.spyOn(API, "post")
|
|
198
|
+
.mockRejectedValue(new Error("connect ECONNREFUSED") as never);
|
|
199
|
+
|
|
200
|
+
/*
|
|
201
|
+
* The row and its trigger are already persisted at this point, so a
|
|
202
|
+
* workflow service outage must not turn a successful import into an error.
|
|
203
|
+
*/
|
|
204
|
+
await expect(
|
|
205
|
+
onCreateSuccess(createdWorkflow(scheduleGraph())),
|
|
206
|
+
).resolves.toBeDefined();
|
|
207
|
+
|
|
208
|
+
expect(updateSpy).toHaveBeenCalledTimes(1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("derives the trigger for a genuinely imported workflow, which has no webhook secret yet", async () => {
|
|
212
|
+
const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
|
|
213
|
+
spyOnApiPost();
|
|
214
|
+
|
|
215
|
+
/*
|
|
216
|
+
* webhookSecretKey carries create: [] access control, so it is stripped
|
|
217
|
+
* from export files and an imported workflow reaches this hook without
|
|
218
|
+
* one. That means two updates run in sequence - key generation, then
|
|
219
|
+
* trigger derivation - and the trigger write must still happen.
|
|
220
|
+
*/
|
|
221
|
+
const imported: Workflow = {
|
|
222
|
+
_id: WORKFLOW_ID,
|
|
223
|
+
id: new ObjectID(WORKFLOW_ID),
|
|
224
|
+
graph: scheduleGraph(),
|
|
225
|
+
} as unknown as Workflow;
|
|
226
|
+
|
|
227
|
+
await onCreateSuccess(imported);
|
|
228
|
+
|
|
229
|
+
expect(updateSpy).toHaveBeenCalledTimes(2);
|
|
230
|
+
|
|
231
|
+
const secretUpdate: JSONObject = (
|
|
232
|
+
updateSpy.mock.calls[0]![0] as JSONObject
|
|
233
|
+
)["data"] as JSONObject;
|
|
234
|
+
const triggerUpdate: JSONObject = (
|
|
235
|
+
updateSpy.mock.calls[1]![0] as JSONObject
|
|
236
|
+
)["data"] as JSONObject;
|
|
237
|
+
|
|
238
|
+
expect(secretUpdate["webhookSecretKey"]).toBeDefined();
|
|
239
|
+
expect(triggerUpdate["triggerId"]).toBe("Schedule");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("writes no trigger for a graph that carries no nodes key", async () => {
|
|
243
|
+
const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
|
|
244
|
+
spyOnApiPost();
|
|
245
|
+
|
|
246
|
+
await onCreateSuccess(createdWorkflow({ edges: [] }));
|
|
247
|
+
|
|
248
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("WorkflowService trigger denormalization on update", () => {
|
|
253
|
+
afterEach(() => {
|
|
254
|
+
jest.restoreAllMocks();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("writes the trigger when a saved graph carries one", async () => {
|
|
258
|
+
const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
|
|
259
|
+
spyOnApiPost();
|
|
260
|
+
|
|
261
|
+
await onUpdateSuccess(scheduleGraph());
|
|
262
|
+
|
|
263
|
+
expect(updateSpy).toHaveBeenCalledTimes(1);
|
|
264
|
+
|
|
265
|
+
const data: JSONObject = (updateSpy.mock.calls[0]![0] as JSONObject)[
|
|
266
|
+
"data"
|
|
267
|
+
] as JSONObject;
|
|
268
|
+
|
|
269
|
+
expect(data["triggerId"]).toBe("Schedule");
|
|
270
|
+
expect(data["triggerArguments"]).toEqual({ "schedule-cron": "0 0 * * *" });
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("clears the trigger when the trigger node is removed in the builder", async () => {
|
|
274
|
+
const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
|
|
275
|
+
spyOnApiPost();
|
|
276
|
+
|
|
277
|
+
await onUpdateSuccess({ nodes: [], edges: [] });
|
|
278
|
+
|
|
279
|
+
const data: JSONObject = (updateSpy.mock.calls[0]![0] as JSONObject)[
|
|
280
|
+
"data"
|
|
281
|
+
] as JSONObject;
|
|
282
|
+
|
|
283
|
+
expect(data["triggerId"]).toBeNull();
|
|
284
|
+
expect(data["triggerArguments"]).toEqual({});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("touches no trigger columns when an update does not carry the graph", async () => {
|
|
288
|
+
const updateSpy: jest.SpyInstance = spyOnUpdateOneById();
|
|
289
|
+
spyOnApiPost();
|
|
290
|
+
|
|
291
|
+
// e.g. renaming a workflow must leave its trigger alone.
|
|
292
|
+
await onUpdateSuccess(undefined);
|
|
293
|
+
|
|
294
|
+
expect(updateSpy).not.toHaveBeenCalled();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("always notifies the workflow service so trigger changes take effect", async () => {
|
|
298
|
+
spyOnUpdateOneById();
|
|
299
|
+
const postSpy: jest.SpyInstance = spyOnApiPost();
|
|
300
|
+
|
|
301
|
+
await onUpdateSuccess(scheduleGraph());
|
|
302
|
+
|
|
303
|
+
expect(postSpy).toHaveBeenCalledTimes(1);
|
|
304
|
+
expect(
|
|
305
|
+
(postSpy.mock.calls[0]![0] as JSONObject)["url"]!.toString(),
|
|
306
|
+
).toContain(`/workflow/update/${WORKFLOW_ID}`);
|
|
307
|
+
});
|
|
308
|
+
});
|