@oneuptime/common 12.0.27 → 12.0.29
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/AnalyticsModels/RumSessionChunk.ts +52 -0
- package/Models/DatabaseModels/CloudResource.ts +17 -1
- package/Models/DatabaseModels/DashboardDomain.ts +43 -0
- package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
- package/Models/DatabaseModels/RumApplication.ts +35 -2
- package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
- package/Server/API/DashboardDomainAPI.ts +74 -0
- package/Server/API/MicrosoftTeamsAPI.ts +81 -180
- package/Server/API/StatusPageDomainAPI.ts +74 -0
- package/Server/API/TelemetryAPI.ts +104 -15
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/ProjectAuthorization.ts +6 -1
- package/Server/Services/DashboardDomainService.ts +131 -0
- package/Server/Services/LogPipelineProcessorService.ts +88 -0
- package/Server/Services/MonitorTemplateService.ts +70 -11
- package/Server/Services/StatusPageDomainService.ts +163 -3
- package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
- package/Server/Types/Domain.ts +17 -0
- package/Server/Utils/APIKey/AccessPermission.ts +24 -0
- package/Server/Utils/Greenlock/Greenlock.ts +156 -58
- package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +12 -5
- package/Server/Utils/SessionReplay/SessionReplayIdentity.ts +102 -0
- package/Server/Utils/SessionReplay/SessionReplayReadService.ts +11 -2
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
- package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
- package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +81 -0
- package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
- package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
- package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
- package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
- package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
- package/Tests/Server/API/SessionReplayAPI.test.ts +232 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
- package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
- package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
- package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
- package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
- package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
- package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
- package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
- package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
- package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
- package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
- package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
- package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
- package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
- package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
- package/Tests/Types/API/HTTPResponse.test.ts +175 -0
- package/Tests/Types/API/Route.test.ts +36 -4
- package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
- package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
- package/Tests/Types/DateLocalTimeString.test.ts +259 -0
- package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
- package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
- package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
- package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
- package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
- package/Tests/UI/Utils/Navigation.test.ts +82 -0
- package/Tests/Utils/Array.test.ts +100 -0
- package/Tests/Utils/CertificateReissue.test.ts +225 -0
- package/Tests/Utils/Grok/Grok.test.ts +492 -0
- package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
- package/Types/API/Route.ts +20 -5
- package/Types/Date.ts +143 -13
- package/Types/Rum/SessionReplay.ts +76 -1
- package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
- package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
- package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
- package/UI/Utils/Navigation.ts +42 -0
- package/Utils/Array.ts +37 -0
- package/Utils/CertificateReissue.ts +128 -0
- package/Utils/Grok/Grok.ts +461 -0
- package/Utils/Grok/GrokPatterns.ts +118 -0
- package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +48 -0
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
- package/build/dist/Models/DatabaseModels/CloudResource.js +19 -2
- package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +37 -3
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
- package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
- package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
- package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +67 -14
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.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/ProjectAuthorization.js +6 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/DashboardDomainService.js +104 -0
- package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
- package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
- package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
- package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
- package/build/dist/Server/Types/Domain.js +16 -0
- package/build/dist/Server/Types/Domain.js.map +1 -1
- package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
- package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
- package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
- package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js +59 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +9 -2
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/API/Route.js +16 -5
- package/build/dist/Types/API/Route.js.map +1 -1
- package/build/dist/Types/Date.js +115 -13
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplay.js +38 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +30 -0
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/Utils/Array.js +25 -0
- package/build/dist/Utils/Array.js.map +1 -1
- package/build/dist/Utils/CertificateReissue.js +97 -0
- package/build/dist/Utils/CertificateReissue.js.map +1 -0
- package/build/dist/Utils/Grok/Grok.js +324 -0
- package/build/dist/Utils/Grok/Grok.js.map +1 -0
- package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
- package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
- package/package.json +1 -1
- package/UI/Utils/JsonWebToken.ts +0 -14
- package/build/dist/UI/Utils/JsonWebToken.js +0 -10
- package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import StatusPageDomainService from "../../../Server/Services/StatusPageDomainService";
|
|
2
|
+
import DashboardDomainService from "../../../Server/Services/DashboardDomainService";
|
|
3
|
+
import CertificateReissueUtil from "../../../Utils/CertificateReissue";
|
|
4
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
5
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
6
|
+
import TooManyRequestsException from "../../../Types/Exception/TooManyRequestsException";
|
|
7
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
8
|
+
import { FindOperator } from "typeorm";
|
|
9
|
+
import { afterEach, describe, expect, jest, test } from "@jest/globals";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* reissueCert - the service behind the dashboard's "Reissue SSL" button, on
|
|
13
|
+
* both custom-domain surfaces.
|
|
14
|
+
*
|
|
15
|
+
* Every certificate OneUptime orders is ordered against a single Let's
|
|
16
|
+
* Encrypt account shared by the whole installation, and the automated renewal
|
|
17
|
+
* cron spends from the same allowance. A customer-pressable button on top of
|
|
18
|
+
* that is only safe if three things hold, and each of them is a way the
|
|
19
|
+
* feature can quietly stop protecting anything:
|
|
20
|
+
*
|
|
21
|
+
* 1. The cooldown is claimed BEFORE the order is attempted, and is not
|
|
22
|
+
* rolled back when the order fails. A failed order still costs a
|
|
23
|
+
* validation attempt at the CA, and a failing domain is exactly the one
|
|
24
|
+
* somebody presses again.
|
|
25
|
+
* 2. The cooldown is claimed by the WRITE, not by a read above it - two
|
|
26
|
+
* clicks that arrive together both see a row that is not cooling down,
|
|
27
|
+
* so only a conditional update can pick a winner.
|
|
28
|
+
* 3. A domain with nothing to reissue never reaches the CA at all.
|
|
29
|
+
*
|
|
30
|
+
* No database and no ACME client: findOneBy, updateOneBy and orderCert are
|
|
31
|
+
* all spied on, so what is asserted is the ORDER and the CONDITIONS of those
|
|
32
|
+
* calls, which is precisely where the protection lives.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
type DomainRow = {
|
|
36
|
+
_id: string;
|
|
37
|
+
id: ObjectID;
|
|
38
|
+
fullDomain: string;
|
|
39
|
+
isCnameVerified: boolean;
|
|
40
|
+
isSslOrdered: boolean;
|
|
41
|
+
isCustomCertificate: boolean;
|
|
42
|
+
certificateReissueRequestedAt?: Date | undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const DOMAIN_ID: ObjectID = ObjectID.generate();
|
|
46
|
+
|
|
47
|
+
type MakeDomainFunction = (overrides?: Partial<DomainRow>) => DomainRow;
|
|
48
|
+
|
|
49
|
+
const makeDomain: MakeDomainFunction = (
|
|
50
|
+
overrides: Partial<DomainRow> = {},
|
|
51
|
+
): DomainRow => {
|
|
52
|
+
return {
|
|
53
|
+
_id: DOMAIN_ID.toString(),
|
|
54
|
+
id: DOMAIN_ID,
|
|
55
|
+
fullDomain: "status.example.com",
|
|
56
|
+
isCnameVerified: true,
|
|
57
|
+
isSslOrdered: true,
|
|
58
|
+
isCustomCertificate: false,
|
|
59
|
+
certificateReissueRequestedAt: undefined,
|
|
60
|
+
...overrides,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type ReissueService = {
|
|
65
|
+
reissueCert: (id: ObjectID) => Promise<void>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type HarnessCalls = {
|
|
69
|
+
// Every updateOneBy the service made, in order.
|
|
70
|
+
updates: Array<{
|
|
71
|
+
query: Record<string, unknown>;
|
|
72
|
+
data: Record<string, unknown>;
|
|
73
|
+
}>;
|
|
74
|
+
// Domains handed to orderCert, in order.
|
|
75
|
+
ordered: Array<string>;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
type SetUpHarnessFunction = (data: {
|
|
79
|
+
service: unknown;
|
|
80
|
+
domain: DomainRow | null;
|
|
81
|
+
// Rows the conditional claim reports as written. 1 = claim won, 0 = lost.
|
|
82
|
+
claimedRowCount?: number;
|
|
83
|
+
orderCertThrows?: Error;
|
|
84
|
+
}) => HarnessCalls;
|
|
85
|
+
|
|
86
|
+
const setUpHarness: SetUpHarnessFunction = (data: {
|
|
87
|
+
service: unknown;
|
|
88
|
+
domain: DomainRow | null;
|
|
89
|
+
claimedRowCount?: number;
|
|
90
|
+
orderCertThrows?: Error;
|
|
91
|
+
}): HarnessCalls => {
|
|
92
|
+
const calls: HarnessCalls = { updates: [], ordered: [] };
|
|
93
|
+
|
|
94
|
+
jest
|
|
95
|
+
.spyOn(data.service as never, "findOneBy")
|
|
96
|
+
.mockResolvedValue(data.domain as never);
|
|
97
|
+
|
|
98
|
+
jest
|
|
99
|
+
.spyOn(data.service as never, "updateOneBy")
|
|
100
|
+
.mockImplementation((async (update: {
|
|
101
|
+
query: Record<string, unknown>;
|
|
102
|
+
data: Record<string, unknown>;
|
|
103
|
+
}): Promise<number> => {
|
|
104
|
+
calls.updates.push({ query: update.query, data: update.data });
|
|
105
|
+
return data.claimedRowCount === undefined ? 1 : data.claimedRowCount;
|
|
106
|
+
}) as never);
|
|
107
|
+
|
|
108
|
+
jest
|
|
109
|
+
.spyOn(data.service as never, "orderCert")
|
|
110
|
+
.mockImplementation((async (domain: {
|
|
111
|
+
fullDomain?: string;
|
|
112
|
+
}): Promise<void> => {
|
|
113
|
+
calls.ordered.push(domain.fullDomain as string);
|
|
114
|
+
|
|
115
|
+
if (data.orderCertThrows) {
|
|
116
|
+
throw data.orderCertThrows;
|
|
117
|
+
}
|
|
118
|
+
}) as never);
|
|
119
|
+
|
|
120
|
+
return calls;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const services: Array<[string, unknown]> = [
|
|
124
|
+
["StatusPageDomainService", StatusPageDomainService],
|
|
125
|
+
["DashboardDomainService", DashboardDomainService],
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
describe.each(services)("%s.reissueCert", (_name: string, service: unknown) => {
|
|
129
|
+
afterEach(() => {
|
|
130
|
+
jest.restoreAllMocks();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("orders a fresh certificate for an eligible domain", async () => {
|
|
134
|
+
const calls: HarnessCalls = setUpHarness({
|
|
135
|
+
service,
|
|
136
|
+
domain: makeDomain(),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await (service as ReissueService).reissueCert(DOMAIN_ID);
|
|
140
|
+
|
|
141
|
+
expect(calls.ordered).toEqual(["status.example.com"]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
/*
|
|
145
|
+
* The whole point of a reissue: it must reach the CA even though the
|
|
146
|
+
* domain already has a working certificate. order-ssl refuses in exactly
|
|
147
|
+
* this state ("SSL is already provisioned"), which is why this button had
|
|
148
|
+
* to exist at all.
|
|
149
|
+
*/
|
|
150
|
+
test("reissues a domain that already has a provisioned certificate", async () => {
|
|
151
|
+
const calls: HarnessCalls = setUpHarness({
|
|
152
|
+
service,
|
|
153
|
+
domain: makeDomain({ isSslOrdered: true }),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await (service as ReissueService).reissueCert(DOMAIN_ID);
|
|
157
|
+
|
|
158
|
+
expect(calls.ordered).toHaveLength(1);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("the cooldown", () => {
|
|
162
|
+
test("stamps the request before ordering anything", async () => {
|
|
163
|
+
const order: Array<string> = [];
|
|
164
|
+
|
|
165
|
+
jest
|
|
166
|
+
.spyOn(service as never, "findOneBy")
|
|
167
|
+
.mockResolvedValue(makeDomain() as never);
|
|
168
|
+
|
|
169
|
+
jest
|
|
170
|
+
.spyOn(service as never, "updateOneBy")
|
|
171
|
+
.mockImplementation((async (): Promise<number> => {
|
|
172
|
+
order.push("stamp");
|
|
173
|
+
return 1;
|
|
174
|
+
}) as never);
|
|
175
|
+
|
|
176
|
+
jest
|
|
177
|
+
.spyOn(service as never, "orderCert")
|
|
178
|
+
.mockImplementation((async (): Promise<void> => {
|
|
179
|
+
order.push("order");
|
|
180
|
+
}) as never);
|
|
181
|
+
|
|
182
|
+
await (service as ReissueService).reissueCert(DOMAIN_ID);
|
|
183
|
+
|
|
184
|
+
expect(order).toEqual(["stamp", "order"]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
/*
|
|
188
|
+
* The regression that would make the throttle useless. A domain whose
|
|
189
|
+
* CNAME is half broken fails validation on every attempt, and Let's
|
|
190
|
+
* Encrypt allows only five failed validations per hostname per hour.
|
|
191
|
+
* If a failed order released the cooldown, that budget - which the
|
|
192
|
+
* renewal cron shares - could be spent in seconds.
|
|
193
|
+
*/
|
|
194
|
+
test("keeps the stamp when the order fails", async () => {
|
|
195
|
+
const calls: HarnessCalls = setUpHarness({
|
|
196
|
+
service,
|
|
197
|
+
domain: makeDomain(),
|
|
198
|
+
orderCertThrows: new Error("CA refused the order"),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
await expect(
|
|
202
|
+
(service as ReissueService).reissueCert(DOMAIN_ID),
|
|
203
|
+
).rejects.toThrow("CA refused the order");
|
|
204
|
+
|
|
205
|
+
expect(calls.updates).toHaveLength(1);
|
|
206
|
+
expect(
|
|
207
|
+
calls.updates[0]!.data["certificateReissueRequestedAt"],
|
|
208
|
+
).toBeInstanceOf(Date);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("claims the row with the cooldown in the query, not with a read", async () => {
|
|
212
|
+
/*
|
|
213
|
+
* Two clicks that arrive together both read a row that is not cooling
|
|
214
|
+
* down. Only a write whose WHERE clause carries the cooldown can pick
|
|
215
|
+
* a winner - which is why the condition has to be in the query.
|
|
216
|
+
*/
|
|
217
|
+
const calls: HarnessCalls = setUpHarness({
|
|
218
|
+
service,
|
|
219
|
+
domain: makeDomain(),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await (service as ReissueService).reissueCert(DOMAIN_ID);
|
|
223
|
+
|
|
224
|
+
expect(calls.updates).toHaveLength(1);
|
|
225
|
+
expect(calls.updates[0]!.query["_id"]).toBe(DOMAIN_ID.toString());
|
|
226
|
+
expect(
|
|
227
|
+
calls.updates[0]!.query["certificateReissueRequestedAt"],
|
|
228
|
+
).toBeDefined();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
/*
|
|
232
|
+
* The condition is rendered to SQL rather than compared by shape,
|
|
233
|
+
* because the two mistakes worth catching here both produce a
|
|
234
|
+
* FindOperator that looks identical from the outside.
|
|
235
|
+
*/
|
|
236
|
+
test("a never-reissued domain is eligible, so the first press works", async () => {
|
|
237
|
+
const calls: HarnessCalls = setUpHarness({
|
|
238
|
+
service,
|
|
239
|
+
domain: makeDomain(),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await (service as ReissueService).reissueCert(DOMAIN_ID);
|
|
243
|
+
|
|
244
|
+
const claim: FindOperator<Date> = calls.updates[0]!.query[
|
|
245
|
+
"certificateReissueRequestedAt"
|
|
246
|
+
] as FindOperator<Date>;
|
|
247
|
+
|
|
248
|
+
/*
|
|
249
|
+
* The OR IS NULL half. Without it every domain that has never been
|
|
250
|
+
* reissued - which is every domain that exists the day this ships -
|
|
251
|
+
* fails the claim and the button refuses on its very first press.
|
|
252
|
+
*/
|
|
253
|
+
const sql: string = claim.getSql!("col");
|
|
254
|
+
|
|
255
|
+
expect(sql).toContain("IS NULL");
|
|
256
|
+
expect(sql).toContain("<=");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("the claim compares against exactly the cutoff the dashboard renders", async () => {
|
|
260
|
+
const calls: HarnessCalls = setUpHarness({
|
|
261
|
+
service,
|
|
262
|
+
domain: makeDomain(),
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const before: Date = OneUptimeDate.getCurrentDate();
|
|
266
|
+
await (service as ReissueService).reissueCert(DOMAIN_ID);
|
|
267
|
+
const after: Date = OneUptimeDate.getCurrentDate();
|
|
268
|
+
|
|
269
|
+
const claim: FindOperator<Date> = calls.updates[0]!.query[
|
|
270
|
+
"certificateReissueRequestedAt"
|
|
271
|
+
] as FindOperator<Date>;
|
|
272
|
+
|
|
273
|
+
const boundValues: Array<unknown> = Object.values(
|
|
274
|
+
claim.objectLiteralParameters as Record<string, unknown>,
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
expect(boundValues).toHaveLength(1);
|
|
278
|
+
|
|
279
|
+
const boundCutoff: Date = OneUptimeDate.fromString(
|
|
280
|
+
boundValues[0] as Date,
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
/*
|
|
284
|
+
* "now" moves between the assertion and the call, so the bound cutoff
|
|
285
|
+
* is pinned to the window the call could have observed rather than to
|
|
286
|
+
* a single instant. Anything other than one cooldown behind now -
|
|
287
|
+
* a cooldown ahead, or no offset at all - falls outside it.
|
|
288
|
+
*/
|
|
289
|
+
expect(boundCutoff.getTime()).toBeGreaterThanOrEqual(
|
|
290
|
+
CertificateReissueUtil.getCooldownCutoff(before).getTime(),
|
|
291
|
+
);
|
|
292
|
+
expect(boundCutoff.getTime()).toBeLessThanOrEqual(
|
|
293
|
+
CertificateReissueUtil.getCooldownCutoff(after).getTime(),
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("refuses with a 429 when the claim finds the row still cooling down", async () => {
|
|
298
|
+
const calls: HarnessCalls = setUpHarness({
|
|
299
|
+
service,
|
|
300
|
+
domain: makeDomain({
|
|
301
|
+
certificateReissueRequestedAt: OneUptimeDate.addRemoveHours(
|
|
302
|
+
OneUptimeDate.getCurrentDate(),
|
|
303
|
+
-1,
|
|
304
|
+
),
|
|
305
|
+
}),
|
|
306
|
+
claimedRowCount: 0,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
await expect(
|
|
310
|
+
(service as ReissueService).reissueCert(DOMAIN_ID),
|
|
311
|
+
).rejects.toThrow(TooManyRequestsException);
|
|
312
|
+
|
|
313
|
+
expect(calls.ordered).toEqual([]);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("the refusal tells the customer how long is left", async () => {
|
|
317
|
+
setUpHarness({
|
|
318
|
+
service,
|
|
319
|
+
domain: makeDomain({
|
|
320
|
+
certificateReissueRequestedAt: OneUptimeDate.addRemoveHours(
|
|
321
|
+
OneUptimeDate.getCurrentDate(),
|
|
322
|
+
-1,
|
|
323
|
+
),
|
|
324
|
+
}),
|
|
325
|
+
claimedRowCount: 0,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
await expect(
|
|
329
|
+
(service as ReissueService).reissueCert(DOMAIN_ID),
|
|
330
|
+
).rejects.toThrow(/try again in/i);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
/*
|
|
334
|
+
* Losing the claim race against a request that landed a millisecond
|
|
335
|
+
* earlier looks identical to a cooldown at the database, but the row
|
|
336
|
+
* this request read has no stamp on it yet. It must still refuse rather
|
|
337
|
+
* than fall through and order.
|
|
338
|
+
*/
|
|
339
|
+
test("refuses when the claim is lost and there is no stamp to explain it", async () => {
|
|
340
|
+
const calls: HarnessCalls = setUpHarness({
|
|
341
|
+
service,
|
|
342
|
+
domain: makeDomain({ certificateReissueRequestedAt: undefined }),
|
|
343
|
+
claimedRowCount: 0,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
await expect(
|
|
347
|
+
(service as ReissueService).reissueCert(DOMAIN_ID),
|
|
348
|
+
).rejects.toThrow(BadDataException);
|
|
349
|
+
|
|
350
|
+
expect(calls.ordered).toEqual([]);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe("domains with nothing to reissue never reach the CA", () => {
|
|
355
|
+
test("a domain that does not exist", async () => {
|
|
356
|
+
const calls: HarnessCalls = setUpHarness({ service, domain: null });
|
|
357
|
+
|
|
358
|
+
await expect(
|
|
359
|
+
(service as ReissueService).reissueCert(DOMAIN_ID),
|
|
360
|
+
).rejects.toThrow(BadDataException);
|
|
361
|
+
|
|
362
|
+
expect(calls.ordered).toEqual([]);
|
|
363
|
+
expect(calls.updates).toEqual([]);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("a domain using a customer-uploaded certificate", async () => {
|
|
367
|
+
const calls: HarnessCalls = setUpHarness({
|
|
368
|
+
service,
|
|
369
|
+
domain: makeDomain({ isCustomCertificate: true }),
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
await expect(
|
|
373
|
+
(service as ReissueService).reissueCert(DOMAIN_ID),
|
|
374
|
+
).rejects.toThrow(BadDataException);
|
|
375
|
+
|
|
376
|
+
expect(calls.ordered).toEqual([]);
|
|
377
|
+
expect(calls.updates).toEqual([]);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("a domain whose CNAME is not verified", async () => {
|
|
381
|
+
const calls: HarnessCalls = setUpHarness({
|
|
382
|
+
service,
|
|
383
|
+
domain: makeDomain({ isCnameVerified: false }),
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
await expect(
|
|
387
|
+
(service as ReissueService).reissueCert(DOMAIN_ID),
|
|
388
|
+
).rejects.toThrow(/CNAME is not verified/i);
|
|
389
|
+
|
|
390
|
+
expect(calls.ordered).toEqual([]);
|
|
391
|
+
expect(calls.updates).toEqual([]);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("a domain that never ordered a certificate in the first place", async () => {
|
|
395
|
+
const calls: HarnessCalls = setUpHarness({
|
|
396
|
+
service,
|
|
397
|
+
domain: makeDomain({ isSslOrdered: false }),
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
await expect(
|
|
401
|
+
(service as ReissueService).reissueCert(DOMAIN_ID),
|
|
402
|
+
).rejects.toThrow(/order one first/i);
|
|
403
|
+
|
|
404
|
+
expect(calls.ordered).toEqual([]);
|
|
405
|
+
expect(calls.updates).toEqual([]);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("a domain with no full domain resolved", async () => {
|
|
409
|
+
const calls: HarnessCalls = setUpHarness({
|
|
410
|
+
service,
|
|
411
|
+
domain: makeDomain({ fullDomain: "" }),
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
await expect(
|
|
415
|
+
(service as ReissueService).reissueCert(DOMAIN_ID),
|
|
416
|
+
).rejects.toThrow(BadDataException);
|
|
417
|
+
|
|
418
|
+
expect(calls.ordered).toEqual([]);
|
|
419
|
+
expect(calls.updates).toEqual([]);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
/*
|
|
423
|
+
* An ineligible domain must be refused without a stamp: refusing it AND
|
|
424
|
+
* burning its cooldown would leave a customer who fixed their CNAME
|
|
425
|
+
* waiting a day for a button that never cost the CA anything.
|
|
426
|
+
*/
|
|
427
|
+
test("an ineligible domain does not burn its cooldown", async () => {
|
|
428
|
+
const calls: HarnessCalls = setUpHarness({
|
|
429
|
+
service,
|
|
430
|
+
domain: makeDomain({ isCnameVerified: false }),
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
await expect(
|
|
434
|
+
(service as ReissueService).reissueCert(DOMAIN_ID),
|
|
435
|
+
).rejects.toThrow();
|
|
436
|
+
|
|
437
|
+
expect(calls.updates).toEqual([]);
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
describe("what the service reads", () => {
|
|
442
|
+
test("selects every field the eligibility checks depend on", async () => {
|
|
443
|
+
const selects: Array<Record<string, unknown>> = [];
|
|
444
|
+
|
|
445
|
+
jest
|
|
446
|
+
.spyOn(service as never, "findOneBy")
|
|
447
|
+
.mockImplementation((async (find: {
|
|
448
|
+
select: Record<string, unknown>;
|
|
449
|
+
}): Promise<DomainRow> => {
|
|
450
|
+
selects.push(find.select);
|
|
451
|
+
return makeDomain();
|
|
452
|
+
}) as never);
|
|
453
|
+
|
|
454
|
+
jest.spyOn(service as never, "updateOneBy").mockResolvedValue(1 as never);
|
|
455
|
+
jest
|
|
456
|
+
.spyOn(service as never, "orderCert")
|
|
457
|
+
.mockResolvedValue(undefined as never);
|
|
458
|
+
|
|
459
|
+
await (service as ReissueService).reissueCert(DOMAIN_ID);
|
|
460
|
+
|
|
461
|
+
/*
|
|
462
|
+
* A field that is not selected reads back as undefined, which is
|
|
463
|
+
* falsy - so a forgotten select here would silently turn a guard into
|
|
464
|
+
* a refusal for everybody, or (for isCustomCertificate) into a hole.
|
|
465
|
+
*/
|
|
466
|
+
for (const field of [
|
|
467
|
+
"fullDomain",
|
|
468
|
+
"isCnameVerified",
|
|
469
|
+
"isSslOrdered",
|
|
470
|
+
"isCustomCertificate",
|
|
471
|
+
"certificateReissueRequestedAt",
|
|
472
|
+
]) {
|
|
473
|
+
expect(selects[0]![field]).toBe(true);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
});
|