@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,377 @@
|
|
|
1
|
+
import AIConfidenceSignal, {
|
|
2
|
+
ConfidenceClassificationToken,
|
|
3
|
+
ConfidenceSignal,
|
|
4
|
+
ConfidenceSource,
|
|
5
|
+
EvidenceInput,
|
|
6
|
+
} from "../../../../../Server/Utils/AI/SRE/ConfidenceSignal";
|
|
7
|
+
import { AIChatCitation } from "../../../../../Types/AI/AIChatTypes";
|
|
8
|
+
import { describe, expect, test } from "@jest/globals";
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* AI SRE confidence signal — the pure, LLM-free half of the module (the
|
|
12
|
+
* deterministic evidence floor, the defensive token parser, and the
|
|
13
|
+
* per-consumer decision helpers). These encode the safety-critical fail
|
|
14
|
+
* directions from the module header's decision table:
|
|
15
|
+
*
|
|
16
|
+
* | source / verdict | ping | instrumentation | auto code fix |
|
|
17
|
+
* |-------------------------------|------|-----------------|---------------|
|
|
18
|
+
* | deterministic-floor | no | yes | no |
|
|
19
|
+
* | classification / CODE_FIX | yes | no | yes |
|
|
20
|
+
* | classification / NO_CODE_FIX | yes | no | no |
|
|
21
|
+
* | classification / INCONCLUSIVE | no | yes | no |
|
|
22
|
+
* | classification-failed | YES | no | no |
|
|
23
|
+
*
|
|
24
|
+
* The two load-bearing invariants these pin: (1) autonomous PR creation
|
|
25
|
+
* (instrumentation + auto code fix) fails toward doing nothing on a broken
|
|
26
|
+
* classifier, and (2) the on-call ping fails LOUDER — a broken classifier must
|
|
27
|
+
* never suppress it. Getting either direction backwards is the failure the
|
|
28
|
+
* threat model (vision §6) is written against, so it must not regress silently.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
function citation(rowCount: number): AIChatCitation {
|
|
32
|
+
return {
|
|
33
|
+
id: "C1",
|
|
34
|
+
toolName: "query_logs",
|
|
35
|
+
label: "logs",
|
|
36
|
+
queryArguments: {},
|
|
37
|
+
rowCount,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function signal(
|
|
42
|
+
source: ConfidenceSource,
|
|
43
|
+
confident: boolean,
|
|
44
|
+
codeFixRecommended?: boolean,
|
|
45
|
+
): ConfidenceSignal {
|
|
46
|
+
return { source, confident, codeFixRecommended };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("AIConfidenceSignal.isDataBearingRowCount", () => {
|
|
50
|
+
test("a positive row count is data-bearing", () => {
|
|
51
|
+
expect(AIConfidenceSignal.isDataBearingRowCount(1)).toBe(true);
|
|
52
|
+
expect(AIConfidenceSignal.isDataBearingRowCount(1000)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("zero rows is server-verified but NOT data-bearing (proof of absence)", () => {
|
|
56
|
+
expect(AIConfidenceSignal.isDataBearingRowCount(0)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("null / undefined coalesce to zero → not data-bearing", () => {
|
|
60
|
+
expect(AIConfidenceSignal.isDataBearingRowCount(null)).toBe(false);
|
|
61
|
+
expect(AIConfidenceSignal.isDataBearingRowCount(undefined)).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a negative row count is not data-bearing", () => {
|
|
65
|
+
expect(AIConfidenceSignal.isDataBearingRowCount(-5)).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("AIConfidenceSignal.evidenceFromCitations", () => {
|
|
70
|
+
test("counts every citation but only data-bearing ones as data-bearing", () => {
|
|
71
|
+
const evidence: EvidenceInput = AIConfidenceSignal.evidenceFromCitations([
|
|
72
|
+
citation(3),
|
|
73
|
+
citation(0),
|
|
74
|
+
citation(7),
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
expect(evidence.citationCount).toBe(3);
|
|
78
|
+
expect(evidence.dataBearingToolCallCount).toBe(2);
|
|
79
|
+
expect(evidence.anyToolReturnedData).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("citations that all returned zero rows count but bear no data", () => {
|
|
83
|
+
const evidence: EvidenceInput = AIConfidenceSignal.evidenceFromCitations([
|
|
84
|
+
citation(0),
|
|
85
|
+
citation(0),
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
expect(evidence.citationCount).toBe(2);
|
|
89
|
+
expect(evidence.dataBearingToolCallCount).toBe(0);
|
|
90
|
+
expect(evidence.anyToolReturnedData).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("no citations → an all-zero evidence input", () => {
|
|
94
|
+
const evidence: EvidenceInput = AIConfidenceSignal.evidenceFromCitations(
|
|
95
|
+
[],
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(evidence.citationCount).toBe(0);
|
|
99
|
+
expect(evidence.dataBearingToolCallCount).toBe(0);
|
|
100
|
+
expect(evidence.anyToolReturnedData).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("AIConfidenceSignal.hasVerifiableEvidence", () => {
|
|
105
|
+
test("true when any of the three signals is present", () => {
|
|
106
|
+
expect(
|
|
107
|
+
AIConfidenceSignal.hasVerifiableEvidence({
|
|
108
|
+
citationCount: 1,
|
|
109
|
+
dataBearingToolCallCount: 0,
|
|
110
|
+
anyToolReturnedData: false,
|
|
111
|
+
}),
|
|
112
|
+
).toBe(true);
|
|
113
|
+
|
|
114
|
+
expect(
|
|
115
|
+
AIConfidenceSignal.hasVerifiableEvidence({
|
|
116
|
+
citationCount: 0,
|
|
117
|
+
dataBearingToolCallCount: 2,
|
|
118
|
+
anyToolReturnedData: false,
|
|
119
|
+
}),
|
|
120
|
+
).toBe(true);
|
|
121
|
+
|
|
122
|
+
expect(
|
|
123
|
+
AIConfidenceSignal.hasVerifiableEvidence({
|
|
124
|
+
citationCount: 0,
|
|
125
|
+
dataBearingToolCallCount: 0,
|
|
126
|
+
anyToolReturnedData: true,
|
|
127
|
+
}),
|
|
128
|
+
).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("false only when the run minted nothing at all", () => {
|
|
132
|
+
expect(
|
|
133
|
+
AIConfidenceSignal.hasVerifiableEvidence({
|
|
134
|
+
citationCount: 0,
|
|
135
|
+
dataBearingToolCallCount: 0,
|
|
136
|
+
anyToolReturnedData: false,
|
|
137
|
+
}),
|
|
138
|
+
).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("a citation with zero data-bearing rows still passes the floor", () => {
|
|
142
|
+
// A tool that ran and proved absence is server-verified evidence.
|
|
143
|
+
const evidence: EvidenceInput = AIConfidenceSignal.evidenceFromCitations([
|
|
144
|
+
citation(0),
|
|
145
|
+
]);
|
|
146
|
+
expect(AIConfidenceSignal.hasVerifiableEvidence(evidence)).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("AIConfidenceSignal.parseClassificationToken", () => {
|
|
151
|
+
test("recognises each of the three verdicts when it is the only one", () => {
|
|
152
|
+
expect(AIConfidenceSignal.parseClassificationToken("CODE_FIX")).toBe(
|
|
153
|
+
"CODE_FIX",
|
|
154
|
+
);
|
|
155
|
+
expect(AIConfidenceSignal.parseClassificationToken("NO_CODE_FIX")).toBe(
|
|
156
|
+
"NO_CODE_FIX",
|
|
157
|
+
);
|
|
158
|
+
expect(AIConfidenceSignal.parseClassificationToken("INCONCLUSIVE")).toBe(
|
|
159
|
+
"INCONCLUSIVE",
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("is case-insensitive", () => {
|
|
164
|
+
expect(AIConfidenceSignal.parseClassificationToken("code_fix")).toBe(
|
|
165
|
+
"CODE_FIX",
|
|
166
|
+
);
|
|
167
|
+
expect(AIConfidenceSignal.parseClassificationToken("Inconclusive")).toBe(
|
|
168
|
+
"INCONCLUSIVE",
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("tolerates editorializing prose around a single verdict", () => {
|
|
173
|
+
expect(
|
|
174
|
+
AIConfidenceSignal.parseClassificationToken(
|
|
175
|
+
"The verdict is CODE_FIX because the null check is missing.",
|
|
176
|
+
),
|
|
177
|
+
).toBe("CODE_FIX");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("NO_CODE_FIX is not mis-read as CODE_FIX (underscore is a word char)", () => {
|
|
181
|
+
// Only NO_CODE_FIX appears as a whole word; CODE_FIX must not also match.
|
|
182
|
+
expect(AIConfidenceSignal.parseClassificationToken("NO_CODE_FIX")).toBe(
|
|
183
|
+
"NO_CODE_FIX",
|
|
184
|
+
);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("multiple distinct verdicts → null (unparseable)", () => {
|
|
188
|
+
expect(
|
|
189
|
+
AIConfidenceSignal.parseClassificationToken("CODE_FIX or INCONCLUSIVE"),
|
|
190
|
+
).toBeNull();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("no verdict token → null", () => {
|
|
194
|
+
expect(
|
|
195
|
+
AIConfidenceSignal.parseClassificationToken("I am not sure what to say."),
|
|
196
|
+
).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("empty / null / undefined → null", () => {
|
|
200
|
+
expect(AIConfidenceSignal.parseClassificationToken("")).toBeNull();
|
|
201
|
+
expect(AIConfidenceSignal.parseClassificationToken(null)).toBeNull();
|
|
202
|
+
expect(AIConfidenceSignal.parseClassificationToken(undefined)).toBeNull();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("word boundaries required — a substring inside another word does not match", () => {
|
|
206
|
+
// "PRECODE_FIXED" contains CODE_FIX as a substring but not as a word.
|
|
207
|
+
expect(
|
|
208
|
+
AIConfidenceSignal.parseClassificationToken("PRECODE_FIXED"),
|
|
209
|
+
).toBeNull();
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("AIConfidenceSignal.shouldSendWorkspaceNotification", () => {
|
|
214
|
+
test("classification-failed pings LOUDER regardless of the confident flag", () => {
|
|
215
|
+
expect(
|
|
216
|
+
AIConfidenceSignal.shouldSendWorkspaceNotification(
|
|
217
|
+
signal("classification-failed", false),
|
|
218
|
+
),
|
|
219
|
+
).toBe(true);
|
|
220
|
+
expect(
|
|
221
|
+
AIConfidenceSignal.shouldSendWorkspaceNotification(
|
|
222
|
+
signal("classification-failed", true),
|
|
223
|
+
),
|
|
224
|
+
).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("otherwise it follows the confident flag", () => {
|
|
228
|
+
expect(
|
|
229
|
+
AIConfidenceSignal.shouldSendWorkspaceNotification(
|
|
230
|
+
signal("classification", true, true),
|
|
231
|
+
),
|
|
232
|
+
).toBe(true);
|
|
233
|
+
expect(
|
|
234
|
+
AIConfidenceSignal.shouldSendWorkspaceNotification(
|
|
235
|
+
signal("deterministic-floor", false, false),
|
|
236
|
+
),
|
|
237
|
+
).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("AIConfidenceSignal.shouldEnqueueInstrumentationTask", () => {
|
|
242
|
+
test("classification-failed fails toward doing nothing", () => {
|
|
243
|
+
expect(
|
|
244
|
+
AIConfidenceSignal.shouldEnqueueInstrumentationTask(
|
|
245
|
+
signal("classification-failed", false),
|
|
246
|
+
),
|
|
247
|
+
).toBe(false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("enqueues on a positive inconclusive verdict (floor or classifier)", () => {
|
|
251
|
+
expect(
|
|
252
|
+
AIConfidenceSignal.shouldEnqueueInstrumentationTask(
|
|
253
|
+
signal("deterministic-floor", false, false),
|
|
254
|
+
),
|
|
255
|
+
).toBe(true);
|
|
256
|
+
expect(
|
|
257
|
+
AIConfidenceSignal.shouldEnqueueInstrumentationTask(
|
|
258
|
+
signal("classification", false, false),
|
|
259
|
+
),
|
|
260
|
+
).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("does not enqueue when the run was confident", () => {
|
|
264
|
+
expect(
|
|
265
|
+
AIConfidenceSignal.shouldEnqueueInstrumentationTask(
|
|
266
|
+
signal("classification", true, true),
|
|
267
|
+
),
|
|
268
|
+
).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("AIConfidenceSignal.isCodeFixRecommended / shouldAutoEnqueueCodeFixTask", () => {
|
|
273
|
+
test("requires classification source, confident, AND codeFixRecommended", () => {
|
|
274
|
+
const positive: ConfidenceSignal = signal("classification", true, true);
|
|
275
|
+
expect(AIConfidenceSignal.isCodeFixRecommended(positive)).toBe(true);
|
|
276
|
+
expect(AIConfidenceSignal.shouldAutoEnqueueCodeFixTask(positive)).toBe(
|
|
277
|
+
true,
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("NO_CODE_FIX (confident but not recommended) is insufficient", () => {
|
|
282
|
+
const noFix: ConfidenceSignal = signal("classification", true, false);
|
|
283
|
+
expect(AIConfidenceSignal.isCodeFixRecommended(noFix)).toBe(false);
|
|
284
|
+
expect(AIConfidenceSignal.shouldAutoEnqueueCodeFixTask(noFix)).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("the deterministic floor never triggers an auto code fix", () => {
|
|
288
|
+
// Even if some caller mislabeled the floor as recommended, source gates it.
|
|
289
|
+
const floor: ConfidenceSignal = signal("deterministic-floor", false, true);
|
|
290
|
+
expect(AIConfidenceSignal.isCodeFixRecommended(floor)).toBe(false);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("classification-failed never triggers an auto code fix", () => {
|
|
294
|
+
const failed: ConfidenceSignal = signal(
|
|
295
|
+
"classification-failed",
|
|
296
|
+
true,
|
|
297
|
+
true,
|
|
298
|
+
);
|
|
299
|
+
expect(AIConfidenceSignal.isCodeFixRecommended(failed)).toBe(false);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("an absent (legacy) codeFixRecommended fails closed", () => {
|
|
303
|
+
const legacy: ConfidenceSignal = {
|
|
304
|
+
source: "classification",
|
|
305
|
+
confident: true,
|
|
306
|
+
};
|
|
307
|
+
expect(AIConfidenceSignal.isCodeFixRecommended(legacy)).toBe(false);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe("AIConfidenceSignal decision table (cross-consumer)", () => {
|
|
312
|
+
interface Row {
|
|
313
|
+
signal: ConfidenceSignal;
|
|
314
|
+
ping: boolean;
|
|
315
|
+
instrumentation: boolean;
|
|
316
|
+
autoCodeFix: boolean;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const rows: Array<Row> = [
|
|
320
|
+
{
|
|
321
|
+
signal: signal("deterministic-floor", false, false),
|
|
322
|
+
ping: false,
|
|
323
|
+
instrumentation: true,
|
|
324
|
+
autoCodeFix: false,
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
signal: signal("classification", true, true),
|
|
328
|
+
ping: true,
|
|
329
|
+
instrumentation: false,
|
|
330
|
+
autoCodeFix: true,
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
signal: signal("classification", true, false),
|
|
334
|
+
ping: true,
|
|
335
|
+
instrumentation: false,
|
|
336
|
+
autoCodeFix: false,
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
signal: signal("classification", false, false),
|
|
340
|
+
ping: false,
|
|
341
|
+
instrumentation: true,
|
|
342
|
+
autoCodeFix: false,
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
signal: signal("classification-failed", false),
|
|
346
|
+
ping: true,
|
|
347
|
+
instrumentation: false,
|
|
348
|
+
autoCodeFix: false,
|
|
349
|
+
},
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
test("every documented row maps to the three consumer decisions", () => {
|
|
353
|
+
for (const row of rows) {
|
|
354
|
+
const context: ConfidenceSource = row.signal.source;
|
|
355
|
+
|
|
356
|
+
expect({
|
|
357
|
+
context,
|
|
358
|
+
ping: AIConfidenceSignal.shouldSendWorkspaceNotification(row.signal),
|
|
359
|
+
instrumentation: AIConfidenceSignal.shouldEnqueueInstrumentationTask(
|
|
360
|
+
row.signal,
|
|
361
|
+
),
|
|
362
|
+
autoCodeFix: AIConfidenceSignal.shouldAutoEnqueueCodeFixTask(
|
|
363
|
+
row.signal,
|
|
364
|
+
),
|
|
365
|
+
}).toEqual({
|
|
366
|
+
context,
|
|
367
|
+
ping: row.ping,
|
|
368
|
+
instrumentation: row.instrumentation,
|
|
369
|
+
autoCodeFix: row.autoCodeFix,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Keep the imported token type referenced for readers of this spec.
|
|
376
|
+
const _tokenType: ConfidenceClassificationToken = "CODE_FIX";
|
|
377
|
+
void _tokenType;
|
|
@@ -41,6 +41,15 @@ import { getJestSpyOn } from "../../../Spy";
|
|
|
41
41
|
* key full control of the project; one that dropped it from the master path
|
|
42
42
|
* would break trusted internal automation. Both directions are pinned below.
|
|
43
43
|
*
|
|
44
|
+
* AuthenticatedRequest sits in the same list but is not a capability in that
|
|
45
|
+
* sense. It is the marker for "some authenticated principal made this call" -
|
|
46
|
+
* it has no PermissionProps, never appears in the admin picker, and no
|
|
47
|
+
* administrator can grant or revoke it (see PermissionCatalogueCoverage.test).
|
|
48
|
+
* Only File/FileModel name it, so what it widens is bounded by those two
|
|
49
|
+
* models. It is here so an API key is not mistaken for a bare logged-in user
|
|
50
|
+
* by TenantPermission.isAccessGrantedOnlyByCurrentUser - see
|
|
51
|
+
* ApiKeyFileAccess.test for the behaviour that depends on it.
|
|
52
|
+
*
|
|
44
53
|
* getApiTenantAccessPermission also reshapes the (cached) permission rows into
|
|
45
54
|
* the runtime UserPermission shape (labelIds carried over, block flag
|
|
46
55
|
* preserved). That mapping is where a dropped label would quietly widen a
|
|
@@ -62,7 +71,7 @@ const permissionValues: (
|
|
|
62
71
|
};
|
|
63
72
|
|
|
64
73
|
describe("APIKeyAccessPermission.getDefaultApiGlobalPermission", () => {
|
|
65
|
-
it("grants Public, User and
|
|
74
|
+
it("grants Public, User, CurrentUser and AuthenticatedRequest for the project", async () => {
|
|
66
75
|
const perm: UserGlobalAccessPermission =
|
|
67
76
|
await APIKeyAccessPermission.getDefaultApiGlobalPermission(projectId);
|
|
68
77
|
|
|
@@ -72,10 +81,16 @@ describe("APIKeyAccessPermission.getDefaultApiGlobalPermission", () => {
|
|
|
72
81
|
return p.toString();
|
|
73
82
|
}),
|
|
74
83
|
).toEqual([projectId.toString()]);
|
|
84
|
+
/*
|
|
85
|
+
* Exact equality rather than four toContain calls: this list IS the
|
|
86
|
+
* privilege boundary, so an addition has to be argued for here rather than
|
|
87
|
+
* slipping in unnoticed.
|
|
88
|
+
*/
|
|
75
89
|
expect(perm.globalPermissions).toEqual([
|
|
76
90
|
Permission.Public,
|
|
77
91
|
Permission.User,
|
|
78
92
|
Permission.CurrentUser,
|
|
93
|
+
Permission.AuthenticatedRequest,
|
|
79
94
|
]);
|
|
80
95
|
});
|
|
81
96
|
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for status page certificate renewal scheduling.
|
|
3
|
+
*
|
|
4
|
+
* These pin the two properties whose absence let customer status pages serve
|
|
5
|
+
* expired certificates:
|
|
6
|
+
*
|
|
7
|
+
* - Renewal must be PROACTIVE. A run picks up a certificate weeks before it
|
|
8
|
+
* expires. Previously renewal ran once a day over every certificate inside a
|
|
9
|
+
* fixed window, strictly one domain at a time; once the fleet outgrew a
|
|
10
|
+
* single run the queue stopped draining, and certificates were reordered only
|
|
11
|
+
* after they had already expired and the provisioning sweep noticed the page
|
|
12
|
+
* was serving a dead certificate.
|
|
13
|
+
*
|
|
14
|
+
* - Renewal must be SPREAD OUT. Let's Encrypt issues for 90 days, so domains
|
|
15
|
+
* ordered together expire together. With one fixed lead time that batch stays
|
|
16
|
+
* together forever and one bad run expires all of it at once. The lead time
|
|
17
|
+
* is jittered per domain so batches disperse.
|
|
18
|
+
*
|
|
19
|
+
* A run is also capped, because the CA rate-limits new orders per account and
|
|
20
|
+
* the reactive sweep spends from the same allowance.
|
|
21
|
+
*
|
|
22
|
+
* AcmeCertificateService.findBy is spied on, so no database is touched, and
|
|
23
|
+
* orderCert is spied on so no ACME order is ever attempted.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import GreenlockUtil from "../../../../Server/Utils/Greenlock/Greenlock";
|
|
27
|
+
import AcmeCertificateService from "../../../../Server/Services/AcmeCertificateService";
|
|
28
|
+
import AcmeCertificate from "../../../../Models/DatabaseModels/AcmeCertificate";
|
|
29
|
+
import OneUptimeDate from "../../../../Types/Date";
|
|
30
|
+
import { afterEach, describe, expect, test, jest } from "@jest/globals";
|
|
31
|
+
|
|
32
|
+
function certificateExpiringInDays(
|
|
33
|
+
domain: string,
|
|
34
|
+
days: number,
|
|
35
|
+
): AcmeCertificate {
|
|
36
|
+
return {
|
|
37
|
+
domain: domain,
|
|
38
|
+
expiresAt: OneUptimeDate.addRemoveDays(
|
|
39
|
+
OneUptimeDate.getCurrentDate(),
|
|
40
|
+
days,
|
|
41
|
+
),
|
|
42
|
+
} as unknown as AcmeCertificate;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/*
|
|
46
|
+
* Drive a renewal run over a fixed set of certificates and report which domains
|
|
47
|
+
* it actually tried to order. The CNAME check is stubbed valid so the run
|
|
48
|
+
* reaches the order step for every domain it selects.
|
|
49
|
+
*/
|
|
50
|
+
async function domainsRenewedFor(
|
|
51
|
+
certificates: Array<AcmeCertificate>,
|
|
52
|
+
): Promise<Array<string>> {
|
|
53
|
+
const ordered: Array<string> = [];
|
|
54
|
+
|
|
55
|
+
jest
|
|
56
|
+
.spyOn(AcmeCertificateService, "findBy")
|
|
57
|
+
.mockResolvedValue(certificates as never);
|
|
58
|
+
|
|
59
|
+
jest
|
|
60
|
+
.spyOn(GreenlockUtil, "orderCert")
|
|
61
|
+
.mockImplementation(async (data: { domain: string }): Promise<void> => {
|
|
62
|
+
ordered.push(data.domain);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await GreenlockUtil.renewAllCertsWhichAreExpiringSoon({
|
|
66
|
+
validateCname: async (): Promise<boolean> => {
|
|
67
|
+
return true;
|
|
68
|
+
},
|
|
69
|
+
notifyDomainRemoved: async (): Promise<void> => {
|
|
70
|
+
return undefined;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return ordered;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe("Certificate renewal lead time", () => {
|
|
78
|
+
test("every domain renews well before it expires", () => {
|
|
79
|
+
const domains: Array<string> = [
|
|
80
|
+
"status.aleyant.com",
|
|
81
|
+
"status.example.com",
|
|
82
|
+
"a.status.syniti.com",
|
|
83
|
+
"b.status.avsw.io",
|
|
84
|
+
"x",
|
|
85
|
+
"",
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
for (const domain of domains) {
|
|
89
|
+
const leadTime: number = GreenlockUtil.getRenewalLeadTimeInDays(domain);
|
|
90
|
+
|
|
91
|
+
expect(leadTime).toBeGreaterThanOrEqual(
|
|
92
|
+
GreenlockUtil.RENEW_LEAD_TIME_MIN_IN_DAYS,
|
|
93
|
+
);
|
|
94
|
+
expect(leadTime).toBeLessThanOrEqual(
|
|
95
|
+
GreenlockUtil.RENEW_LEAD_TIME_MAX_IN_DAYS,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("the shortest possible lead time still leaves days of margin", () => {
|
|
101
|
+
/*
|
|
102
|
+
* The whole point is that renewal happens before expiry, not at it. Even
|
|
103
|
+
* the least eager domain must have room for several failed runs.
|
|
104
|
+
*/
|
|
105
|
+
expect(GreenlockUtil.RENEW_LEAD_TIME_MIN_IN_DAYS).toBeGreaterThanOrEqual(7);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("a domain's lead time is stable across calls", () => {
|
|
109
|
+
const first: number =
|
|
110
|
+
GreenlockUtil.getRenewalLeadTimeInDays("status.aleyant.com");
|
|
111
|
+
const second: number =
|
|
112
|
+
GreenlockUtil.getRenewalLeadTimeInDays("status.aleyant.com");
|
|
113
|
+
|
|
114
|
+
expect(first).toBe(second);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("domains expiring on the same day do not all come due on the same day", () => {
|
|
118
|
+
/*
|
|
119
|
+
* The thundering herd: a batch ordered together must not renew together.
|
|
120
|
+
*/
|
|
121
|
+
const leadTimes: Set<number> = new Set<number>();
|
|
122
|
+
|
|
123
|
+
for (let i: number = 0; i < 200; i++) {
|
|
124
|
+
leadTimes.add(
|
|
125
|
+
GreenlockUtil.getRenewalLeadTimeInDays(`status${i}.example.com`),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
expect(leadTimes.size).toBeGreaterThan(1);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("Certificate renewal run", () => {
|
|
134
|
+
afterEach(() => {
|
|
135
|
+
jest.restoreAllMocks();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("renews a certificate that is close to expiry", async () => {
|
|
139
|
+
const domain: string = "status.aleyant.com";
|
|
140
|
+
|
|
141
|
+
// one day left: due under any lead time in the range.
|
|
142
|
+
const renewed: Array<string> = await domainsRenewedFor([
|
|
143
|
+
certificateExpiringInDays(domain, 1),
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
expect(renewed).toEqual([domain]);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("renews before expiry rather than after it", async () => {
|
|
150
|
+
/*
|
|
151
|
+
* This is the regression. A certificate with three weeks of life left is
|
|
152
|
+
* inside every domain's lead time, so a run must pick it up now - not wait
|
|
153
|
+
* until it has expired and the page is already broken.
|
|
154
|
+
*/
|
|
155
|
+
const domain: string = "status.aleyant.com";
|
|
156
|
+
|
|
157
|
+
const renewed: Array<string> = await domainsRenewedFor([
|
|
158
|
+
certificateExpiringInDays(domain, 21),
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
expect(renewed).toEqual([domain]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("leaves a freshly issued certificate alone", async () => {
|
|
165
|
+
const renewed: Array<string> = await domainsRenewedFor([
|
|
166
|
+
certificateExpiringInDays("status.aleyant.com", 89),
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
expect(renewed).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("caps how many certificates one run orders", async () => {
|
|
173
|
+
/*
|
|
174
|
+
* A large backlog must not be dispatched as a single burst - the CA
|
|
175
|
+
* rate-limits new orders per account, and orders refused for rate limiting
|
|
176
|
+
* are renewals that did not happen.
|
|
177
|
+
*/
|
|
178
|
+
const certificates: Array<AcmeCertificate> = Array.from(
|
|
179
|
+
{ length: GreenlockUtil.RENEW_MAX_PER_RUN + 25 },
|
|
180
|
+
(_v: unknown, i: number) => {
|
|
181
|
+
return certificateExpiringInDays(`status${i}.example.com`, 1);
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const renewed: Array<string> = await domainsRenewedFor(certificates);
|
|
186
|
+
|
|
187
|
+
expect(renewed).toHaveLength(GreenlockUtil.RENEW_MAX_PER_RUN);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("spends a capped run on the certificates closest to expiring", async () => {
|
|
191
|
+
/*
|
|
192
|
+
* findBy returns rows sorted by expiry, and the cap is applied after the
|
|
193
|
+
* due filter - so the domains at the edge of expiry must be the ones a
|
|
194
|
+
* short run pays for, never the ones with weeks left.
|
|
195
|
+
*/
|
|
196
|
+
const urgent: Array<AcmeCertificate> = Array.from(
|
|
197
|
+
{ length: GreenlockUtil.RENEW_MAX_PER_RUN },
|
|
198
|
+
(_v: unknown, i: number) => {
|
|
199
|
+
return certificateExpiringInDays(`urgent${i}.example.com`, 1);
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const lessUrgent: Array<AcmeCertificate> = Array.from(
|
|
204
|
+
{ length: 10 },
|
|
205
|
+
(_v: unknown, i: number) => {
|
|
206
|
+
return certificateExpiringInDays(`later${i}.example.com`, 24);
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const renewed: Array<string> = await domainsRenewedFor([
|
|
211
|
+
...urgent,
|
|
212
|
+
...lessUrgent,
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
for (const domain of renewed) {
|
|
216
|
+
expect(domain.startsWith("urgent")).toBe(true);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("one domain that cannot be renewed does not abandon the rest of the run", async () => {
|
|
221
|
+
const ordered: Array<string> = [];
|
|
222
|
+
|
|
223
|
+
jest
|
|
224
|
+
.spyOn(AcmeCertificateService, "findBy")
|
|
225
|
+
.mockResolvedValue([
|
|
226
|
+
certificateExpiringInDays("broken.example.com", 1),
|
|
227
|
+
certificateExpiringInDays("healthy.example.com", 1),
|
|
228
|
+
] as never);
|
|
229
|
+
|
|
230
|
+
jest
|
|
231
|
+
.spyOn(GreenlockUtil, "orderCert")
|
|
232
|
+
.mockImplementation(async (data: { domain: string }): Promise<void> => {
|
|
233
|
+
if (data.domain === "broken.example.com") {
|
|
234
|
+
throw new Error("CA refused the order");
|
|
235
|
+
}
|
|
236
|
+
ordered.push(data.domain);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await GreenlockUtil.renewAllCertsWhichAreExpiringSoon({
|
|
240
|
+
validateCname: async (): Promise<boolean> => {
|
|
241
|
+
return true;
|
|
242
|
+
},
|
|
243
|
+
notifyDomainRemoved: async (): Promise<void> => {
|
|
244
|
+
return undefined;
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
expect(ordered).toContain("healthy.example.com");
|
|
249
|
+
});
|
|
250
|
+
});
|