@oneuptime/common 12.0.7 → 12.0.8
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/TelemetryEntity.ts +45 -0
- package/Models/DatabaseModels/TelemetryEntityRelationship.ts +26 -0
- package/Models/DatabaseModels/WorkflowLog.ts +38 -0
- package/Server/API/UserAPI.ts +16 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.ts +41 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/MasterAdminAuthorization.ts +11 -6
- package/Server/Services/TelemetryEntityRelationshipService.ts +3 -0
- package/Server/Services/TelemetryEntityService.ts +114 -1
- package/Server/Types/Workflow/Components/API/Utils.ts +44 -1
- package/Server/Types/Workflow/TriggerCode.ts +12 -0
- package/Server/Types/Workflow/Workflow.ts +5 -0
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +689 -0
- package/Server/Utils/VM/VMAPI.ts +56 -8
- package/Tests/App/Dashboard/MonitorCriteriaAttributeFilter.test.tsx +574 -0
- package/Tests/Server/API/UserProjectsAPI.test.ts +478 -4
- package/Tests/Server/Services/TelemetryEntityManualCreate.test.ts +248 -0
- package/Tests/Server/Types/Workflow/Components/ApiComponentHeaders.test.ts +192 -0
- package/Tests/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.test.ts +152 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +322 -0
- package/Tests/Server/Utils/VM/VMAPISubstitution.test.ts +243 -0
- package/Tests/Types/SerializableObjectDictionaryImportCycle.test.ts +197 -0
- package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +140 -0
- package/Tests/Types/Workflow/StepTrace.test.ts +235 -0
- package/Tests/Types/Workflow/TemplateSyntax.test.ts +379 -0
- package/Tests/Types/Workflow/Templates.test.ts +403 -0
- package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +459 -0
- package/Tests/UI/Components/DictionaryAttributeFilterRow.test.tsx +477 -0
- package/Tests/UI/Components/Forms/AnchoredFieldPopupKeyboard.test.tsx +382 -0
- package/Tests/UI/Components/Forms/ColorPickerPicking.test.tsx +569 -0
- package/Tests/UI/Components/Forms/ValidationJSON.test.ts +241 -0
- package/Tests/UI/Components/LogTimeRangePicker.test.tsx +42 -0
- package/Tests/UI/Components/ModalPortalDismissal.test.tsx +90 -0
- package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +49 -7
- package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +325 -0
- package/Tests/UI/Components/Workflow/GraphLint.test.ts +821 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +287 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +185 -0
- package/Tests/UI/Components/Workflow/ModelSchema.test.ts +174 -0
- package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +152 -0
- package/Tests/UI/Components/Workflow/Utils.test.ts +192 -0
- package/Tests/UI/Monitor/MonitorStepCriteriaView.test.tsx +432 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +514 -0
- package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +240 -0
- package/Tests/Utils/Schema/AnalyticsModelSchema.test.ts +469 -0
- package/Tests/Utils/Schema/ModelSchema.test.ts +268 -0
- package/Tests/Utils/Telemetry/EntityKeyNonTelemetry.test.ts +193 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +31 -0
- package/Types/SerializableObjectDictionary.ts +133 -39
- package/Types/Telemetry/EntitySource.ts +40 -0
- package/Types/Telemetry/EntityType.ts +27 -0
- package/Types/Telemetry/EntityTypeGroups.ts +65 -0
- package/Types/Workflow/Component.ts +29 -0
- package/Types/Workflow/Components/API.ts +35 -0
- package/Types/Workflow/Components/BaseModel.ts +7 -3
- package/Types/Workflow/StepTrace.ts +181 -0
- package/Types/Workflow/TemplateSyntax.ts +499 -0
- package/Types/Workflow/Templates.ts +316 -0
- package/UI/Components/Date/CustomTimeRangeModal.tsx +278 -0
- package/UI/Components/Date/TimeRangePickerDropdown.tsx +250 -0
- package/UI/Components/Dictionary/Dictionary.tsx +23 -6
- package/UI/Components/Forms/Fields/ColorPicker.tsx +66 -10
- package/UI/Components/Forms/Fields/IconPicker.tsx +48 -10
- package/UI/Components/Forms/Types/Field.ts +7 -0
- package/UI/Components/Forms/Validation.ts +63 -1
- package/UI/Components/Input/Input.tsx +31 -3
- package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +11 -216
- package/UI/Components/Modal/Modal.tsx +37 -4
- package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +11 -210
- package/UI/Components/Workflow/ArgumentsForm.tsx +194 -7
- package/UI/Components/Workflow/Component.tsx +28 -26
- package/UI/Components/Workflow/ComponentSettingsModal.tsx +69 -8
- package/UI/Components/Workflow/GraphLint.ts +591 -0
- package/UI/Components/Workflow/ModelColumnEditor.tsx +396 -0
- package/UI/Components/Workflow/ModelSchema.ts +196 -0
- package/UI/Components/Workflow/RunForm.tsx +41 -7
- package/UI/Components/Workflow/RunStatusWatcher.ts +122 -0
- package/UI/Components/Workflow/StepTraceViewer.tsx +186 -0
- package/UI/Components/Workflow/Utils.ts +93 -1
- package/UI/Components/Workflow/Workflow.tsx +77 -4
- package/UI/Types/LayeredDismissal.ts +31 -0
- package/UI/Types/UseAnchoredFieldPopup.ts +99 -0
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +533 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +45 -7
- package/Utils/Telemetry/EntityKey.ts +66 -0
- package/build/dist/Models/DatabaseModels/TelemetryEntity.js +47 -0
- package/build/dist/Models/DatabaseModels/TelemetryEntity.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js +27 -0
- package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkflowLog.js +39 -0
- package/build/dist/Models/DatabaseModels/WorkflowLog.js.map +1 -1
- package/build/dist/Server/API/UserAPI.js +16 -1
- package/build/dist/Server/API/UserAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.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/Middleware/MasterAdminAuthorization.js +11 -6
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +3 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityService.js +91 -1
- package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Utils.js +28 -0
- package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
- package/build/dist/Server/Types/Workflow/TriggerCode.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +507 -0
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -0
- package/build/dist/Server/Utils/VM/VMAPI.js +51 -4
- package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +133 -39
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/Types/Telemetry/EntitySource.js +39 -0
- package/build/dist/Types/Telemetry/EntitySource.js.map +1 -0
- package/build/dist/Types/Telemetry/EntityType.js +27 -0
- package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
- package/build/dist/Types/Telemetry/EntityTypeGroups.js +57 -0
- package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -0
- package/build/dist/Types/Workflow/Component.js +21 -0
- package/build/dist/Types/Workflow/Component.js.map +1 -1
- package/build/dist/Types/Workflow/Components/API.js +35 -0
- package/build/dist/Types/Workflow/Components/API.js.map +1 -1
- package/build/dist/Types/Workflow/Components/BaseModel.js +7 -3
- package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
- package/build/dist/Types/Workflow/StepTrace.js +104 -0
- package/build/dist/Types/Workflow/StepTrace.js.map +1 -0
- package/build/dist/Types/Workflow/TemplateSyntax.js +326 -0
- package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -0
- package/build/dist/Types/Workflow/Templates.js +218 -0
- package/build/dist/Types/Workflow/Templates.js.map +1 -0
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +126 -0
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -0
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +123 -0
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -0
- package/build/dist/UI/Components/Dictionary/Dictionary.js +22 -6
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +52 -13
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js +28 -12
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +44 -0
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +12 -4
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +5 -108
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +31 -5
- package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +5 -105
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +140 -11
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Component.js +20 -19
- package/build/dist/UI/Components/Workflow/Component.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +31 -3
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/GraphLint.js +396 -0
- package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +225 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelSchema.js +118 -0
- package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -0
- package/build/dist/UI/Components/Workflow/RunForm.js +22 -6
- package/build/dist/UI/Components/Workflow/RunForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +76 -0
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -0
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js +76 -0
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -0
- package/build/dist/UI/Components/Workflow/Utils.js +73 -1
- package/build/dist/UI/Components/Workflow/Utils.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Workflow.js +51 -6
- package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
- package/build/dist/UI/Types/LayeredDismissal.js +21 -0
- package/build/dist/UI/Types/LayeredDismissal.js.map +1 -0
- package/build/dist/UI/Types/UseAnchoredFieldPopup.js +74 -1
- package/build/dist/UI/Types/UseAnchoredFieldPopup.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +386 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +35 -6
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/EntityKey.js +52 -0
- package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import TelemetryEntity from "../../../../Models/DatabaseModels/TelemetryEntity";
|
|
2
|
+
import {
|
|
3
|
+
buildInventoryEntityModel,
|
|
4
|
+
compactAttributes,
|
|
5
|
+
ErasedInventorySource,
|
|
6
|
+
INVENTORY_SOURCES,
|
|
7
|
+
inventoryEntityNeedsUpdate,
|
|
8
|
+
InventoryRowProjection,
|
|
9
|
+
} from "../../../../Server/Utils/Telemetry/InventoryEntityRegistry";
|
|
10
|
+
import ColumnLength from "../../../../Types/Database/ColumnLength";
|
|
11
|
+
import Dictionary from "../../../../Types/Dictionary";
|
|
12
|
+
import ObjectID from "../../../../Types/ObjectID";
|
|
13
|
+
import EntitySource from "../../../../Types/Telemetry/EntitySource";
|
|
14
|
+
import EntityType from "../../../../Types/Telemetry/EntityType";
|
|
15
|
+
import { INVENTORY_ENTITY_TYPES } from "../../../../Types/Telemetry/EntityTypeGroups";
|
|
16
|
+
import { keyForInventoryEntity } from "../../../../Utils/Telemetry/EntityKey";
|
|
17
|
+
import { describe, expect, test } from "@jest/globals";
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* The pure half of the inventory mirror — the mapping from an inventory row
|
|
21
|
+
* to its registry row, and the drift check that decides whether to write.
|
|
22
|
+
*
|
|
23
|
+
* Worth testing closely for two reasons. A mistake in the mapping mints a
|
|
24
|
+
* duplicate entity that looks entirely correct, and a mistake in the drift
|
|
25
|
+
* check either writes on every reconcile (seven table scans' worth of
|
|
26
|
+
* needless UPDATEs every fifteen minutes) or never writes at all (renames
|
|
27
|
+
* never propagate).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
31
|
+
"0f8b9c0d-e1a2-4b3c-8d5e-6f7a8b9c0d1e",
|
|
32
|
+
);
|
|
33
|
+
const RESOURCE_ID: ObjectID = new ObjectID(
|
|
34
|
+
"6f2e5c1a-0b3d-4e5f-8a9b-1c2d3e4f5a6b",
|
|
35
|
+
);
|
|
36
|
+
const NOW: Date = new Date("2026-03-04T05:06:07.000Z");
|
|
37
|
+
|
|
38
|
+
function projection(
|
|
39
|
+
overrides: Partial<InventoryRowProjection> = {},
|
|
40
|
+
): InventoryRowProjection {
|
|
41
|
+
return {
|
|
42
|
+
id: RESOURCE_ID,
|
|
43
|
+
projectId: PROJECT_ID,
|
|
44
|
+
displayName: "core-switch-1",
|
|
45
|
+
descriptiveAttributes: { "net.device.hostname": "core-switch-1.dc1" },
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const NETWORK_SOURCE: Pick<
|
|
51
|
+
ErasedInventorySource,
|
|
52
|
+
"entityType" | "resourceType"
|
|
53
|
+
> = {
|
|
54
|
+
entityType: EntityType.NetworkDevice,
|
|
55
|
+
resourceType: "NetworkDevice",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
describe("compactAttributes", () => {
|
|
59
|
+
test("keeps ordinary values", () => {
|
|
60
|
+
expect(compactAttributes({ a: "1", b: "two" })).toEqual({
|
|
61
|
+
a: "1",
|
|
62
|
+
b: "two",
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("drops undefined, null, empty and whitespace-only values", () => {
|
|
67
|
+
const out: Dictionary<string> = compactAttributes({
|
|
68
|
+
keep: "yes",
|
|
69
|
+
undef: undefined,
|
|
70
|
+
nul: null,
|
|
71
|
+
empty: "",
|
|
72
|
+
blank: " ",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(out).toEqual({ keep: "yes" });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("trims surviving values", () => {
|
|
79
|
+
expect(compactAttributes({ a: " spaced " })).toEqual({ a: "spaced" });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("returns an empty object when everything is dropped", () => {
|
|
83
|
+
expect(compactAttributes({ a: undefined, b: "" })).toEqual({});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("buildInventoryEntityModel", () => {
|
|
88
|
+
test("marks the row as inventory-sourced", () => {
|
|
89
|
+
const model: TelemetryEntity = buildInventoryEntityModel({
|
|
90
|
+
source: NETWORK_SOURCE,
|
|
91
|
+
row: projection(),
|
|
92
|
+
now: NOW,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* The single most important assertion in this file: PruneStaleEntities
|
|
97
|
+
* exempts rows by source, so a mirror that came out marked `discovered`
|
|
98
|
+
* would be deleted the first time the sweep ran past its TTL.
|
|
99
|
+
*/
|
|
100
|
+
expect(model.source).toBe(EntitySource.Inventory);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("derives the key from the owning row id, matching keyForInventoryEntity", () => {
|
|
104
|
+
const model: TelemetryEntity = buildInventoryEntityModel({
|
|
105
|
+
source: NETWORK_SOURCE,
|
|
106
|
+
row: projection(),
|
|
107
|
+
now: NOW,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(model.entityKey).toBe(
|
|
111
|
+
keyForInventoryEntity(
|
|
112
|
+
PROJECT_ID.toString(),
|
|
113
|
+
EntityType.NetworkDevice,
|
|
114
|
+
RESOURCE_ID.toString(),
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("a rename does not change identity", () => {
|
|
120
|
+
const before: TelemetryEntity = buildInventoryEntityModel({
|
|
121
|
+
source: NETWORK_SOURCE,
|
|
122
|
+
row: projection({ displayName: "core-switch-1" }),
|
|
123
|
+
now: NOW,
|
|
124
|
+
});
|
|
125
|
+
const after: TelemetryEntity = buildInventoryEntityModel({
|
|
126
|
+
source: NETWORK_SOURCE,
|
|
127
|
+
row: projection({ displayName: "core-switch-1-renamed" }),
|
|
128
|
+
now: NOW,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(after.entityKey).toBe(before.entityKey);
|
|
132
|
+
expect(after.displayName).not.toBe(before.displayName);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("populates the polymorphic pointer back to the owning row", () => {
|
|
136
|
+
const model: TelemetryEntity = buildInventoryEntityModel({
|
|
137
|
+
source: NETWORK_SOURCE,
|
|
138
|
+
row: projection(),
|
|
139
|
+
now: NOW,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(model.resourceType).toBe("NetworkDevice");
|
|
143
|
+
expect(model.resourceId?.toString()).toBe(RESOURCE_ID.toString());
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("carries project, type, attributes and timestamps through", () => {
|
|
147
|
+
const model: TelemetryEntity = buildInventoryEntityModel({
|
|
148
|
+
source: NETWORK_SOURCE,
|
|
149
|
+
row: projection(),
|
|
150
|
+
now: NOW,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(model.projectId?.toString()).toBe(PROJECT_ID.toString());
|
|
154
|
+
expect(model.entityType).toBe(EntityType.NetworkDevice);
|
|
155
|
+
expect(model.descriptiveAttributes).toEqual({
|
|
156
|
+
"net.device.hostname": "core-switch-1.dc1",
|
|
157
|
+
});
|
|
158
|
+
expect(model.firstSeenAt).toEqual(NOW);
|
|
159
|
+
expect(model.lastSeenAt).toEqual(NOW);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("records the owning row id as the identifying attribute", () => {
|
|
163
|
+
const model: TelemetryEntity = buildInventoryEntityModel({
|
|
164
|
+
source: NETWORK_SOURCE,
|
|
165
|
+
row: projection(),
|
|
166
|
+
now: NOW,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(model.identifyingAttributes).toEqual({
|
|
170
|
+
"oneuptime.resource.id": RESOURCE_ID.toString(),
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("clamps displayName to the column width", () => {
|
|
175
|
+
/*
|
|
176
|
+
* displayName is a bounded varchar; an over-long name would otherwise
|
|
177
|
+
* fail the INSERT with a 22001 and lose the row entirely.
|
|
178
|
+
*/
|
|
179
|
+
const model: TelemetryEntity = buildInventoryEntityModel({
|
|
180
|
+
source: NETWORK_SOURCE,
|
|
181
|
+
row: projection({ displayName: "n".repeat(ColumnLength.ShortText + 50) }),
|
|
182
|
+
now: NOW,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(model.displayName!.length).toBe(ColumnLength.ShortText);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("distinct rows of one type produce distinct keys", () => {
|
|
189
|
+
const first: TelemetryEntity = buildInventoryEntityModel({
|
|
190
|
+
source: NETWORK_SOURCE,
|
|
191
|
+
row: projection(),
|
|
192
|
+
now: NOW,
|
|
193
|
+
});
|
|
194
|
+
const second: TelemetryEntity = buildInventoryEntityModel({
|
|
195
|
+
source: NETWORK_SOURCE,
|
|
196
|
+
row: projection({
|
|
197
|
+
id: new ObjectID("11111111-2222-3333-4444-555555555555"),
|
|
198
|
+
}),
|
|
199
|
+
now: NOW,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(first.entityKey).not.toBe(second.entityKey);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("the same row id under two types produces distinct keys", () => {
|
|
206
|
+
const asDevice: TelemetryEntity = buildInventoryEntityModel({
|
|
207
|
+
source: NETWORK_SOURCE,
|
|
208
|
+
row: projection(),
|
|
209
|
+
now: NOW,
|
|
210
|
+
});
|
|
211
|
+
const asIoT: TelemetryEntity = buildInventoryEntityModel({
|
|
212
|
+
source: { entityType: EntityType.IoTDevice, resourceType: "IoTDevice" },
|
|
213
|
+
row: projection(),
|
|
214
|
+
now: NOW,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(asDevice.entityKey).not.toBe(asIoT.entityKey);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe("inventoryEntityNeedsUpdate", () => {
|
|
222
|
+
function pair(existingOverrides: Partial<InventoryRowProjection>): {
|
|
223
|
+
existing: TelemetryEntity;
|
|
224
|
+
desired: TelemetryEntity;
|
|
225
|
+
} {
|
|
226
|
+
return {
|
|
227
|
+
existing: buildInventoryEntityModel({
|
|
228
|
+
source: NETWORK_SOURCE,
|
|
229
|
+
row: projection(existingOverrides),
|
|
230
|
+
now: NOW,
|
|
231
|
+
}),
|
|
232
|
+
desired: buildInventoryEntityModel({
|
|
233
|
+
source: NETWORK_SOURCE,
|
|
234
|
+
row: projection(),
|
|
235
|
+
now: NOW,
|
|
236
|
+
}),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
test("false in the steady state, so a quiet reconcile writes nothing", () => {
|
|
241
|
+
const { existing, desired } = pair({});
|
|
242
|
+
expect(inventoryEntityNeedsUpdate({ existing, desired })).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("true when the row was renamed", () => {
|
|
246
|
+
const { existing, desired } = pair({ displayName: "old-name" });
|
|
247
|
+
expect(inventoryEntityNeedsUpdate({ existing, desired })).toBe(true);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("true when descriptive attributes changed", () => {
|
|
251
|
+
const { existing, desired } = pair({
|
|
252
|
+
descriptiveAttributes: { "net.device.hostname": "old-host" },
|
|
253
|
+
});
|
|
254
|
+
expect(inventoryEntityNeedsUpdate({ existing, desired })).toBe(true);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("true when an attribute was added", () => {
|
|
258
|
+
const { existing, desired } = pair({ descriptiveAttributes: {} });
|
|
259
|
+
expect(inventoryEntityNeedsUpdate({ existing, desired })).toBe(true);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("true when the polymorphic pointer is missing", () => {
|
|
263
|
+
const { existing, desired } = pair({});
|
|
264
|
+
// Deleted rather than assigned undefined: exactOptionalPropertyTypes.
|
|
265
|
+
delete (existing as { resourceId?: ObjectID }).resourceId;
|
|
266
|
+
expect(inventoryEntityNeedsUpdate({ existing, desired })).toBe(true);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("true when the pointer type is wrong", () => {
|
|
270
|
+
const { existing, desired } = pair({});
|
|
271
|
+
existing.resourceType = "SomethingElse";
|
|
272
|
+
expect(inventoryEntityNeedsUpdate({ existing, desired })).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("ignores lastSeenAt, which never advances for mirrored rows", () => {
|
|
276
|
+
const { existing, desired } = pair({});
|
|
277
|
+
existing.lastSeenAt = new Date("2020-01-01T00:00:00.000Z");
|
|
278
|
+
expect(inventoryEntityNeedsUpdate({ existing, desired })).toBe(false);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("INVENTORY_SOURCES", () => {
|
|
283
|
+
test("covers exactly the declared inventory entity types", () => {
|
|
284
|
+
const covered: Array<EntityType> = INVENTORY_SOURCES.map(
|
|
285
|
+
(source: ErasedInventorySource) => {
|
|
286
|
+
return source.entityType;
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
expect(covered.sort()).toEqual(Array.from(INVENTORY_ENTITY_TYPES).sort());
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("registers each entity type exactly once", () => {
|
|
294
|
+
const seen: Set<EntityType> = new Set<EntityType>();
|
|
295
|
+
|
|
296
|
+
for (const source of INVENTORY_SOURCES) {
|
|
297
|
+
expect(seen.has(source.entityType)).toBe(false);
|
|
298
|
+
seen.add(source.entityType);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("registers each resourceType exactly once", () => {
|
|
303
|
+
const seen: Set<string> = new Set<string>();
|
|
304
|
+
|
|
305
|
+
for (const source of INVENTORY_SOURCES) {
|
|
306
|
+
expect(seen.has(source.resourceType)).toBe(false);
|
|
307
|
+
seen.add(source.resourceType);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("every source names a non-empty resourceType", () => {
|
|
312
|
+
for (const source of INVENTORY_SOURCES) {
|
|
313
|
+
expect(source.resourceType.length).toBeGreaterThan(0);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("no source mirrors a manually creatable type", () => {
|
|
318
|
+
for (const source of INVENTORY_SOURCES) {
|
|
319
|
+
expect(INVENTORY_ENTITY_TYPES.has(source.entityType)).toBe(true);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
});
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Substitution behaviour of VMUtil.replaceValueInPlace, focused on the three
|
|
3
|
+
* ways a resolved value used to come out wrong: unescaped quotes when the
|
|
4
|
+
* caller passed an object, `$`-patterns in the replacement text, and
|
|
5
|
+
* `[last]` against a key that is not an array.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
jest.mock("../../../../Server/Utils/VM/VMRunner", () => {
|
|
9
|
+
return {
|
|
10
|
+
__esModule: true,
|
|
11
|
+
default: {
|
|
12
|
+
runCodeInSandbox: jest.fn(),
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
jest.mock("../../../../Server/Utils/Logger", () => {
|
|
18
|
+
return {
|
|
19
|
+
__esModule: true,
|
|
20
|
+
default: {
|
|
21
|
+
error: jest.fn(),
|
|
22
|
+
debug: jest.fn(),
|
|
23
|
+
info: jest.fn(),
|
|
24
|
+
warn: jest.fn(),
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
jest.mock("../../../../Server/Utils/Telemetry/CaptureSpan", () => {
|
|
30
|
+
return {
|
|
31
|
+
__esModule: true,
|
|
32
|
+
default: () => {
|
|
33
|
+
return (
|
|
34
|
+
_target: any,
|
|
35
|
+
_propertyKey: string,
|
|
36
|
+
descriptor: PropertyDescriptor,
|
|
37
|
+
) => {
|
|
38
|
+
return descriptor;
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
import VMUtil from "../../../../Server/Utils/VM/VMAPI";
|
|
45
|
+
import { JSONObject, JSONValue } from "../../../../Types/JSON";
|
|
46
|
+
import { describe, expect, it } from "@jest/globals";
|
|
47
|
+
|
|
48
|
+
type MakeStorageFunction = (variables: JSONObject) => JSONObject;
|
|
49
|
+
|
|
50
|
+
const makeStorage: MakeStorageFunction = (
|
|
51
|
+
variables: JSONObject,
|
|
52
|
+
): JSONObject => {
|
|
53
|
+
return {
|
|
54
|
+
local: {
|
|
55
|
+
variables: variables,
|
|
56
|
+
components: {},
|
|
57
|
+
},
|
|
58
|
+
global: {
|
|
59
|
+
variables: {},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
describe("replaceValueInPlace — object input (the key/value headers shape)", () => {
|
|
65
|
+
it("returns an object, not a string", () => {
|
|
66
|
+
const result: JSONValue = VMUtil.replaceValueInPlace(
|
|
67
|
+
makeStorage({ token: "abc" }),
|
|
68
|
+
{ Authorization: "Bearer {{local.variables.token}}" } as never,
|
|
69
|
+
false,
|
|
70
|
+
) as unknown as JSONValue;
|
|
71
|
+
|
|
72
|
+
expect(typeof result).toBe("object");
|
|
73
|
+
expect(result).toEqual({ Authorization: "Bearer abc" });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* The regression this guards: the object is stringified, so every
|
|
78
|
+
* placeholder sits inside a JSON string literal. A resolved value carrying a
|
|
79
|
+
* quote used to be spliced in raw, the JSON.parse on the way back out threw,
|
|
80
|
+
* and the caller silently received a corrupted string where it asked for an
|
|
81
|
+
* object — which then spread into per-character HTTP headers.
|
|
82
|
+
*/
|
|
83
|
+
it("escapes a resolved value containing a double quote", () => {
|
|
84
|
+
const result: JSONValue = VMUtil.replaceValueInPlace(
|
|
85
|
+
makeStorage({ note: 'he said "hi"' }),
|
|
86
|
+
{ "X-Note": "{{local.variables.note}}" } as never,
|
|
87
|
+
false,
|
|
88
|
+
) as unknown as JSONValue;
|
|
89
|
+
|
|
90
|
+
expect(typeof result).toBe("object");
|
|
91
|
+
expect(result).toEqual({ "X-Note": 'he said "hi"' });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("escapes a resolved value containing a newline", () => {
|
|
95
|
+
const result: JSONValue = VMUtil.replaceValueInPlace(
|
|
96
|
+
makeStorage({ note: "line1\nline2" }),
|
|
97
|
+
{ "X-Note": "{{local.variables.note}}" } as never,
|
|
98
|
+
false,
|
|
99
|
+
) as unknown as JSONValue;
|
|
100
|
+
|
|
101
|
+
expect(typeof result).toBe("object");
|
|
102
|
+
expect(result).toEqual({ "X-Note": "line1\nline2" });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("escapes a resolved value containing a backslash", () => {
|
|
106
|
+
const result: JSONValue = VMUtil.replaceValueInPlace(
|
|
107
|
+
makeStorage({ path: "C:\\Users" }),
|
|
108
|
+
{ "X-Path": "{{local.variables.path}}" } as never,
|
|
109
|
+
false,
|
|
110
|
+
) as unknown as JSONValue;
|
|
111
|
+
|
|
112
|
+
expect(typeof result).toBe("object");
|
|
113
|
+
expect((result as JSONObject)["X-Path"]).toBe("C:\\Users");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("escapes a resolved value containing a regex", () => {
|
|
117
|
+
const result: JSONValue = VMUtil.replaceValueInPlace(
|
|
118
|
+
makeStorage({ pattern: "\\d+\\s" }),
|
|
119
|
+
{ "X-Pattern": "{{local.variables.pattern}}" } as never,
|
|
120
|
+
false,
|
|
121
|
+
) as unknown as JSONValue;
|
|
122
|
+
|
|
123
|
+
expect((result as JSONObject)["X-Pattern"]).toBe("\\d+\\s");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("escapes a value carrying a backslash and a quote together", () => {
|
|
127
|
+
const result: JSONValue = VMUtil.replaceValueInPlace(
|
|
128
|
+
makeStorage({ v: 'a\\b"c' }),
|
|
129
|
+
{ "X-V": "{{local.variables.v}}" } as never,
|
|
130
|
+
false,
|
|
131
|
+
) as unknown as JSONValue;
|
|
132
|
+
|
|
133
|
+
expect((result as JSONObject)["X-V"]).toBe('a\\b"c');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("leaves an object with no placeholders untouched", () => {
|
|
137
|
+
const result: JSONValue = VMUtil.replaceValueInPlace(
|
|
138
|
+
makeStorage({}),
|
|
139
|
+
{ A: "b" } as never,
|
|
140
|
+
false,
|
|
141
|
+
) as unknown as JSONValue;
|
|
142
|
+
|
|
143
|
+
expect(result).toEqual({ A: "b" });
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("replaceValueInPlace — $ in resolved values", () => {
|
|
148
|
+
/*
|
|
149
|
+
* String.replace treats $&, $', $` and $1 in the REPLACEMENT as substitution
|
|
150
|
+
* patterns. A resolved value of "A$&B" used to render as "A{{v}}B", pasting
|
|
151
|
+
* the matched placeholder back into the output.
|
|
152
|
+
*/
|
|
153
|
+
it("does not treat $& in a value as a substitution pattern", () => {
|
|
154
|
+
const result: string = VMUtil.replaceValueInPlace(
|
|
155
|
+
makeStorage({ v: "A$&B" }),
|
|
156
|
+
"start {{local.variables.v}} end",
|
|
157
|
+
false,
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(result).toBe("start A$&B end");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("does not treat $` or $' as substitution patterns", () => {
|
|
164
|
+
expect(
|
|
165
|
+
VMUtil.replaceValueInPlace(
|
|
166
|
+
makeStorage({ v: "x$`y" }),
|
|
167
|
+
"a {{local.variables.v}} b",
|
|
168
|
+
false,
|
|
169
|
+
),
|
|
170
|
+
).toBe("a x$`y b");
|
|
171
|
+
|
|
172
|
+
expect(
|
|
173
|
+
VMUtil.replaceValueInPlace(
|
|
174
|
+
makeStorage({ v: "x$'y" }),
|
|
175
|
+
"a {{local.variables.v}} b",
|
|
176
|
+
false,
|
|
177
|
+
),
|
|
178
|
+
).toBe("a x$'y b");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("keeps a plain dollar amount intact", () => {
|
|
182
|
+
expect(
|
|
183
|
+
VMUtil.replaceValueInPlace(
|
|
184
|
+
makeStorage({ price: "$50" }),
|
|
185
|
+
"Total: {{local.variables.price}} today",
|
|
186
|
+
false,
|
|
187
|
+
),
|
|
188
|
+
).toBe("Total: $50 today");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("deepFind — [last] accessor", () => {
|
|
193
|
+
type FindFunction = (storage: JSONObject, path: string) => JSONValue;
|
|
194
|
+
|
|
195
|
+
const find: FindFunction = (storage: JSONObject, path: string): JSONValue => {
|
|
196
|
+
return VMUtil.deepFind(storage, path);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
it("resolves the final element of an array", () => {
|
|
200
|
+
expect(find({ items: ["a", "b", "c"] }, "items[last]")).toBe("c");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("returns undefined rather than throwing when the key is missing", () => {
|
|
204
|
+
expect(find({}, "items[last]")).toBeUndefined();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("returns undefined rather than throwing when the key is not an array", () => {
|
|
208
|
+
expect(find({ items: "not an array" }, "items[last]")).toBeUndefined();
|
|
209
|
+
expect(find({ items: 5 }, "items[last]")).toBeUndefined();
|
|
210
|
+
expect(find({ items: null }, "items[last]")).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("returns undefined for an empty array", () => {
|
|
214
|
+
expect(find({ items: [] }, "items[last]")).toBeUndefined();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("still resolves a numeric accessor", () => {
|
|
218
|
+
expect(find({ items: ["a", "b"] }, "items[0]")).toBe("a");
|
|
219
|
+
expect(find({ items: ["a", "b"] }, "items[9]")).toBeUndefined();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("replaceValueInPlace — unresolved references", () => {
|
|
224
|
+
it("leaves the braces in place, which is what the builder warns about", () => {
|
|
225
|
+
expect(
|
|
226
|
+
VMUtil.replaceValueInPlace(
|
|
227
|
+
makeStorage({}),
|
|
228
|
+
"hello {{local.variables.missing}}",
|
|
229
|
+
false,
|
|
230
|
+
),
|
|
231
|
+
).toBe("hello {{local.variables.missing}}");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("does not resolve a reference padded with spaces", () => {
|
|
235
|
+
expect(
|
|
236
|
+
VMUtil.replaceValueInPlace(
|
|
237
|
+
makeStorage({ v: "x" }),
|
|
238
|
+
"a {{ local.variables.v }} b",
|
|
239
|
+
false,
|
|
240
|
+
),
|
|
241
|
+
).toBe("a {{ local.variables.v }} b");
|
|
242
|
+
});
|
|
243
|
+
});
|