@oneuptime/common 12.0.0 → 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/OnCallDutyPolicyScheduleLayer.ts +12 -2
- 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/LocalMigrationGenerationDataSource.ts +12 -2
- 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/LlmProviderService.ts +69 -9
- 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/RunnerService.ts +1 -7
- 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/Server/Utils/Monitor/Criteria/CompareCriteria.ts +12 -0
- package/Tests/App/Dashboard/RunnerInstallInstructions.test.tsx +119 -0
- package/Tests/App/Dashboard/RunnerStatus.test.tsx +274 -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/LlmProviderUsableByProject.test.ts +293 -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/AI/ToolArgsExtractors.test.ts +141 -0
- package/Tests/Server/Utils/AI/Toolbox/WidgetBuilder.test.ts +205 -0
- package/Tests/Server/Utils/Database/TruncateColumnValue.test.ts +179 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +897 -0
- package/Tests/Types/Events/Recurring.test.ts +157 -0
- package/Tests/Types/Runner/RunnerLiveStatus.test.ts +320 -0
- package/Tests/UI/Components/AiInvestigationSettingsCard.test.tsx +285 -0
- package/Tests/UI/Components/Detail/EntityFields.test.tsx +166 -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/Runbook/RunbookStep.ts +15 -0
- package/Types/Runner/RunnerLiveStatus.ts +114 -0
- package/Types/StatusPage/StatusPageReport.ts +11 -0
- package/Types/StatusPage/StatusPageReportPeriodType.ts +21 -0
- package/UI/Components/Detail/Detail.tsx +155 -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/OnCallDutyPolicyScheduleLayer.js +16 -4
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.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/LocalMigrationGenerationDataSource.js +11 -1
- package/build/dist/Server/Infrastructure/Postgres/LocalMigrationGenerationDataSource.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/LlmProviderService.js +61 -8
- package/build/dist/Server/Services/LlmProviderService.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/RunnerService.js +1 -6
- package/build/dist/Server/Services/RunnerService.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/Server/Utils/Monitor/Criteria/CompareCriteria.js +11 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- 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/Runbook/RunbookStep.js.map +1 -1
- package/build/dist/Types/Runner/RunnerLiveStatus.js +69 -0
- package/build/dist/Types/Runner/RunnerLiveStatus.js.map +1 -0
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js +22 -0
- package/build/dist/Types/StatusPage/StatusPageReportPeriodType.js.map +1 -0
- package/build/dist/UI/Components/Detail/Detail.js +97 -0
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- 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,510 @@
|
|
|
1
|
+
import GlobalCache from "../../../Server/Infrastructure/GlobalCache";
|
|
2
|
+
import HostService from "../../../Server/Services/HostService";
|
|
3
|
+
import Host from "../../../Models/DatabaseModels/Host";
|
|
4
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Regression suite for issue #3006.
|
|
9
|
+
*
|
|
10
|
+
* HostService.updateLastSeen writes liveness (lastSeenAt +
|
|
11
|
+
* otelCollectorStatus) in the SAME hook-free UPDATE as a dozen optional
|
|
12
|
+
* columns harvested from OpenTelemetry resource attributes. Because that
|
|
13
|
+
* raw path bypasses DatabaseService's length validation, an oversized
|
|
14
|
+
* attribute reached Postgres unchecked and failed the whole statement —
|
|
15
|
+
* so a host with telemetry still arriving stopped advancing lastSeenAt and
|
|
16
|
+
* MarkDisconnectedHosts flipped it to "disconnected" 15 minutes later.
|
|
17
|
+
*
|
|
18
|
+
* Two independent guards are pinned here:
|
|
19
|
+
* 1. collector-supplied strings are clamped to their column widths, so
|
|
20
|
+
* the statement does not fail in the first place, and
|
|
21
|
+
* 2. if the enriched write fails ANYWAY (for any reason at all), the
|
|
22
|
+
* liveness columns are retried on their own. Enrichment is what gets
|
|
23
|
+
* dropped, never the heartbeat.
|
|
24
|
+
*
|
|
25
|
+
* Guard 1 has since moved DOWN a layer: updateColumnsByIdWithoutHooks
|
|
26
|
+
* clamps for every caller, so no service can forget it (see
|
|
27
|
+
* HookFreeWriteLengthClamp.test.ts, which pins that for all eleven
|
|
28
|
+
* telemetry models). The clamp assertions below therefore run the real
|
|
29
|
+
* write path against a faked repository and inspect the values actually
|
|
30
|
+
* BOUND to the SQL — the same end-to-end claim, checked where it is now
|
|
31
|
+
* true rather than where it used to be.
|
|
32
|
+
*
|
|
33
|
+
* Everything external is mocked — no Postgres, no Redis.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const HOST_ID: ObjectID = ObjectID.generate();
|
|
37
|
+
|
|
38
|
+
/** Longer than every ShortText column (100) and the old LongText bound (500). */
|
|
39
|
+
const HUGE_IP_LIST: string = Array.from(
|
|
40
|
+
{ length: 55 },
|
|
41
|
+
(_unused: unknown, i: number) => {
|
|
42
|
+
return `fe80::42:acff:fe11:${(0x1000 + i).toString(16)}`;
|
|
43
|
+
},
|
|
44
|
+
).join(", ");
|
|
45
|
+
|
|
46
|
+
type WriteCall = { id: ObjectID; data: Record<string, unknown> };
|
|
47
|
+
|
|
48
|
+
/** The (inline) shape of updateLastSeen's optional metadata argument. */
|
|
49
|
+
type UpdateLastSeenExtra = Parameters<typeof HostService.updateLastSeen>[1];
|
|
50
|
+
|
|
51
|
+
/** The argument to the hook-free UPDATE the service ultimately issues. */
|
|
52
|
+
type ColumnWrite = Parameters<
|
|
53
|
+
typeof HostService.updateColumnsByIdWithoutHooks
|
|
54
|
+
>[0];
|
|
55
|
+
|
|
56
|
+
let writes: Array<WriteCall> = [];
|
|
57
|
+
let cache: Map<string, string> = new Map<string, string>();
|
|
58
|
+
let deletedCacheKeys: Array<string> = [];
|
|
59
|
+
|
|
60
|
+
function mockCache(): void {
|
|
61
|
+
jest
|
|
62
|
+
.spyOn(GlobalCache, "getString")
|
|
63
|
+
.mockImplementation(async (namespace: string, key: string) => {
|
|
64
|
+
return cache.get(`${namespace}:${key}`) ?? null;
|
|
65
|
+
});
|
|
66
|
+
jest
|
|
67
|
+
.spyOn(GlobalCache, "setString")
|
|
68
|
+
.mockImplementation(
|
|
69
|
+
async (namespace: string, key: string, value: string) => {
|
|
70
|
+
cache.set(`${namespace}:${key}`, value);
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
jest
|
|
74
|
+
.spyOn(GlobalCache, "deleteKey")
|
|
75
|
+
.mockImplementation(async (namespace: string, key: string) => {
|
|
76
|
+
deletedCacheKeys.push(`${namespace}:${key}`);
|
|
77
|
+
cache.delete(`${namespace}:${key}`);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function recordWrite(input: {
|
|
82
|
+
id: ObjectID;
|
|
83
|
+
data: unknown;
|
|
84
|
+
}): Record<string, unknown> {
|
|
85
|
+
const data: Record<string, unknown> = {
|
|
86
|
+
...(input.data as Record<string, unknown>),
|
|
87
|
+
};
|
|
88
|
+
writes.push({ id: input.id, data: data });
|
|
89
|
+
return data;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Records every hook-free UPDATE instead of issuing it. */
|
|
93
|
+
function mockWritesSucceeding(): void {
|
|
94
|
+
jest
|
|
95
|
+
.spyOn(HostService, "updateColumnsByIdWithoutHooks")
|
|
96
|
+
.mockImplementation(async (input: ColumnWrite) => {
|
|
97
|
+
recordWrite(input);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Fails any write that carries more than the two liveness columns — the
|
|
103
|
+
* shape of a Postgres rejection caused by one bad optional column.
|
|
104
|
+
*/
|
|
105
|
+
function mockEnrichedWriteFailing(
|
|
106
|
+
message: string = `value too long for type character varying(100)`,
|
|
107
|
+
): void {
|
|
108
|
+
jest
|
|
109
|
+
.spyOn(HostService, "updateColumnsByIdWithoutHooks")
|
|
110
|
+
.mockImplementation(async (input: ColumnWrite) => {
|
|
111
|
+
const data: Record<string, unknown> = recordWrite(input);
|
|
112
|
+
if (Object.keys(data).length > 2) {
|
|
113
|
+
throw new Error(message);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Runs the REAL updateColumnsByIdWithoutHooks against a faked repository,
|
|
120
|
+
* recording the values actually BOUND to the generated SQL. The length
|
|
121
|
+
* clamp lives inside that method now, so this is the only way to observe
|
|
122
|
+
* it end-to-end from the service without a database — mocking the method
|
|
123
|
+
* out (as the other blocks do) would mock the clamp out with it.
|
|
124
|
+
*/
|
|
125
|
+
function mockRealWritePath(): void {
|
|
126
|
+
const host: Host = new Host();
|
|
127
|
+
let bound: Record<string, unknown> = {};
|
|
128
|
+
|
|
129
|
+
jest.spyOn(HostService, "getRepository").mockReturnValue({
|
|
130
|
+
metadata: {
|
|
131
|
+
tableName: "Host",
|
|
132
|
+
findColumnWithPropertyName: (
|
|
133
|
+
propertyName: string,
|
|
134
|
+
): { databaseName: string } | undefined => {
|
|
135
|
+
return host.hasColumn(propertyName)
|
|
136
|
+
? { databaseName: propertyName }
|
|
137
|
+
: undefined;
|
|
138
|
+
},
|
|
139
|
+
updateDateColumn: { databaseName: "updatedAt" },
|
|
140
|
+
primaryColumns: [{ databaseName: "_id" }],
|
|
141
|
+
},
|
|
142
|
+
manager: {
|
|
143
|
+
connection: {
|
|
144
|
+
driver: {
|
|
145
|
+
preparePersistentValue: (
|
|
146
|
+
value: unknown,
|
|
147
|
+
column: { databaseName: string },
|
|
148
|
+
): unknown => {
|
|
149
|
+
bound[column.databaseName] = value;
|
|
150
|
+
return value;
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
query: async (
|
|
155
|
+
_sql: string,
|
|
156
|
+
params: Array<unknown>,
|
|
157
|
+
): Promise<Array<unknown>> => {
|
|
158
|
+
writes.push({
|
|
159
|
+
id: new ObjectID(String(params[params.length - 1])),
|
|
160
|
+
data: bound,
|
|
161
|
+
});
|
|
162
|
+
bound = {};
|
|
163
|
+
return [];
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
167
|
+
} as any);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function lastWrite(): Record<string, unknown> {
|
|
171
|
+
return writes[writes.length - 1]?.data as Record<string, unknown>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
describe("HostService.updateLastSeen", () => {
|
|
175
|
+
beforeEach(() => {
|
|
176
|
+
writes = [];
|
|
177
|
+
cache = new Map<string, string>();
|
|
178
|
+
deletedCacheKeys = [];
|
|
179
|
+
mockCache();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
afterEach(() => {
|
|
183
|
+
jest.restoreAllMocks();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("liveness columns", () => {
|
|
187
|
+
beforeEach(() => {
|
|
188
|
+
mockWritesSucceeding();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("always writes lastSeenAt and marks the collector connected", async () => {
|
|
192
|
+
await HostService.updateLastSeen(HOST_ID);
|
|
193
|
+
|
|
194
|
+
expect(writes).toHaveLength(1);
|
|
195
|
+
expect(lastWrite()["otelCollectorStatus"]).toBe("connected");
|
|
196
|
+
expect(lastWrite()["lastSeenAt"]).toBeInstanceOf(Date);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("writes to the host it was given", async () => {
|
|
200
|
+
await HostService.updateLastSeen(HOST_ID);
|
|
201
|
+
expect(writes[0]?.id.toString()).toBe(HOST_ID.toString());
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("skips the write when the same metadata was written recently", async () => {
|
|
205
|
+
await HostService.updateLastSeen(HOST_ID, { osType: "linux" });
|
|
206
|
+
await HostService.updateLastSeen(HOST_ID, { osType: "linux" });
|
|
207
|
+
|
|
208
|
+
expect(writes).toHaveLength(1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("writes again as soon as any metadata changes", async () => {
|
|
212
|
+
await HostService.updateLastSeen(HOST_ID, { osType: "linux" });
|
|
213
|
+
await HostService.updateLastSeen(HOST_ID, { osType: "windows" });
|
|
214
|
+
|
|
215
|
+
expect(writes).toHaveLength(2);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("oversized collector metadata", () => {
|
|
220
|
+
beforeEach(() => {
|
|
221
|
+
// The clamp lives in the write path, so run the real one.
|
|
222
|
+
mockRealWritePath();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("stores a 55-address host.ip list untouched — the column is text now", async () => {
|
|
226
|
+
await HostService.updateLastSeen(HOST_ID, {
|
|
227
|
+
hostIpAddresses: HUGE_IP_LIST,
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
expect(HUGE_IP_LIST.length).toBeGreaterThan(500);
|
|
231
|
+
expect(lastWrite()["hostIpAddresses"]).toBe(HUGE_IP_LIST);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("clamps an oversized ShortText attribute instead of failing the write", async () => {
|
|
235
|
+
const longHostId: string = "i".repeat(400);
|
|
236
|
+
|
|
237
|
+
await HostService.updateLastSeen(HOST_ID, { hostId: longHostId });
|
|
238
|
+
|
|
239
|
+
expect(lastWrite()["hostId"]).toHaveLength(100);
|
|
240
|
+
expect(lastWrite()["hostId"]).toBe("i".repeat(100));
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("clamps an oversized LongText attribute to 500", async () => {
|
|
244
|
+
const longOsVersion: string = "v".repeat(900);
|
|
245
|
+
|
|
246
|
+
await HostService.updateLastSeen(HOST_ID, { osVersion: longOsVersion });
|
|
247
|
+
|
|
248
|
+
expect(lastWrite()["osVersion"]).toHaveLength(500);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("clamping never costs the liveness columns", async () => {
|
|
252
|
+
await HostService.updateLastSeen(HOST_ID, {
|
|
253
|
+
hostId: "i".repeat(400),
|
|
254
|
+
hostArch: "a".repeat(400),
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(lastWrite()["otelCollectorStatus"]).toBe("connected");
|
|
258
|
+
expect(lastWrite()["lastSeenAt"]).toBeInstanceOf(Date);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const CLAMPED_COLUMNS: Array<[string, number]> = [
|
|
262
|
+
["osType", 100],
|
|
263
|
+
["hostId", 100],
|
|
264
|
+
["hostArch", 100],
|
|
265
|
+
["hostType", 100],
|
|
266
|
+
["containerRuntime", 100],
|
|
267
|
+
["agentVersion", 100],
|
|
268
|
+
["deploymentEnvironment", 100],
|
|
269
|
+
["runtimeName", 100],
|
|
270
|
+
["runtimeVersion", 100],
|
|
271
|
+
["cloudProvider", 100],
|
|
272
|
+
["cloudPlatform", 100],
|
|
273
|
+
["cloudRegion", 100],
|
|
274
|
+
["cloudAccountId", 100],
|
|
275
|
+
["osVersion", 500],
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
test.each(CLAMPED_COLUMNS)(
|
|
279
|
+
"%s is clamped to %i characters",
|
|
280
|
+
async (column: string, maxLength: number) => {
|
|
281
|
+
await HostService.updateLastSeen(HOST_ID, {
|
|
282
|
+
[column]: "x".repeat(maxLength + 250),
|
|
283
|
+
} as UpdateLastSeenExtra);
|
|
284
|
+
|
|
285
|
+
expect(lastWrite()[column]).toHaveLength(maxLength);
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
test("values that already fit are passed through byte-for-byte", async () => {
|
|
290
|
+
await HostService.updateLastSeen(HOST_ID, {
|
|
291
|
+
osType: "linux",
|
|
292
|
+
osVersion: "Debian GNU/Linux 12 (bookworm)",
|
|
293
|
+
hostArch: "amd64",
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
expect(lastWrite()["osType"]).toBe("linux");
|
|
297
|
+
expect(lastWrite()["osVersion"]).toBe("Debian GNU/Linux 12 (bookworm)");
|
|
298
|
+
expect(lastWrite()["hostArch"]).toBe("amd64");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("numeric and relation columns are left alone by the clamp", async () => {
|
|
302
|
+
const dockerHostId: ObjectID = ObjectID.generate();
|
|
303
|
+
|
|
304
|
+
await HostService.updateLastSeen(HOST_ID, {
|
|
305
|
+
cpuCores: 64,
|
|
306
|
+
totalMemoryBytes: 274877906944,
|
|
307
|
+
processCount: 1337,
|
|
308
|
+
dockerHostId: dockerHostId,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(lastWrite()["cpuCores"]).toBe(64);
|
|
312
|
+
expect(lastWrite()["totalMemoryBytes"]).toBe(274877906944);
|
|
313
|
+
expect(lastWrite()["processCount"]).toBe(1337);
|
|
314
|
+
expect(lastWrite()["dockerHostId"]).toBe(dockerHostId);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe("fallback when the enriched write fails anyway", () => {
|
|
319
|
+
beforeEach(() => {
|
|
320
|
+
mockEnrichedWriteFailing();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("does not throw — ingest keeps going", async () => {
|
|
324
|
+
await expect(
|
|
325
|
+
HostService.updateLastSeen(HOST_ID, {
|
|
326
|
+
hostIpAddresses: HUGE_IP_LIST,
|
|
327
|
+
}),
|
|
328
|
+
).resolves.toBeUndefined();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("retries with liveness only, so lastSeenAt still advances", async () => {
|
|
332
|
+
await HostService.updateLastSeen(HOST_ID, {
|
|
333
|
+
hostIpAddresses: HUGE_IP_LIST,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
expect(writes).toHaveLength(2);
|
|
337
|
+
expect(Object.keys(lastWrite()).sort()).toEqual([
|
|
338
|
+
"lastSeenAt",
|
|
339
|
+
"otelCollectorStatus",
|
|
340
|
+
]);
|
|
341
|
+
expect(lastWrite()["otelCollectorStatus"]).toBe("connected");
|
|
342
|
+
expect(lastWrite()["lastSeenAt"]).toBeInstanceOf(Date);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("busts the throttle cache so the next batch is not skipped", async () => {
|
|
346
|
+
await HostService.updateLastSeen(HOST_ID, {
|
|
347
|
+
hostIpAddresses: HUGE_IP_LIST,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(deletedCacheKeys).toHaveLength(1);
|
|
351
|
+
expect(deletedCacheKeys[0]).toContain(HOST_ID.toString());
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("the next batch retries rather than short-circuiting on a cache hit", async () => {
|
|
355
|
+
await HostService.updateLastSeen(HOST_ID, {
|
|
356
|
+
hostIpAddresses: HUGE_IP_LIST,
|
|
357
|
+
});
|
|
358
|
+
writes = [];
|
|
359
|
+
|
|
360
|
+
await HostService.updateLastSeen(HOST_ID, {
|
|
361
|
+
hostIpAddresses: HUGE_IP_LIST,
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// Enriched attempt + liveness fallback, not a silent no-op.
|
|
365
|
+
expect(writes).toHaveLength(2);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("a failing cache delete does not stop the liveness retry", async () => {
|
|
369
|
+
jest
|
|
370
|
+
.spyOn(GlobalCache, "deleteKey")
|
|
371
|
+
.mockRejectedValue(new Error("redis down"));
|
|
372
|
+
|
|
373
|
+
await HostService.updateLastSeen(HOST_ID, {
|
|
374
|
+
hostIpAddresses: HUGE_IP_LIST,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
expect(Object.keys(lastWrite()).sort()).toEqual([
|
|
378
|
+
"lastSeenAt",
|
|
379
|
+
"otelCollectorStatus",
|
|
380
|
+
]);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("surfaces a genuinely broken database instead of hiding it", async () => {
|
|
384
|
+
// Both attempts fail — the caller must find out.
|
|
385
|
+
jest
|
|
386
|
+
.spyOn(HostService, "updateColumnsByIdWithoutHooks")
|
|
387
|
+
.mockRejectedValue(new Error("connection terminated"));
|
|
388
|
+
|
|
389
|
+
await expect(
|
|
390
|
+
HostService.updateLastSeen(HOST_ID, { osType: "linux" }),
|
|
391
|
+
).rejects.toThrow("connection terminated");
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
describe("cache outages", () => {
|
|
396
|
+
beforeEach(() => {
|
|
397
|
+
mockWritesSucceeding();
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("a read failure still writes — never mark a live host disconnected", async () => {
|
|
401
|
+
jest
|
|
402
|
+
.spyOn(GlobalCache, "getString")
|
|
403
|
+
.mockRejectedValue(new Error("redis down"));
|
|
404
|
+
|
|
405
|
+
await HostService.updateLastSeen(HOST_ID, { osType: "linux" });
|
|
406
|
+
|
|
407
|
+
expect(writes).toHaveLength(1);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("a write failure still writes", async () => {
|
|
411
|
+
jest
|
|
412
|
+
.spyOn(GlobalCache, "setString")
|
|
413
|
+
.mockRejectedValue(new Error("redis down"));
|
|
414
|
+
|
|
415
|
+
await HostService.updateLastSeen(HOST_ID, { osType: "linux" });
|
|
416
|
+
|
|
417
|
+
expect(writes).toHaveLength(1);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe("DatabaseService.truncateStringColumnsToMaxLength", () => {
|
|
423
|
+
test("clamps only what exceeds its column width", () => {
|
|
424
|
+
const data: Record<string, unknown> = {
|
|
425
|
+
osType: "l".repeat(101),
|
|
426
|
+
hostArch: "amd64",
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
HostService.truncateStringColumnsToMaxLength(data);
|
|
430
|
+
|
|
431
|
+
expect(data["osType"]).toHaveLength(100);
|
|
432
|
+
expect(data["hostArch"]).toBe("amd64");
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("leaves text columns untouched at any length", () => {
|
|
436
|
+
const huge: string = "1.2.3.4, ".repeat(5000);
|
|
437
|
+
const data: Record<string, unknown> = { hostIpAddresses: huge };
|
|
438
|
+
|
|
439
|
+
HostService.truncateStringColumnsToMaxLength(data);
|
|
440
|
+
|
|
441
|
+
expect(data["hostIpAddresses"]).toBe(huge);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("leaves non-string values untouched", () => {
|
|
445
|
+
const date: Date = new Date();
|
|
446
|
+
const id: ObjectID = ObjectID.generate();
|
|
447
|
+
const data: Record<string, unknown> = {
|
|
448
|
+
lastSeenAt: date,
|
|
449
|
+
cpuCores: 8,
|
|
450
|
+
isArchived: false,
|
|
451
|
+
dockerHostId: id,
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
HostService.truncateStringColumnsToMaxLength(data);
|
|
455
|
+
|
|
456
|
+
expect(data).toEqual({
|
|
457
|
+
lastSeenAt: date,
|
|
458
|
+
cpuCores: 8,
|
|
459
|
+
isArchived: false,
|
|
460
|
+
dockerHostId: id,
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("ignores keys that are not columns on the model", () => {
|
|
465
|
+
const data: Record<string, unknown> = { notAColumn: "z".repeat(5000) };
|
|
466
|
+
|
|
467
|
+
HostService.truncateStringColumnsToMaxLength(data);
|
|
468
|
+
|
|
469
|
+
expect(data["notAColumn"]).toHaveLength(5000);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("ignores empty strings", () => {
|
|
473
|
+
const data: Record<string, unknown> = { osType: "" };
|
|
474
|
+
|
|
475
|
+
HostService.truncateStringColumnsToMaxLength(data);
|
|
476
|
+
|
|
477
|
+
expect(data["osType"]).toBe("");
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
test("mutates in place and returns the same object", () => {
|
|
481
|
+
const data: Record<string, unknown> = { osType: "l".repeat(200) };
|
|
482
|
+
|
|
483
|
+
expect(HostService.truncateStringColumnsToMaxLength(data)).toBe(data);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
test("a value exactly at the limit is not clipped", () => {
|
|
487
|
+
const exact: string = "l".repeat(100);
|
|
488
|
+
const data: Record<string, unknown> = { osType: exact };
|
|
489
|
+
|
|
490
|
+
HostService.truncateStringColumnsToMaxLength(data);
|
|
491
|
+
|
|
492
|
+
expect(data["osType"]).toBe(exact);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("every clamped value stays a prefix of the original", () => {
|
|
496
|
+
const original: string = "abcdefghij".repeat(40);
|
|
497
|
+
const data: Record<string, unknown> = { osType: original };
|
|
498
|
+
|
|
499
|
+
HostService.truncateStringColumnsToMaxLength(data);
|
|
500
|
+
|
|
501
|
+
expect(original.startsWith(data["osType"] as string)).toBe(true);
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("Host really does have the columns this suite assumes", () => {
|
|
505
|
+
const host: Host = new Host();
|
|
506
|
+
for (const column of ["osType", "osVersion", "hostIpAddresses"]) {
|
|
507
|
+
expect(host.hasColumn(column)).toBe(true);
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
});
|