@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
|
@@ -7,6 +7,7 @@ import Typeof from "../../../Types/Typeof";
|
|
|
7
7
|
import { FindOperator, Raw } from "typeorm";
|
|
8
8
|
import { FindWhereProperty } from "../../../Types/BaseDatabase/Query";
|
|
9
9
|
import CaptureSpan from "../../Utils/Telemetry/CaptureSpan";
|
|
10
|
+
import buildJSONColumnQuery, { JSONColumnQuery } from "./JSONColumnQuery";
|
|
10
11
|
|
|
11
12
|
export default class QueryHelper {
|
|
12
13
|
@CaptureSpan()
|
|
@@ -668,40 +669,25 @@ export default class QueryHelper {
|
|
|
668
669
|
) as FindWhereProperty<any>;
|
|
669
670
|
}
|
|
670
671
|
|
|
672
|
+
/**
|
|
673
|
+
* Filter on the keys of a jsonb column — `customFields` above all, whose
|
|
674
|
+
* keys are the names a project gave its custom fields.
|
|
675
|
+
*
|
|
676
|
+
* The per-key predicates (equality, "is any of", contains, numeric
|
|
677
|
+
* comparison, is-empty, and their negations) live in JSONColumnQuery. Two
|
|
678
|
+
* things this used to get wrong and no longer does: the key was spliced
|
|
679
|
+
* straight into the SQL string, and every value was compared with `=`, so an
|
|
680
|
+
* operator such as `Includes` reached Postgres stringified.
|
|
681
|
+
*/
|
|
671
682
|
@CaptureSpan()
|
|
672
683
|
public static queryJson(value: JSONObject): FindWhereProperty<any> {
|
|
673
|
-
// seed random text
|
|
674
|
-
const values: JSONObject = {};
|
|
675
|
-
|
|
676
|
-
let queryText: string = "";
|
|
677
|
-
|
|
678
|
-
if (typeof value === Typeof.String) {
|
|
679
|
-
value = JSON.parse(value.toString());
|
|
680
|
-
}
|
|
681
|
-
|
|
682
684
|
if (value instanceof FindOperator) {
|
|
683
685
|
return value;
|
|
684
686
|
}
|
|
685
687
|
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
const temp: string = Text.generateRandomText(10);
|
|
690
|
-
|
|
691
|
-
values[temp] = value[key];
|
|
692
|
-
|
|
693
|
-
if (!queryText) {
|
|
694
|
-
queryText = `(COLUMN_NAME_ALIAS->>'${key}' = :${temp as string}`;
|
|
695
|
-
} else {
|
|
696
|
-
queryText += ` AND COLUMN_NAME_ALIAS->>'${key}' = :${temp as string}`;
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
if (hasValue) {
|
|
701
|
-
queryText += ")";
|
|
702
|
-
} else {
|
|
703
|
-
queryText = "(COLUMN_NAME_ALIAS IS NULL OR COLUMN_NAME_ALIAS = '{}')";
|
|
704
|
-
}
|
|
688
|
+
const jsonQuery: JSONColumnQuery = buildJSONColumnQuery({
|
|
689
|
+
value: value as JSONObject,
|
|
690
|
+
});
|
|
705
691
|
|
|
706
692
|
return Raw((alias: string) => {
|
|
707
693
|
/*
|
|
@@ -709,16 +695,15 @@ export default class QueryHelper {
|
|
|
709
695
|
* we need to convert this to "tableName"."columnName"
|
|
710
696
|
*/
|
|
711
697
|
|
|
712
|
-
|
|
698
|
+
const columnReference: string = alias
|
|
713
699
|
.split(".")
|
|
714
700
|
.map((item: string) => {
|
|
715
701
|
return `"${item}"`;
|
|
716
702
|
})
|
|
717
703
|
.join(".");
|
|
718
704
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
}, values);
|
|
705
|
+
return jsonQuery.toSql(columnReference);
|
|
706
|
+
}, jsonQuery.parameters);
|
|
722
707
|
}
|
|
723
708
|
|
|
724
709
|
@CaptureSpan()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import ColumnLength from "../../../Types/Database/ColumnLength";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* ------------------------------------------------------------------
|
|
5
|
+
* TruncateColumnValue
|
|
6
|
+
*
|
|
7
|
+
* Per-value clamps for bounded varchar columns written by the bulk
|
|
8
|
+
* inventory upsert paths (Kubernetes / Docker / Docker Swarm / Podman /
|
|
9
|
+
* Proxmox / Ceph / IoT).
|
|
10
|
+
*
|
|
11
|
+
* Those services hand-build `INSERT ... ON CONFLICT` statements and
|
|
12
|
+
* flush them in chunks of 500 rows through
|
|
13
|
+
* `getRepository().manager.query(...)`, which bypasses the length
|
|
14
|
+
* validation DatabaseService applies to ordinary CRUD writes. A single
|
|
15
|
+
* value past its column limit raises 22001 ("value too long for type
|
|
16
|
+
* character varying(N)") and aborts the WHOLE multi-row INSERT, so one
|
|
17
|
+
* malformed resource silently drops up to 499 healthy inventory rows
|
|
18
|
+
* with it. Clamping per value keeps the blast radius at one field.
|
|
19
|
+
*
|
|
20
|
+
* Truncation is the last line of defense, not the primary fix: columns
|
|
21
|
+
* that legitimately carry long values (image references, Kubernetes
|
|
22
|
+
* DNS-subdomain names) should be widened in the model + a migration so
|
|
23
|
+
* these clamps effectively never fire.
|
|
24
|
+
* ------------------------------------------------------------------
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export function truncateToLength(value: string, maxLength: number): string;
|
|
28
|
+
export function truncateToLength(
|
|
29
|
+
value: string | null | undefined,
|
|
30
|
+
maxLength: number,
|
|
31
|
+
): string | null;
|
|
32
|
+
export function truncateToLength(
|
|
33
|
+
value: string | null | undefined,
|
|
34
|
+
maxLength: number,
|
|
35
|
+
): string | null {
|
|
36
|
+
if (value === null || value === undefined) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (value.length <= maxLength) {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const clipped: string = value.substring(0, maxLength);
|
|
45
|
+
|
|
46
|
+
/*
|
|
47
|
+
* JS string length counts UTF-16 code units, so a cut at maxLength can
|
|
48
|
+
* land in the middle of a surrogate pair (any astral-plane character:
|
|
49
|
+
* emoji, rare CJK). The orphaned high surrogate is not valid UTF-8 —
|
|
50
|
+
* Node encodes it as U+FFFD on the way to the driver — so drop it.
|
|
51
|
+
* A well-formed string never ends a character on a high surrogate,
|
|
52
|
+
* which makes this check unambiguous. Postgres counts characters, not
|
|
53
|
+
* code units, so clipping by code units is always conservative: the
|
|
54
|
+
* result can only be shorter than the column bound, never longer.
|
|
55
|
+
*/
|
|
56
|
+
const lastCodeUnit: number = clipped.charCodeAt(clipped.length - 1);
|
|
57
|
+
if (lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff) {
|
|
58
|
+
return clipped.substring(0, clipped.length - 1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return clipped;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Clamp to ColumnLength.ShortText — varchar(100). */
|
|
65
|
+
export function truncateShortText(value: string): string;
|
|
66
|
+
export function truncateShortText(
|
|
67
|
+
value: string | null | undefined,
|
|
68
|
+
): string | null;
|
|
69
|
+
export function truncateShortText(
|
|
70
|
+
value: string | null | undefined,
|
|
71
|
+
): string | null {
|
|
72
|
+
return truncateToLength(value, ColumnLength.ShortText);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Clamp to ColumnLength.LongText — varchar(500). */
|
|
76
|
+
export function truncateLongText(value: string): string;
|
|
77
|
+
export function truncateLongText(
|
|
78
|
+
value: string | null | undefined,
|
|
79
|
+
): string | null;
|
|
80
|
+
export function truncateLongText(
|
|
81
|
+
value: string | null | undefined,
|
|
82
|
+
): string | null {
|
|
83
|
+
return truncateToLength(value, ColumnLength.LongText);
|
|
84
|
+
}
|
|
@@ -192,6 +192,18 @@ export default class CompareCriteria {
|
|
|
192
192
|
return null;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
/*
|
|
196
|
+
* parseInt("abc") returns NaN rather than throwing, and NaN is typeof
|
|
197
|
+
* "number" — so without this guard a non-numeric threshold would leak
|
|
198
|
+
* past callers that only check for `=== null`, leaving every numeric
|
|
199
|
+
* comparison silently false (value > NaN is always false). Treat an
|
|
200
|
+
* unparseable value the same as a missing one so the caller can ignore
|
|
201
|
+
* it instead of firing a broken criterion.
|
|
202
|
+
*/
|
|
203
|
+
if (Number.isNaN(threshold as number)) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
195
207
|
return threshold as number;
|
|
196
208
|
}
|
|
197
209
|
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { describe, expect, test } from "@jest/globals";
|
|
5
|
+
import RunnerInstallInstructions from "../../../../App/FeatureSet/Dashboard/src/Components/Runner/InstallInstructions";
|
|
6
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* The setup card renders the docker run command with the Runner's secret key
|
|
10
|
+
* embedded in it. Reading that key is restricted to Project Owner, Project
|
|
11
|
+
* Admin and Runbook Admin (see the ColumnAccessControl on Runner.key), so for
|
|
12
|
+
* a Member or a Viewer the field simply is not in the API response.
|
|
13
|
+
*
|
|
14
|
+
* The Runners table asked for the key and passed `(item.key as string) || ""`
|
|
15
|
+
* straight through, so those users were handed a command ending in
|
|
16
|
+
* ONEUPTIME_RUNNER_KEY= with nothing after it — which copies cleanly, runs on
|
|
17
|
+
* the host, and fails there with an authentication error. These tests pin that
|
|
18
|
+
* no command is ever rendered without a key.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const RUNNER_ID: ObjectID = new ObjectID("abc-123");
|
|
22
|
+
|
|
23
|
+
type RenderWithKeyFunction = (key: string) => HTMLElement;
|
|
24
|
+
|
|
25
|
+
const renderWithKey: RenderWithKeyFunction = (key: string): HTMLElement => {
|
|
26
|
+
const { container } = render(
|
|
27
|
+
<RunnerInstallInstructions runnerId={RUNNER_ID} runnerKey={key} />,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return container;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe("RunnerInstallInstructions", () => {
|
|
34
|
+
describe("when the key is readable", () => {
|
|
35
|
+
test("renders the docker run command", () => {
|
|
36
|
+
const container: HTMLElement = renderWithKey("super-secret-key");
|
|
37
|
+
|
|
38
|
+
expect(container.textContent).toContain("docker run");
|
|
39
|
+
expect(container.textContent).toContain("oneuptime/runner:release");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("embeds the Runner id and key", () => {
|
|
43
|
+
const container: HTMLElement = renderWithKey("super-secret-key");
|
|
44
|
+
|
|
45
|
+
expect(container.textContent).toContain("ONEUPTIME_RUNNER_ID=abc-123");
|
|
46
|
+
expect(container.textContent).toContain(
|
|
47
|
+
"ONEUPTIME_RUNNER_KEY=super-secret-key",
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("does not render the permission warning", () => {
|
|
52
|
+
renderWithKey("super-secret-key");
|
|
53
|
+
|
|
54
|
+
expect(
|
|
55
|
+
screen.queryByText(/do not have permission/i),
|
|
56
|
+
).not.toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* The container needs outbound HTTPS only; saying so here is what stops
|
|
61
|
+
* someone opening an inbound hole for it.
|
|
62
|
+
*/
|
|
63
|
+
test("states that the Runner needs no inbound connections", () => {
|
|
64
|
+
const container: HTMLElement = renderWithKey("super-secret-key");
|
|
65
|
+
|
|
66
|
+
expect(container.textContent).toContain(
|
|
67
|
+
"does not accept inbound connections",
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/*
|
|
72
|
+
* Added because "I ran the command and it still says Never connected" is
|
|
73
|
+
* the obvious next confusion once the never-connected state exists.
|
|
74
|
+
*/
|
|
75
|
+
test("explains that the first heartbeat takes up to a minute", () => {
|
|
76
|
+
const container: HTMLElement = renderWithKey("super-secret-key");
|
|
77
|
+
|
|
78
|
+
expect(container.textContent).toContain("every 60 seconds");
|
|
79
|
+
expect(container.textContent).toContain("Connected");
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("when the key is not readable", () => {
|
|
84
|
+
test("renders the permission warning instead", () => {
|
|
85
|
+
renderWithKey("");
|
|
86
|
+
|
|
87
|
+
expect(
|
|
88
|
+
screen.getByText(
|
|
89
|
+
"You do not have permission to view this Runner's key",
|
|
90
|
+
),
|
|
91
|
+
).toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
* The load-bearing assertion: no command at all, rather than a command
|
|
96
|
+
* that is silently broken.
|
|
97
|
+
*/
|
|
98
|
+
test("renders no docker command at all", () => {
|
|
99
|
+
const container: HTMLElement = renderWithKey("");
|
|
100
|
+
|
|
101
|
+
expect(container.textContent).not.toContain("docker run");
|
|
102
|
+
expect(container.textContent).not.toContain("ONEUPTIME_RUNNER_KEY");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("never emits an empty ONEUPTIME_RUNNER_KEY=", () => {
|
|
106
|
+
const container: HTMLElement = renderWithKey("");
|
|
107
|
+
|
|
108
|
+
expect(container.textContent).not.toMatch(/ONEUPTIME_RUNNER_KEY=\s/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("says who can supply the command", () => {
|
|
112
|
+
const container: HTMLElement = renderWithKey("");
|
|
113
|
+
|
|
114
|
+
expect(container.textContent).toContain("Project Owner");
|
|
115
|
+
expect(container.textContent).toContain("Project Admin");
|
|
116
|
+
expect(container.textContent).toContain("Runbook Admin");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { describe, expect, test } from "@jest/globals";
|
|
5
|
+
import RunnerStatusElement from "../../../../App/FeatureSet/Dashboard/src/Components/Runner/RunnerStatus";
|
|
6
|
+
import Runner, {
|
|
7
|
+
RunnerConnectionStatus,
|
|
8
|
+
} from "../../../Models/DatabaseModels/Runner";
|
|
9
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
10
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* The one component every Runner connection indicator in the dashboard now
|
|
14
|
+
* renders through. Three call sites used to hand-roll this, and all three read
|
|
15
|
+
* the persisted connectionStatus column — which is written on create and on
|
|
16
|
+
* the first heartbeat and never again. A Runner that died months ago still has
|
|
17
|
+
* it set to "connected".
|
|
18
|
+
*
|
|
19
|
+
* So the assertions that matter most here are the ones proving the rendered
|
|
20
|
+
* status tracks lastAlive and IGNORES connectionStatus, in both directions.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
type RenderRunnerFunction = (
|
|
24
|
+
runner: Runner | JSONObject,
|
|
25
|
+
showLastSeen?: boolean,
|
|
26
|
+
) => void;
|
|
27
|
+
|
|
28
|
+
const renderRunner: RenderRunnerFunction = (
|
|
29
|
+
runner: Runner | JSONObject,
|
|
30
|
+
showLastSeen?: boolean,
|
|
31
|
+
): void => {
|
|
32
|
+
render(
|
|
33
|
+
showLastSeen === undefined ? (
|
|
34
|
+
<RunnerStatusElement runner={runner} />
|
|
35
|
+
) : (
|
|
36
|
+
<RunnerStatusElement runner={runner} showLastSeen={showLastSeen} />
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type MinutesAgoFunction = (minutes: number) => Date;
|
|
42
|
+
|
|
43
|
+
const minutesAgo: MinutesAgoFunction = (minutes: number): Date => {
|
|
44
|
+
return OneUptimeDate.getSomeMinutesAgo(minutes);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type StatusTextFunction = () => string;
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
* Statusbubble renders role="status" with aria-label "Status: <text>", so the
|
|
51
|
+
* accessible name is the assertion surface — it is what a screen reader
|
|
52
|
+
* announces, not just what happens to be in a div.
|
|
53
|
+
*/
|
|
54
|
+
const statusText: StatusTextFunction = (): string => {
|
|
55
|
+
return screen.getByRole("status").getAttribute("aria-label") || "";
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
describe("RunnerStatusElement", () => {
|
|
59
|
+
describe("never connected", () => {
|
|
60
|
+
test("a Runner with no lastAlive reads 'Never connected'", () => {
|
|
61
|
+
renderRunner({ lastAlive: null });
|
|
62
|
+
|
|
63
|
+
expect(screen.getByText("Never connected")).toBeInTheDocument();
|
|
64
|
+
expect(statusText()).toBe("Status: Never connected");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("a Runner with lastAlive absent entirely reads 'Never connected'", () => {
|
|
68
|
+
renderRunner({});
|
|
69
|
+
|
|
70
|
+
expect(screen.getByText("Never connected")).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* The whole point of the third state. A Runner created ten seconds ago
|
|
75
|
+
* whose container has not been started is not a failure, and must not be
|
|
76
|
+
* dressed as one — it used to render the same red "Disconnected" as a
|
|
77
|
+
* Runner that crashed in production.
|
|
78
|
+
*/
|
|
79
|
+
test("it is not called Disconnected", () => {
|
|
80
|
+
renderRunner({ lastAlive: null });
|
|
81
|
+
|
|
82
|
+
expect(screen.queryByText("Disconnected")).not.toBeInTheDocument();
|
|
83
|
+
expect(statusText()).not.toContain("Disconnected");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("it is grey, not red", () => {
|
|
87
|
+
const { container } = render(
|
|
88
|
+
<RunnerStatusElement runner={{ lastAlive: null }} />,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const html: string = container.innerHTML;
|
|
92
|
+
|
|
93
|
+
// Gray500 #6b7280, Red #fd625e.
|
|
94
|
+
expect(html).toContain("107, 114, 128");
|
|
95
|
+
expect(html).not.toContain("253, 98, 94");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("it does not animate", () => {
|
|
99
|
+
const { container } = render(
|
|
100
|
+
<RunnerStatusElement runner={{ lastAlive: null }} />,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(container.innerHTML).not.toContain("animate-ping");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("connected", () => {
|
|
108
|
+
test("a heartbeat right now reads 'Connected'", () => {
|
|
109
|
+
renderRunner({ lastAlive: OneUptimeDate.getCurrentDate() });
|
|
110
|
+
|
|
111
|
+
expect(screen.getByText("Connected")).toBeInTheDocument();
|
|
112
|
+
expect(statusText()).toBe("Status: Connected");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("a heartbeat four minutes ago still reads 'Connected'", () => {
|
|
116
|
+
renderRunner({ lastAlive: minutesAgo(4) });
|
|
117
|
+
|
|
118
|
+
expect(screen.getByText("Connected")).toBeInTheDocument();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("it is green and animating", () => {
|
|
122
|
+
const { container } = render(
|
|
123
|
+
<RunnerStatusElement
|
|
124
|
+
runner={{ lastAlive: OneUptimeDate.getCurrentDate() }}
|
|
125
|
+
/>,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Green #2ab57d.
|
|
129
|
+
expect(container.innerHTML).toContain("42, 181, 125");
|
|
130
|
+
expect(container.innerHTML).toContain("animate-ping");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("disconnected", () => {
|
|
135
|
+
test("a heartbeat an hour ago reads 'Disconnected'", () => {
|
|
136
|
+
renderRunner({ lastAlive: minutesAgo(60) });
|
|
137
|
+
|
|
138
|
+
expect(statusText()).toContain("Disconnected");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/*
|
|
142
|
+
* "How long has it been gone" is the entire question when a Runner drops,
|
|
143
|
+
* and the detail card has no other column carrying it.
|
|
144
|
+
*/
|
|
145
|
+
test("it appends how long ago the last heartbeat was", () => {
|
|
146
|
+
renderRunner({ lastAlive: minutesAgo(60) });
|
|
147
|
+
|
|
148
|
+
expect(statusText()).toBe("Status: Disconnected · an hour ago");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("a day-old heartbeat says a day ago", () => {
|
|
152
|
+
renderRunner({ lastAlive: minutesAgo(60 * 24) });
|
|
153
|
+
|
|
154
|
+
expect(statusText()).toBe("Status: Disconnected · a day ago");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
/*
|
|
158
|
+
* In the Runners table a Last Seen column sits directly beside this one,
|
|
159
|
+
* so repeating the relative time would be noise.
|
|
160
|
+
*/
|
|
161
|
+
test("showLastSeen={false} suppresses the relative time", () => {
|
|
162
|
+
renderRunner({ lastAlive: minutesAgo(60) }, false);
|
|
163
|
+
|
|
164
|
+
expect(statusText()).toBe("Status: Disconnected");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("showLastSeen={true} is the same as the default", () => {
|
|
168
|
+
renderRunner({ lastAlive: minutesAgo(60) }, true);
|
|
169
|
+
|
|
170
|
+
expect(statusText()).toBe("Status: Disconnected · an hour ago");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("it is red and does not animate", () => {
|
|
174
|
+
const { container } = render(
|
|
175
|
+
<RunnerStatusElement runner={{ lastAlive: minutesAgo(60) }} />,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Red #fd625e.
|
|
179
|
+
expect(container.innerHTML).toContain("253, 98, 94");
|
|
180
|
+
expect(container.innerHTML).not.toContain("animate-ping");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
/*
|
|
185
|
+
* The regression suite. Each of these is a shape the old inline
|
|
186
|
+
* implementations got wrong.
|
|
187
|
+
*/
|
|
188
|
+
describe("connectionStatus is ignored", () => {
|
|
189
|
+
test("stale lastAlive reads Disconnected even when connectionStatus says Connected", () => {
|
|
190
|
+
renderRunner({
|
|
191
|
+
connectionStatus: RunnerConnectionStatus.Connected,
|
|
192
|
+
lastAlive: minutesAgo(60 * 24 * 90),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(statusText()).toContain("Disconnected");
|
|
196
|
+
expect(screen.queryByText("Connected")).not.toBeInTheDocument();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("fresh lastAlive reads Connected even when connectionStatus says Disconnected", () => {
|
|
200
|
+
renderRunner({
|
|
201
|
+
connectionStatus: RunnerConnectionStatus.Disconnected,
|
|
202
|
+
lastAlive: OneUptimeDate.getCurrentDate(),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(statusText()).toBe("Status: Connected");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
/*
|
|
209
|
+
* Exactly what RunnerService.onBeforeCreate produces: connectionStatus
|
|
210
|
+
* defaulted to Disconnected, lastAlive still null. The user has not
|
|
211
|
+
* failed at anything yet — they have not run the Docker command.
|
|
212
|
+
*/
|
|
213
|
+
test("a freshly created Runner reads Never connected, not Disconnected", () => {
|
|
214
|
+
renderRunner({
|
|
215
|
+
connectionStatus: RunnerConnectionStatus.Disconnected,
|
|
216
|
+
lastAlive: null,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(statusText()).toBe("Status: Never connected");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("connectionStatus alone never produces a Connected bubble", () => {
|
|
223
|
+
renderRunner({ connectionStatus: RunnerConnectionStatus.Connected });
|
|
224
|
+
|
|
225
|
+
expect(statusText()).toBe("Status: Never connected");
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("input shapes", () => {
|
|
230
|
+
/*
|
|
231
|
+
* The table hands over a hydrated model; ModelDetail's getElement hands
|
|
232
|
+
* over the same. The API layer hands over a plain object with an ISO
|
|
233
|
+
* string. All three have to work.
|
|
234
|
+
*/
|
|
235
|
+
test("accepts a hydrated Runner model", () => {
|
|
236
|
+
const runner: Runner = new Runner();
|
|
237
|
+
runner.lastAlive = OneUptimeDate.getCurrentDate();
|
|
238
|
+
|
|
239
|
+
renderRunner(runner);
|
|
240
|
+
|
|
241
|
+
expect(statusText()).toBe("Status: Connected");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("accepts a hydrated Runner model that never connected", () => {
|
|
245
|
+
renderRunner(new Runner());
|
|
246
|
+
|
|
247
|
+
expect(statusText()).toBe("Status: Never connected");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("accepts an ISO string lastAlive", () => {
|
|
251
|
+
renderRunner({ lastAlive: new Date().toISOString() });
|
|
252
|
+
|
|
253
|
+
expect(statusText()).toBe("Status: Connected");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("accepts a stale ISO string lastAlive", () => {
|
|
257
|
+
renderRunner({ lastAlive: minutesAgo(60).toISOString() });
|
|
258
|
+
|
|
259
|
+
expect(statusText()).toContain("Disconnected");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
/*
|
|
263
|
+
* An unparseable timestamp must not be rendered as a healthy Runner. It
|
|
264
|
+
* falls to Disconnected, and the appended relative time is suppressed
|
|
265
|
+
* rather than printed as "Invalid date".
|
|
266
|
+
*/
|
|
267
|
+
test("an unparseable lastAlive is Disconnected and prints no garbage time", () => {
|
|
268
|
+
renderRunner({ lastAlive: "not-a-date" });
|
|
269
|
+
|
|
270
|
+
expect(statusText()).toContain("Disconnected");
|
|
271
|
+
expect(statusText()).not.toContain("Invalid");
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|