@oneuptime/common 12.0.14 → 12.0.15
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/GlobalOidc.ts +20 -0
- package/Models/DatabaseModels/GlobalSso.ts +20 -0
- package/Models/DatabaseModels/MarketingConversion.ts +19 -9
- package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +46 -0
- package/Models/DatabaseModels/StatusPage.ts +7 -4
- package/Server/API/StatusPageAPI.ts +29 -4
- package/Server/EnvironmentConfig.ts +7 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/SlackAuthorization.ts +43 -6
- package/Server/Middleware/UserAuthorization.ts +236 -26
- package/Server/Middleware/WhatsAppAuthorization.ts +43 -3
- package/Server/Services/GlobalOidcProjectService.ts +126 -1
- package/Server/Services/GlobalOidcService.ts +132 -0
- package/Server/Services/GlobalSsoProjectService.ts +126 -1
- package/Server/Services/GlobalSsoService.ts +132 -0
- package/Server/Services/UserEmailService.ts +4 -4
- package/Server/Utils/CronTab.ts +73 -0
- package/Server/Utils/GlobalSsoAuthorization.ts +175 -0
- package/Server/Utils/Marketing/ConversionUploadProvider.ts +35 -5
- package/Server/Utils/Marketing/Providers/GoogleAds.ts +1 -1
- package/Server/Utils/Marketing/Providers/LinkedIn.ts +1 -1
- package/Server/Utils/Marketing/Providers/Meta.ts +1 -1
- package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +1 -1
- package/Server/Utils/Marketing/Providers/Reddit.ts +1 -1
- package/Server/Utils/Monitor/Criteria/APIRequestCriteria.ts +48 -33
- package/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.ts +35 -30
- package/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.ts +33 -28
- package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +475 -47
- package/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.ts +35 -30
- package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +34 -28
- package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +33 -28
- package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +69 -40
- package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +35 -29
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +8 -0
- package/Server/Utils/Monitor/MonitorResource.ts +161 -21
- package/Server/Utils/StatusPageContentSecurityPolicy.ts +56 -0
- package/Server/Utils/StatusPageCustomizationAccess.ts +117 -0
- package/Tests/Server/API/StatusPageCustomizationOrigin.test.ts +348 -0
- package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +9 -59
- package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +227 -0
- package/Tests/Server/Middleware/SlackAuthorization.test.ts +149 -0
- package/Tests/Server/Middleware/UserAuthorization.test.ts +27 -13
- package/Tests/Server/Middleware/UserAuthorizationGlobalSsoGovernance.test.ts +1455 -0
- package/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts +91 -2
- package/Tests/Server/Middleware/WhatsAppAuthorization.test.ts +233 -0
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +21 -8
- package/Tests/Server/Services/GlobalSsoProviderAuthorization.test.ts +1242 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementHydration.test.ts +459 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +10 -0
- package/Tests/Server/Types/AnalyticsDatabase/AggregateUtilBucketTimestamp.test.ts +416 -0
- package/Tests/Server/Types/Database/QueryHelperManyToManyAndEmptyValues.test.ts +551 -0
- package/Tests/Server/Types/Database/SelectUtil.test.ts +348 -0
- package/Tests/Server/Utils/CronTab.test.ts +116 -0
- package/Tests/Server/Utils/GlobalSSOToken.test.ts +581 -0
- package/Tests/Server/Utils/GlobalSsoAuthorization.test.ts +1401 -0
- package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +254 -0
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +1 -1
- package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaEvaluateOverTime.test.ts +508 -0
- package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaPortTimings.test.ts +30 -9
- package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +987 -0
- package/Tests/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.test.ts +916 -0
- package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +382 -0
- package/Tests/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.test.ts +1219 -0
- package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +86 -0
- package/Tests/Server/Utils/StatusPageContentSecurityPolicy.test.ts +136 -0
- package/Tests/Server/Utils/StatusPageCustomizationAccess.test.ts +223 -0
- package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +165 -0
- package/Tests/Types/NetworkDevice/NetworkDeviceLinkRuleScope.test.ts +215 -0
- package/Tests/UI/Components/Forms/FormSubmitAnalytics.test.tsx +206 -0
- package/Tests/UI/Utils/AIChatExport/ConversationMarkdown.test.ts +774 -0
- package/Tests/UI/Utils/AIChatExport/MarkdownBlocks.test.ts +791 -0
- package/Tests/UI/Utils/AIChatExport/MarkdownSafety.test.ts +544 -0
- package/Tests/Utils/Analytics.test.ts +187 -0
- package/Tests/Utils/Monitor/MonitorMetricType.test.ts +80 -0
- package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +1291 -0
- package/Tests/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.test.ts +214 -0
- package/Types/Marketing/MarketingConversion.ts +12 -0
- package/Types/Monitor/CriteriaFilter.ts +9 -0
- package/Types/NetworkDevice/NetworkDeviceLinkRuleScope.ts +51 -0
- package/UI/Components/FormModal/BasicFormModal.tsx +12 -0
- package/UI/Components/Forms/BasicForm.tsx +13 -1
- package/UI/Components/Forms/BasicModelForm.tsx +2 -1
- package/UI/Components/Forms/ModelForm.tsx +10 -1
- package/UI/Components/Forms/Utils/FormAnalyticsName.ts +39 -0
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +6 -1
- package/Utils/Analytics.ts +55 -2
- package/Utils/Monitor/MonitorMetricType.ts +40 -0
- package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +587 -37
- package/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.ts +115 -0
- package/build/dist/Models/DatabaseModels/GlobalOidc.js +21 -0
- package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalSso.js +21 -0
- package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MarketingConversion.js +20 -10
- package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +47 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +4 -4
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +25 -4
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +6 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js +29 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.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/SlackAuthorization.js +28 -4
- package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/UserAuthorization.js +197 -22
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/WhatsAppAuthorization.js +32 -2
- package/build/dist/Server/Middleware/WhatsAppAuthorization.js.map +1 -1
- package/build/dist/Server/Services/GlobalOidcProjectService.js +105 -0
- package/build/dist/Server/Services/GlobalOidcProjectService.js.map +1 -1
- package/build/dist/Server/Services/GlobalOidcService.js +127 -0
- package/build/dist/Server/Services/GlobalOidcService.js.map +1 -1
- package/build/dist/Server/Services/GlobalSsoProjectService.js +105 -0
- package/build/dist/Server/Services/GlobalSsoProjectService.js.map +1 -1
- package/build/dist/Server/Services/GlobalSsoService.js +127 -0
- package/build/dist/Server/Services/GlobalSsoService.js.map +1 -1
- package/build/dist/Server/Services/UserEmailService.js +4 -4
- package/build/dist/Server/Utils/CronTab.js +66 -0
- package/build/dist/Server/Utils/CronTab.js.map +1 -1
- package/build/dist/Server/Utils/GlobalSsoAuthorization.js +112 -0
- package/build/dist/Server/Utils/GlobalSsoAuthorization.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +17 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js +37 -35
- package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js +20 -22
- package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +370 -42
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +64 -52
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +25 -29
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +8 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +123 -23
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js +39 -0
- package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js.map +1 -0
- package/build/dist/Server/Utils/StatusPageCustomizationAccess.js +85 -0
- package/build/dist/Server/Utils/StatusPageCustomizationAccess.js.map +1 -0
- package/build/dist/Types/Marketing/MarketingConversion.js +10 -0
- package/build/dist/Types/Marketing/MarketingConversion.js.map +1 -1
- package/build/dist/Types/Monitor/CriteriaFilter.js +1 -0
- package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
- package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js +46 -0
- package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js.map +1 -0
- package/build/dist/UI/Components/FormModal/BasicFormModal.js +2 -1
- package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js +10 -1
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicModelForm.js +2 -1
- package/build/dist/UI/Components/Forms/BasicModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +4 -1
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js +33 -0
- package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js.map +1 -0
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js +2 -1
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/Utils/Analytics.js +50 -2
- package/build/dist/Utils/Analytics.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js +37 -0
- package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +318 -37
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js +87 -0
- package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js.map +1 -0
- package/package.json +1 -1
|
@@ -33,6 +33,16 @@
|
|
|
33
33
|
* MonitorResourceProbeAgreementReuse.test.ts. Pure in-memory model instances.
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
+
/*
|
|
37
|
+
* MonitorResource's import chain reaches the native isolated-vm addon
|
|
38
|
+
* through the sandbox runner. Nothing here touches the sandbox and the
|
|
39
|
+
* prebuilt binary cannot always dlopen in the test environment, so stub it
|
|
40
|
+
* out before anything imports it.
|
|
41
|
+
*/
|
|
42
|
+
jest.mock("isolated-vm", () => {
|
|
43
|
+
return {};
|
|
44
|
+
});
|
|
45
|
+
|
|
36
46
|
jest.mock("../../../../Server/Utils/Monitor/MonitorCriteriaEvaluator", () => {
|
|
37
47
|
return {
|
|
38
48
|
__esModule: true,
|
|
@@ -452,4 +462,80 @@ describe("MonitorResourceUtil.monitorResource ingested monitor step selection",
|
|
|
452
462
|
);
|
|
453
463
|
});
|
|
454
464
|
});
|
|
465
|
+
|
|
466
|
+
/*
|
|
467
|
+
* Over-time criteria size their evaluation window from the monitor's
|
|
468
|
+
* polling schedule, so the monitor load on this path has to select it.
|
|
469
|
+
* Without it the cadence is always inferred from the samples, and a
|
|
470
|
+
* monitor that can only ever produce one sample per window can never
|
|
471
|
+
* satisfy "All Values" - alerting goes quiet with nothing logged.
|
|
472
|
+
*/
|
|
473
|
+
describe("monitor load", () => {
|
|
474
|
+
test("selects the monitoring interval", async () => {
|
|
475
|
+
const step: MonitorStep = makeStep("Step one criteria");
|
|
476
|
+
const monitor: Monitor = makeMonitor({
|
|
477
|
+
monitorType: MonitorType.Website,
|
|
478
|
+
steps: [step],
|
|
479
|
+
});
|
|
480
|
+
monitor.monitoringInterval = "*/5 * * * *";
|
|
481
|
+
|
|
482
|
+
const probeId: ObjectID = ObjectID.generate();
|
|
483
|
+
|
|
484
|
+
const monitorProbe: MonitorProbe = new MonitorProbe();
|
|
485
|
+
monitorProbe.id = ObjectID.generate();
|
|
486
|
+
monitorProbe.probeId = probeId;
|
|
487
|
+
monitorProbe.isEnabled = true;
|
|
488
|
+
|
|
489
|
+
findOneByIdMock.mockResolvedValue(monitor);
|
|
490
|
+
findByMock.mockResolvedValue([monitorProbe]);
|
|
491
|
+
|
|
492
|
+
await MonitorResourceUtil.monitorResource(
|
|
493
|
+
makeProbeResult({
|
|
494
|
+
monitor: monitor,
|
|
495
|
+
probeId: probeId,
|
|
496
|
+
monitorStepId: step.id,
|
|
497
|
+
}),
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
const select: Record<string, unknown> = (
|
|
501
|
+
findOneByIdMock.mock.calls[0]![0] as {
|
|
502
|
+
select: Record<string, unknown>;
|
|
503
|
+
}
|
|
504
|
+
).select;
|
|
505
|
+
|
|
506
|
+
expect(select["monitoringInterval"]).toBe(true);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("hands the monitoring interval to the criteria evaluator", async () => {
|
|
510
|
+
const step: MonitorStep = makeStep("Step one criteria");
|
|
511
|
+
const monitor: Monitor = makeMonitor({
|
|
512
|
+
monitorType: MonitorType.Website,
|
|
513
|
+
steps: [step],
|
|
514
|
+
});
|
|
515
|
+
monitor.monitoringInterval = "*/5 * * * *";
|
|
516
|
+
|
|
517
|
+
const probeId: ObjectID = ObjectID.generate();
|
|
518
|
+
|
|
519
|
+
const monitorProbe: MonitorProbe = new MonitorProbe();
|
|
520
|
+
monitorProbe.id = ObjectID.generate();
|
|
521
|
+
monitorProbe.probeId = probeId;
|
|
522
|
+
monitorProbe.isEnabled = true;
|
|
523
|
+
|
|
524
|
+
findOneByIdMock.mockResolvedValue(monitor);
|
|
525
|
+
findByMock.mockResolvedValue([monitorProbe]);
|
|
526
|
+
|
|
527
|
+
await MonitorResourceUtil.monitorResource(
|
|
528
|
+
makeProbeResult({
|
|
529
|
+
monitor: monitor,
|
|
530
|
+
probeId: probeId,
|
|
531
|
+
monitorStepId: step.id,
|
|
532
|
+
}),
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
const evaluatorInput: { monitor: Monitor } = processMonitorStepMock.mock
|
|
536
|
+
.calls[0]![0] as { monitor: Monitor };
|
|
537
|
+
|
|
538
|
+
expect(evaluatorInput.monitor.monitoringInterval).toBe("*/5 * * * *");
|
|
539
|
+
});
|
|
540
|
+
});
|
|
455
541
|
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import applyStatusPageContentSecurityPolicy, {
|
|
2
|
+
isStatusPageFallbackDocumentPath,
|
|
3
|
+
STATUS_PAGE_CONTENT_SECURITY_POLICY_HEADER_NAME,
|
|
4
|
+
STATUS_PAGE_FALLBACK_CONTENT_SECURITY_POLICY,
|
|
5
|
+
} from "../../../Server/Utils/StatusPageContentSecurityPolicy";
|
|
6
|
+
import { ExpressRequest, ExpressResponse } from "../../../Server/Utils/Express";
|
|
7
|
+
import { describe, expect, it, jest } from "@jest/globals";
|
|
8
|
+
|
|
9
|
+
type FakeResponse = {
|
|
10
|
+
set: ReturnType<typeof jest.fn>;
|
|
11
|
+
headersSent: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function fakeRequest(path: string): ExpressRequest {
|
|
15
|
+
return { path } as unknown as ExpressRequest;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function fakeResponse(headersSent: boolean = false): FakeResponse {
|
|
19
|
+
return {
|
|
20
|
+
set: jest.fn(),
|
|
21
|
+
headersSent,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function policyDirectives(): Map<string, Array<string>> {
|
|
26
|
+
const directives: Map<string, Array<string>> = new Map();
|
|
27
|
+
|
|
28
|
+
for (const rawDirective of STATUS_PAGE_FALLBACK_CONTENT_SECURITY_POLICY.split(
|
|
29
|
+
";",
|
|
30
|
+
)) {
|
|
31
|
+
const parts: Array<string> = rawDirective.trim().split(/\s+/);
|
|
32
|
+
const name: string | undefined = parts.shift();
|
|
33
|
+
|
|
34
|
+
if (name) {
|
|
35
|
+
directives.set(name, parts);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return directives;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("status page fallback Content Security Policy", () => {
|
|
43
|
+
it.each([
|
|
44
|
+
"/status-page",
|
|
45
|
+
"/status-page/",
|
|
46
|
+
"/status-page/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
|
|
47
|
+
"/status-page/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee/incidents",
|
|
48
|
+
"/status-page/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee?lang=en",
|
|
49
|
+
])("classifies %s as a fallback document path", (path: string) => {
|
|
50
|
+
expect(isStatusPageFallbackDocumentPath(path)).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it.each([
|
|
54
|
+
"",
|
|
55
|
+
"/",
|
|
56
|
+
"/incidents",
|
|
57
|
+
"/rss",
|
|
58
|
+
"/llms.txt",
|
|
59
|
+
"/status-page-api/master-page/id",
|
|
60
|
+
"/status-pages/id",
|
|
61
|
+
])(
|
|
62
|
+
"does not classify custom-domain or sibling path %s as fallback",
|
|
63
|
+
(path: string) => {
|
|
64
|
+
expect(isStatusPageFallbackDocumentPath(path)).toBe(false);
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
it("sets the policy on a fallback document", () => {
|
|
69
|
+
const res: FakeResponse = fakeResponse();
|
|
70
|
+
|
|
71
|
+
applyStatusPageContentSecurityPolicy(
|
|
72
|
+
fakeRequest("/status-page/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"),
|
|
73
|
+
res as unknown as ExpressResponse,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(res.set).toHaveBeenCalledTimes(1);
|
|
77
|
+
expect(res.set).toHaveBeenCalledWith(
|
|
78
|
+
STATUS_PAGE_CONTENT_SECURITY_POLICY_HEADER_NAME,
|
|
79
|
+
STATUS_PAGE_FALLBACK_CONTENT_SECURITY_POLICY,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("leaves a custom-domain document unchanged", () => {
|
|
84
|
+
const res: FakeResponse = fakeResponse();
|
|
85
|
+
|
|
86
|
+
applyStatusPageContentSecurityPolicy(
|
|
87
|
+
fakeRequest("/"),
|
|
88
|
+
res as unknown as ExpressResponse,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(res.set).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("does not touch a response whose headers were already sent", () => {
|
|
95
|
+
const res: FakeResponse = fakeResponse(true);
|
|
96
|
+
|
|
97
|
+
applyStatusPageContentSecurityPolicy(
|
|
98
|
+
fakeRequest("/status-page/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"),
|
|
99
|
+
res as unknown as ExpressResponse,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(res.set).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("blocks dynamic JavaScript evaluation", () => {
|
|
106
|
+
const scriptSources: Array<string> =
|
|
107
|
+
policyDirectives().get("script-src") || [];
|
|
108
|
+
|
|
109
|
+
expect(scriptSources).not.toContain("'unsafe-eval'");
|
|
110
|
+
expect(STATUS_PAGE_FALLBACK_CONTENT_SECURITY_POLICY).not.toContain(
|
|
111
|
+
"unsafe-eval",
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("blocks inline event-handler attributes even though template scripts remain inline", () => {
|
|
116
|
+
const directives: Map<string, Array<string>> = policyDirectives();
|
|
117
|
+
|
|
118
|
+
expect(directives.get("script-src")).toContain("'unsafe-inline'");
|
|
119
|
+
expect(directives.get("script-src-attr")).toEqual(["'none'"]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("keeps the template bundle and HTTPS Tag Manager dependencies loadable", () => {
|
|
123
|
+
const scriptSources: Array<string> =
|
|
124
|
+
policyDirectives().get("script-src") || [];
|
|
125
|
+
|
|
126
|
+
expect(scriptSources).toContain("'self'");
|
|
127
|
+
expect(scriptSources).toContain("https:");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("forbids plugin documents and attacker-controlled base URLs", () => {
|
|
131
|
+
const directives: Map<string, Array<string>> = policyDirectives();
|
|
132
|
+
|
|
133
|
+
expect(directives.get("object-src")).toEqual(["'none'"]);
|
|
134
|
+
expect(directives.get("base-uri")).toEqual(["'none'"]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import StatusPageDomain from "../../../Models/DatabaseModels/StatusPageDomain";
|
|
2
|
+
import StatusPageDomainService from "../../../Server/Services/StatusPageDomainService";
|
|
3
|
+
import {
|
|
4
|
+
canServeStatusPageCustomizations,
|
|
5
|
+
isPrimaryStatusPageHost,
|
|
6
|
+
normalizeStatusPageRequestHost,
|
|
7
|
+
} from "../../../Server/Utils/StatusPageCustomizationAccess";
|
|
8
|
+
import { ExpressRequest } from "../../../Server/Utils/Express";
|
|
9
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
10
|
+
import {
|
|
11
|
+
afterEach,
|
|
12
|
+
beforeEach,
|
|
13
|
+
describe,
|
|
14
|
+
expect,
|
|
15
|
+
it,
|
|
16
|
+
jest,
|
|
17
|
+
} from "@jest/globals";
|
|
18
|
+
|
|
19
|
+
const makeRequest: (host?: string, forwardedHost?: string) => ExpressRequest = (
|
|
20
|
+
host?: string,
|
|
21
|
+
forwardedHost?: string,
|
|
22
|
+
): ExpressRequest => {
|
|
23
|
+
const headers: Record<string, string> = {};
|
|
24
|
+
|
|
25
|
+
if (host !== undefined) {
|
|
26
|
+
headers["host"] = host;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (forwardedHost !== undefined) {
|
|
30
|
+
headers["x-forwarded-host"] = forwardedHost;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
headers,
|
|
35
|
+
get: (name: string): string | undefined => {
|
|
36
|
+
return headers[name.toLowerCase()];
|
|
37
|
+
},
|
|
38
|
+
} as unknown as ExpressRequest;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
describe("normalizeStatusPageRequestHost", () => {
|
|
42
|
+
it.each([
|
|
43
|
+
["status.example.com", "status.example.com"],
|
|
44
|
+
[" STATUS.Example.COM ", "status.example.com"],
|
|
45
|
+
["status.example.com:443", "status.example.com"],
|
|
46
|
+
["status.example.com.:8443", "status.example.com"],
|
|
47
|
+
["[::1]", "[::1]"],
|
|
48
|
+
["[::1]:8080", "[::1]"],
|
|
49
|
+
])("normalizes %s", (rawHost: string, expected: string) => {
|
|
50
|
+
expect(normalizeStatusPageRequestHost(rawHost)).toBe(expected);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it.each([
|
|
54
|
+
undefined,
|
|
55
|
+
"",
|
|
56
|
+
" ",
|
|
57
|
+
"https://status.example.com",
|
|
58
|
+
"status.example.com/path",
|
|
59
|
+
"status.example.com?query=true",
|
|
60
|
+
"status.example.com#fragment",
|
|
61
|
+
"user@status.example.com",
|
|
62
|
+
"status.example.com,evil.example.com",
|
|
63
|
+
"status example.com",
|
|
64
|
+
"status.example.com:not-a-port",
|
|
65
|
+
"status.example.com:99999",
|
|
66
|
+
"status..example.com",
|
|
67
|
+
])("rejects malformed authority %p", (rawHost: string | undefined) => {
|
|
68
|
+
expect(normalizeStatusPageRequestHost(rawHost)).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("isPrimaryStatusPageHost", () => {
|
|
73
|
+
const originalHost: string | undefined = process.env["HOST"];
|
|
74
|
+
|
|
75
|
+
afterEach(() => {
|
|
76
|
+
if (originalHost === undefined) {
|
|
77
|
+
delete process.env["HOST"];
|
|
78
|
+
} else {
|
|
79
|
+
process.env["HOST"] = originalHost;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("recognizes the configured application host", () => {
|
|
84
|
+
process.env["HOST"] = "APP.Example.com:443";
|
|
85
|
+
|
|
86
|
+
expect(isPrimaryStatusPageHost("app.example.com")).toBe(true);
|
|
87
|
+
expect(isPrimaryStatusPageHost("status.example.com")).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it.each(["localhost", "ingress"])(
|
|
91
|
+
"always recognizes the built-in primary host %s",
|
|
92
|
+
(hostname: string) => {
|
|
93
|
+
process.env["HOST"] = "app.example.com";
|
|
94
|
+
|
|
95
|
+
expect(isPrimaryStatusPageHost(hostname)).toBe(true);
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
it("fails closed when the configured application host is malformed", () => {
|
|
100
|
+
process.env["HOST"] = "https://app.example.com/path";
|
|
101
|
+
|
|
102
|
+
expect(isPrimaryStatusPageHost("status.example.com")).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("canServeStatusPageCustomizations", () => {
|
|
107
|
+
const originalHost: string | undefined = process.env["HOST"];
|
|
108
|
+
let statusPageId: ObjectID;
|
|
109
|
+
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
process.env["HOST"] = "app.example.com";
|
|
112
|
+
statusPageId = ObjectID.generate();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
afterEach(() => {
|
|
116
|
+
jest.restoreAllMocks();
|
|
117
|
+
|
|
118
|
+
if (originalHost === undefined) {
|
|
119
|
+
delete process.env["HOST"];
|
|
120
|
+
} else {
|
|
121
|
+
process.env["HOST"] = originalHost;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it.each([
|
|
126
|
+
["the configured application host", "APP.EXAMPLE.COM:443"],
|
|
127
|
+
["localhost", "localhost:3000"],
|
|
128
|
+
["the internal ingress host", "ingress"],
|
|
129
|
+
["a missing Host", undefined],
|
|
130
|
+
["a malformed Host", "status.example.com/path"],
|
|
131
|
+
])(
|
|
132
|
+
"denies %s without consulting status-page domains",
|
|
133
|
+
async (_label: string, host: string | undefined) => {
|
|
134
|
+
const lookupSpy: ReturnType<typeof jest.spyOn> = jest.spyOn(
|
|
135
|
+
StatusPageDomainService,
|
|
136
|
+
"findOneBy",
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
await expect(
|
|
140
|
+
canServeStatusPageCustomizations({
|
|
141
|
+
req: makeRequest(host),
|
|
142
|
+
statusPageId,
|
|
143
|
+
}),
|
|
144
|
+
).resolves.toBe(false);
|
|
145
|
+
|
|
146
|
+
expect(lookupSpy).not.toHaveBeenCalled();
|
|
147
|
+
},
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
it("requires a verified domain bound to the exact status page and normalized host", async () => {
|
|
151
|
+
const statusPageDomain: StatusPageDomain = new StatusPageDomain();
|
|
152
|
+
statusPageDomain.id = ObjectID.generate();
|
|
153
|
+
|
|
154
|
+
const lookupSpy: ReturnType<typeof jest.spyOn> = jest
|
|
155
|
+
.spyOn(StatusPageDomainService, "findOneBy")
|
|
156
|
+
.mockResolvedValue(statusPageDomain);
|
|
157
|
+
|
|
158
|
+
await expect(
|
|
159
|
+
canServeStatusPageCustomizations({
|
|
160
|
+
req: makeRequest("STATUS.Customer.Example.:443"),
|
|
161
|
+
statusPageId,
|
|
162
|
+
}),
|
|
163
|
+
).resolves.toBe(true);
|
|
164
|
+
|
|
165
|
+
expect(lookupSpy).toHaveBeenCalledTimes(1);
|
|
166
|
+
expect(lookupSpy).toHaveBeenCalledWith({
|
|
167
|
+
query: {
|
|
168
|
+
fullDomain: "status.customer.example",
|
|
169
|
+
statusPageId,
|
|
170
|
+
isCnameVerified: true,
|
|
171
|
+
domain: {
|
|
172
|
+
isVerified: true,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
select: {
|
|
176
|
+
_id: true,
|
|
177
|
+
},
|
|
178
|
+
props: {
|
|
179
|
+
isRoot: true,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("denies an unknown or unverified custom domain", async () => {
|
|
185
|
+
jest.spyOn(StatusPageDomainService, "findOneBy").mockResolvedValue(null);
|
|
186
|
+
|
|
187
|
+
await expect(
|
|
188
|
+
canServeStatusPageCustomizations({
|
|
189
|
+
req: makeRequest("status.customer.example"),
|
|
190
|
+
statusPageId,
|
|
191
|
+
}),
|
|
192
|
+
).resolves.toBe(false);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("fails closed when the status-page-domain lookup throws", async () => {
|
|
196
|
+
jest
|
|
197
|
+
.spyOn(StatusPageDomainService, "findOneBy")
|
|
198
|
+
.mockRejectedValue(new Error("database unavailable"));
|
|
199
|
+
|
|
200
|
+
await expect(
|
|
201
|
+
canServeStatusPageCustomizations({
|
|
202
|
+
req: makeRequest("status.customer.example"),
|
|
203
|
+
statusPageId,
|
|
204
|
+
}),
|
|
205
|
+
).resolves.toBe(false);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("never lets X-Forwarded-Host override the raw primary Host", async () => {
|
|
209
|
+
const lookupSpy: ReturnType<typeof jest.spyOn> = jest.spyOn(
|
|
210
|
+
StatusPageDomainService,
|
|
211
|
+
"findOneBy",
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
await expect(
|
|
215
|
+
canServeStatusPageCustomizations({
|
|
216
|
+
req: makeRequest("app.example.com", "status.customer.example"),
|
|
217
|
+
statusPageId,
|
|
218
|
+
}),
|
|
219
|
+
).resolves.toBe(false);
|
|
220
|
+
|
|
221
|
+
expect(lookupSpy).not.toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS,
|
|
3
|
+
MAX_MONITOR_REQUEST_TIMEOUT_IN_MS,
|
|
4
|
+
clampMonitorRequestTimeoutInMs,
|
|
5
|
+
} from "../../../Types/Monitor/MonitorStep";
|
|
6
|
+
import { describe, expect, test } from "@jest/globals";
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* clampMonitorRequestTimeoutInMs constrains a per-step request timeout to the
|
|
10
|
+
* range the probe accepts. It has three decision points:
|
|
11
|
+
* 1. a falsy value (0 / NaN and, defensively, null / undefined) -> DEFAULT
|
|
12
|
+
* 2. a non-positive value (value <= 0) -> DEFAULT
|
|
13
|
+
* 3. a value above the cap (value > MAX) -> MAX
|
|
14
|
+
* anything else passes through unchanged. These tests walk each branch, both
|
|
15
|
+
* boundaries and the defensive coercions the callers can hand it.
|
|
16
|
+
*
|
|
17
|
+
* Note: with the current config DEFAULT and MAX happen to share the value
|
|
18
|
+
* 60000, so every assertion checks the constant that names the branch under
|
|
19
|
+
* test rather than the bare number, keeping the intent readable even if the
|
|
20
|
+
* two constants diverge later.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
describe("clampMonitorRequestTimeoutInMs", () => {
|
|
24
|
+
describe("invalid / non-positive inputs fall back to the default", () => {
|
|
25
|
+
test("zero returns the default (caught by the falsy check)", () => {
|
|
26
|
+
const result: number = clampMonitorRequestTimeoutInMs(0);
|
|
27
|
+
expect(result).toBe(DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("negative zero returns the default", () => {
|
|
31
|
+
const result: number = clampMonitorRequestTimeoutInMs(-0);
|
|
32
|
+
expect(result).toBe(DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("NaN returns the default (NaN is falsy)", () => {
|
|
36
|
+
const result: number = clampMonitorRequestTimeoutInMs(NaN);
|
|
37
|
+
expect(result).toBe(DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("a small negative value returns the default (value <= 0 branch)", () => {
|
|
41
|
+
const result: number = clampMonitorRequestTimeoutInMs(-1);
|
|
42
|
+
expect(result).toBe(DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("a negative fraction returns the default", () => {
|
|
46
|
+
const result: number = clampMonitorRequestTimeoutInMs(-0.5);
|
|
47
|
+
expect(result).toBe(DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("a large negative value returns the default, not a clamp", () => {
|
|
51
|
+
const result: number = clampMonitorRequestTimeoutInMs(-999999);
|
|
52
|
+
expect(result).toBe(DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("negative Infinity returns the default", () => {
|
|
56
|
+
const result: number = clampMonitorRequestTimeoutInMs(-Infinity);
|
|
57
|
+
expect(result).toBe(DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
* The signature is typed as number, but real callers (JSON / DB rows,
|
|
62
|
+
* loosely typed UI state) can hand it a missing value. The falsy guard is
|
|
63
|
+
* what protects the probe here, so exercise it explicitly.
|
|
64
|
+
*/
|
|
65
|
+
test("undefined coerced through the signature returns the default", () => {
|
|
66
|
+
const result: number = clampMonitorRequestTimeoutInMs(
|
|
67
|
+
undefined as unknown as number,
|
|
68
|
+
);
|
|
69
|
+
expect(result).toBe(DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("null coerced through the signature returns the default", () => {
|
|
73
|
+
const result: number = clampMonitorRequestTimeoutInMs(
|
|
74
|
+
null as unknown as number,
|
|
75
|
+
);
|
|
76
|
+
expect(result).toBe(DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("in-range values pass through unchanged", () => {
|
|
81
|
+
test("the smallest positive integer is returned as-is", () => {
|
|
82
|
+
const result: number = clampMonitorRequestTimeoutInMs(1);
|
|
83
|
+
expect(result).toBe(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("a fractional millisecond value is preserved", () => {
|
|
87
|
+
const result: number = clampMonitorRequestTimeoutInMs(1500.75);
|
|
88
|
+
expect(result).toBe(1500.75);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("a typical mid-range timeout is returned unchanged", () => {
|
|
92
|
+
const result: number = clampMonitorRequestTimeoutInMs(30000);
|
|
93
|
+
expect(result).toBe(30000);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("a value just below the cap is not clamped", () => {
|
|
97
|
+
const justUnder: number = MAX_MONITOR_REQUEST_TIMEOUT_IN_MS - 1;
|
|
98
|
+
const result: number = clampMonitorRequestTimeoutInMs(justUnder);
|
|
99
|
+
expect(result).toBe(justUnder);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("the upper boundary", () => {
|
|
104
|
+
test("a value exactly at the cap is returned, not clamped (> is strict)", () => {
|
|
105
|
+
const result: number = clampMonitorRequestTimeoutInMs(
|
|
106
|
+
MAX_MONITOR_REQUEST_TIMEOUT_IN_MS,
|
|
107
|
+
);
|
|
108
|
+
expect(result).toBe(MAX_MONITOR_REQUEST_TIMEOUT_IN_MS);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("one millisecond over the cap is clamped down to the cap", () => {
|
|
112
|
+
const result: number = clampMonitorRequestTimeoutInMs(
|
|
113
|
+
MAX_MONITOR_REQUEST_TIMEOUT_IN_MS + 1,
|
|
114
|
+
);
|
|
115
|
+
expect(result).toBe(MAX_MONITOR_REQUEST_TIMEOUT_IN_MS);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("values well above the cap are clamped", () => {
|
|
120
|
+
test("a value far above the cap is clamped to the cap", () => {
|
|
121
|
+
const result: number = clampMonitorRequestTimeoutInMs(
|
|
122
|
+
MAX_MONITOR_REQUEST_TIMEOUT_IN_MS * 10,
|
|
123
|
+
);
|
|
124
|
+
expect(result).toBe(MAX_MONITOR_REQUEST_TIMEOUT_IN_MS);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("positive Infinity is clamped to the cap", () => {
|
|
128
|
+
const result: number = clampMonitorRequestTimeoutInMs(Infinity);
|
|
129
|
+
expect(result).toBe(MAX_MONITOR_REQUEST_TIMEOUT_IN_MS);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("purity and stability", () => {
|
|
134
|
+
test("the same input always yields the same output", () => {
|
|
135
|
+
const first: number = clampMonitorRequestTimeoutInMs(45000);
|
|
136
|
+
const second: number = clampMonitorRequestTimeoutInMs(45000);
|
|
137
|
+
expect(first).toBe(second);
|
|
138
|
+
expect(first).toBe(45000);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("the exported cap is a positive finite number", () => {
|
|
142
|
+
expect(Number.isFinite(MAX_MONITOR_REQUEST_TIMEOUT_IN_MS)).toBe(true);
|
|
143
|
+
expect(MAX_MONITOR_REQUEST_TIMEOUT_IN_MS).toBeGreaterThan(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("the clamped result never exceeds the cap across a sweep", () => {
|
|
147
|
+
const samples: Array<number> = [
|
|
148
|
+
-5000,
|
|
149
|
+
0,
|
|
150
|
+
1,
|
|
151
|
+
500,
|
|
152
|
+
60000,
|
|
153
|
+
60001,
|
|
154
|
+
250000,
|
|
155
|
+
Infinity,
|
|
156
|
+
NaN,
|
|
157
|
+
];
|
|
158
|
+
for (const sample of samples) {
|
|
159
|
+
const result: number = clampMonitorRequestTimeoutInMs(sample);
|
|
160
|
+
expect(result).toBeLessThanOrEqual(MAX_MONITOR_REQUEST_TIMEOUT_IN_MS);
|
|
161
|
+
expect(result).toBeGreaterThan(0);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|