From d6fdefa0ec38c238570840857ddee20a35b8a528 Mon Sep 17 00:00:00 2001 From: Brian Hill Date: Mon, 21 Sep 2026 21:18:14 -0400 Subject: [PATCH] Attach flow summary to EnrollmentContext on success, not just StatusMessage Command's enrollment UI doesn't surface StatusMessage on a successful/pending result at all - only EnrollmentContext is shown, so the flow summary added for failures was invisible on the success path. Add AttachFlowSummary (following the same fix already proven on feature/ev-ov-dv-multiname-certs) to also attach a "Flow Summary" entry to EnrollmentContext for success/pending results, alongside whatever DCV instructions came back, and use it for the FAILED results returned from GetEnrollmentResult/GetRenewResponse/ GetReIssueResult, which the earlier Status=30 fix didn't cover since they're already FAILED coming out of RequestManager. Called after TryPublishCnameDcvAsync in the New enrollment path so the "Flow Summary" entry is never present yet when DNS auto-publish walks EnrollmentContext looking for real CNAME records to publish. --- .../CSCGlobalCAPluginTests.cs | 50 +++++++++++++++++++ cscglobal-caplugin/CSCGlobalCAPlugin.cs | 28 +++++++++++ 2 files changed, 78 insertions(+) diff --git a/cscglobal-caplugin.Tests/CSCGlobalCAPluginTests.cs b/cscglobal-caplugin.Tests/CSCGlobalCAPluginTests.cs index c3402e6..a062965 100644 --- a/cscglobal-caplugin.Tests/CSCGlobalCAPluginTests.cs +++ b/cscglobal-caplugin.Tests/CSCGlobalCAPluginTests.cs @@ -837,6 +837,56 @@ public async Task Enroll_New_Success_ReturnsExternalValidation() Assert.Equal((int)EndEntityStatus.EXTERNALVALIDATION, result.Status); Assert.Equal("uuid-new", result.CARequestID); + // Command's enrollment UI doesn't surface StatusMessage on a successful/pending result - + // only EnrollmentContext is - so the flow summary must be attached there instead. + Assert.NotNull(result.EnrollmentContext); + Assert.True(result.EnrollmentContext.ContainsKey("Flow Summary")); + Assert.Contains("Enroll-New", result.EnrollmentContext["Flow Summary"]); + } + + [Fact] + public async Task Enroll_New_SuccessWithDcvDetails_KeepsDcvEntriesAlongsideFlowSummary() + { + var mockClient = new Mock(); + mockClient.Setup(c => c.SubmitGetCustomFields()).ReturnsAsync(new List()); + mockClient.Setup(c => c.SubmitRegistrationAsync(It.IsAny())).ReturnsAsync(new RegistrationResponse + { + Result = new Result + { + CommonName = "dcv.example.com", + Status = new Status { Uuid = "uuid-dcv" }, + DcvDetails = new List + { + new DcvDetail { CName = new CName { Name = "_dnsauth.example.com", Value = "token" } } + } + } + }); + + var plugin = MakePlugin(mockClient); + var result = await plugin.Enroll("csr", "CN=test", new Dictionary(), ProductInfo(), + RequestFormat.PKCS10, EnrollmentType.New); + + Assert.Equal("token", result.EnrollmentContext["_dnsauth.example.com"]); + Assert.True(result.EnrollmentContext.ContainsKey("Flow Summary")); + } + + [Fact] + public async Task Enroll_New_RegistrationErrorFromCsc_PrependsFlowSummaryToStatusMessage() + { + var mockClient = new Mock(); + mockClient.Setup(c => c.SubmitGetCustomFields()).ReturnsAsync(new List()); + mockClient.Setup(c => c.SubmitRegistrationAsync(It.IsAny())).ReturnsAsync(new RegistrationResponse + { + RegistrationError = new RegistrationError { Description = "Open order in progress" } + }); + + var plugin = MakePlugin(mockClient); + var result = await plugin.Enroll("csr", "CN=test", new Dictionary(), ProductInfo(), + RequestFormat.PKCS10, EnrollmentType.New); + + Assert.Equal((int)EndEntityStatus.FAILED, result.Status); + Assert.Contains("Enroll-New", result.StatusMessage); + Assert.Contains("Open order in progress", result.StatusMessage); } [Fact] diff --git a/cscglobal-caplugin/CSCGlobalCAPlugin.cs b/cscglobal-caplugin/CSCGlobalCAPlugin.cs index 457ef42..c84f909 100644 --- a/cscglobal-caplugin/CSCGlobalCAPlugin.cs +++ b/cscglobal-caplugin/CSCGlobalCAPlugin.cs @@ -711,10 +711,12 @@ await flow.StepAsync("PollForIssuance", async () => if (newPolled != null) { flow.Step("PollResult", "issued during poll window"); + AttachFlowSummary(newPolled, flow); Logger.MethodExit(LogLevel.Debug); return newPolled; } + AttachFlowSummary(enrollResult, flow); Logger.MethodExit(LogLevel.Debug); return enrollResult; @@ -864,6 +866,7 @@ await flow.StepAsync("PollForIssuance", async () => { renewPolled = await TryPollForIssuedCertAsync(renewResult?.CARequestID); }); + AttachFlowSummary(renewPolled ?? renewResult, flow); Logger.MethodExit(LogLevel.Debug); return renewPolled ?? renewResult; } @@ -943,6 +946,7 @@ await flow.StepAsync("PollForIssuance", async () => { reissuePolled = await TryPollForIssuedCertAsync(reissueResult?.CARequestID); }); + AttachFlowSummary(reissuePolled ?? reissueResult, flow); Logger.MethodExit(LogLevel.Debug); return reissuePolled ?? reissueResult; } @@ -987,6 +991,30 @@ await flow.StepAsync("PollForIssuance", async () => } } + // CSC Global business-level failures (e.g. "Open order in progress") come back from + // RequestManager as a terse StatusMessage with no context on what the plugin actually did + // before hitting that error - prepend the flow's step-by-step summary so the message shown + // to the requester in Command explains what ran, not just how it ended. Command's enrollment + // UI does not surface StatusMessage on a successful/pending result at all - only + // EnrollmentContext is - so attach the summary there instead, as its own entry alongside + // whatever DCV instructions came back. Must be called after TryPublishCnameDcvAsync, which + // treats every EnrollmentContext entry as a candidate DNS record to publish - calling this + // first would make it try to publish "Flow Summary" as a CNAME. + private static void AttachFlowSummary(EnrollmentResult? result, FlowLogger flow) + { + if (result == null) + return; + + if (result.Status == (int)EndEntityStatus.FAILED) + { + result.StatusMessage = $"{flow.GetSummary()}\n\n{result.StatusMessage}"; + return; + } + + result.EnrollmentContext ??= new Dictionary(); + result.EnrollmentContext["Flow Summary"] = flow.GetSummary(); + } + //done public async Task Ping() {