Skip to content

Commit 797f666

Browse files
committed
test(integration): live "DCV on" verification mirroring the DCV-off test
Adds EnrollWithDcvOn_OrderIssuedEndToEnd_AndAppearsInSync — the symmetric counterpart to EnrollWithDcvOff_OrderAppearsInSync_PluginDidNotInvokeDcv. Drives a fresh enrollment with DCV ON end-to-end through the plugin against the live sandbox. Flow: 1. Build the plugin with DcvEnabled=true and the real Cloudflare factory. 2. Enroll a fresh randomized scrup.org subdomain. 3. Run plugin.Synchronize, find the order, assert its status is GENERATED or EXTERNALVALIDATION (never FAILED). 4. If GENERATED, assert the cert PEM is non-empty. Verified live: enroll completed in ~9s, sync took ~2:35 (driving the sync-DCV-retry across all pending orders in the account), the new order surfaced at Status=90 in sync, and a follow-up plugin.GetSingleRecord call against the same CARequestID returned Status=40 GENERATED with a populated cert PEM. End-to-end DCV path works.
1 parent 7ce9971 commit 797f666

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

CERTInext.IntegrationTests/DcvLifecycleTests.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,82 @@ public async Task EnrollWithDcvOff_OrderAppearsInSync_PluginDidNotInvokeDcv()
282282
$"order {enrollResult.CARequestID} surfaced in sync with Status={record.Status}. ---");
283283
}
284284

285+
/// <summary>
286+
/// Symmetric counterpart to <see cref="EnrollWithDcvOff_OrderAppearsInSync_PluginDidNotInvokeDcv"/>.
287+
/// Drives a fresh enrollment with DCV ON end-to-end against the live sandbox and
288+
/// asserts the issued cert flows through Synchronize. This is the v3.3+
289+
/// production scenario — plugin places the order, runs DNS TXT staging via
290+
/// Cloudflare, asks CERTInext to verify, waits for issuance, and the resulting
291+
/// GENERATED record surfaces in the gateway's inventory.
292+
/// </summary>
293+
[SkippableFact]
294+
public async Task EnrollWithDcvOn_OrderIssuedEndToEnd_AndAppearsInSync()
295+
{
296+
IntegrationSkip.IfNotConfigured(_fixture);
297+
Skip.If(!_fixture.IsCloudflareConfigured,
298+
"CERTINEXT_CF_API_TOKEN + CERTINEXT_CF_ZONE_ID required — DCV-on test must publish real TXT records.");
299+
300+
string suffix = System.Guid.NewGuid().ToString("N").Substring(0, 8);
301+
string cn = $"dcv-on-{suffix}.scrup.org";
302+
303+
var plugin = BuildPlugin(dcvEnabled: true);
304+
305+
// --- Enroll phase ---
306+
var enrollSw = System.Diagnostics.Stopwatch.StartNew();
307+
var enrollResult = await plugin.Enroll(
308+
csr: GenerateCsrPem(cn),
309+
subject: $"CN={cn}",
310+
san: new Dictionary<string, string[]> { ["dns"] = new[] { cn } },
311+
productInfo: IntegrationTestData.DvSslProductInfo(_fixture.Config.DefaultProductCode),
312+
requestFormat: RequestFormat.PKCS10,
313+
enrollmentType: EnrollmentType.New);
314+
enrollSw.Stop();
315+
316+
enrollResult.Should().NotBeNull();
317+
enrollResult.CARequestID.Should().NotBeNullOrWhiteSpace();
318+
_output.WriteLine($"Enroll completed in {enrollSw.Elapsed:mm\\:ss\\.fff}");
319+
_output.WriteLine($" CARequestID: {enrollResult.CARequestID}");
320+
_output.WriteLine($" Status: {enrollResult.Status}");
321+
_output.WriteLine($" Certificate: {(string.IsNullOrWhiteSpace(enrollResult.Certificate) ? "(not in Enroll response)" : enrollResult.Certificate[..60] + "...")}");
322+
323+
// Enroll must NOT be FAILED. GENERATED if the bounded issuance wait caught
324+
// the cert before returning; EXTERNALVALIDATION if not — sync will catch it.
325+
new[] { (int)EndEntityStatus.EXTERNALVALIDATION, (int)EndEntityStatus.GENERATED }
326+
.Should().Contain(enrollResult.Status,
327+
$"DCV-on Enroll must return pending or issued; got {enrollResult.Status}");
328+
329+
// --- Sync phase ---
330+
var syncSw = System.Diagnostics.Stopwatch.StartNew();
331+
var synced = await RunSyncAsync(plugin);
332+
syncSw.Stop();
333+
_output.WriteLine($"Synchronize returned {synced.Count} records in {syncSw.Elapsed:mm\\:ss\\.fff}");
334+
335+
var record = synced.FirstOrDefault(r => r.CARequestID == enrollResult.CARequestID);
336+
record.Should().NotBeNull(
337+
$"the enrolled order ({enrollResult.CARequestID}) must appear in plugin.Synchronize results");
338+
_output.WriteLine($" Sync record status: {record!.Status}");
339+
_output.WriteLine($" Cert PEM length: {(record.Certificate?.Length ?? 0)}");
340+
341+
// The plugin's sync-DCV-retry should have advanced any still-pending orders.
342+
// With Cloudflare DCV available, every DCV-on enrollment should resolve to
343+
// GENERATED by the time sync returns. If we see EXTERNALVALIDATION here it
344+
// means CERTInext's async issuance window is still in flight after our sync —
345+
// worth noting but not a hard failure (the next sync will pick it up).
346+
record.Status.Should().BeOneOf((int)EndEntityStatus.GENERATED, (int)EndEntityStatus.EXTERNALVALIDATION);
347+
348+
// Hard signal we'll always demand: when sync returns GENERATED, there must
349+
// be a non-empty PEM. A GENERATED record with no Certificate would mean the
350+
// plugin's download path is broken.
351+
if (record.Status == (int)EndEntityStatus.GENERATED)
352+
{
353+
record.Certificate.Should().NotBeNullOrWhiteSpace(
354+
"GENERATED records must carry the issued PEM in their Certificate field");
355+
}
356+
357+
_output.WriteLine($"--- Verdict: DCV-on enroll for {cn} drove DCV end-to-end via plugin, " +
358+
$"order {enrollResult.CARequestID} surfaced in sync with Status={record.Status}. ---");
359+
}
360+
285361
/// <summary>
286362
/// Exercises the deferred-DCV retry path during single-record refresh against an
287363
/// existing pending order. Reads <c>CERTINEXT_PENDING_ORDER_ID</c> from the

0 commit comments

Comments
 (0)