Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
167 commits
Select commit Hold shift + click to select a range
3e3d581
feat(enroll): synchronous certificate pickup (Sectigo parity) — v1.0.1
spbsoluble Jul 31, 2026
77fe123
chore(enroll): log RequestFormat on the enrollment-start line
spbsoluble Jul 31, 2026
49616cc
fix(client): don't retry non-idempotent order/CSR submits on a networ…
spbsoluble Jul 31, 2026
e8e4739
fix(client): enrich orphaned-order warnings + SubmitCSR transient gui…
spbsoluble Jul 31, 2026
6ae12b7
fix(enroll): harden synchronous pickup — DCV gating, wait ceiling, au…
spbsoluble Jul 31, 2026
f499f65
fix(enroll): UCC SANs never reached CERTInext — additionalDomains sen…
spbsoluble Aug 13, 2026
947ae68
docs: refresh docsource against current code
spbsoluble Aug 13, 2026
5d2d154
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Aug 13, 2026
a890a7d
fix(enroll): renewal product code, AutoApprove UI text, config log vi…
spbsoluble Aug 13, 2026
00ccdbd
docs(changelog): add PR #28's renewal product-code, AutoApprove, and …
spbsoluble Aug 13, 2026
59aa2b2
fix(logging): add trace-level payload dumps for enrollment request/re…
spbsoluble Sep 14, 2026
1447a5c
feat(enroll): port ValidityYears from release-1.1 (PR #22)
spbsoluble Sep 14, 2026
be44ef7
feat(v2): add CERTInext V2 REST API support behind UseV2Api flag
spbsoluble Sep 21, 2026
f0eb37e
test(v2): fix V2 integration test env var names and assertions
spbsoluble Sep 21, 2026
8992fcb
feat(v2): add V2 DCV client methods, chainPem, issuedAt/expiresAt, op…
spbsoluble Sep 22, 2026
a4e03a4
docs(architecture): add V2 enrollment flow diagrams and update V1 pol…
spbsoluble Sep 22, 2026
2d3a833
test(v2): add integration tests for GetProductDetails, GetSingleRecor…
spbsoluble Sep 22, 2026
db6f33e
fix: address V2 code-review findings (resource leaks, DCV family slug…
spbsoluble Sep 22, 2026
70b83b2
fix: resource leak, V2 audit-trail gap, and trace-level authKey exposure
spbsoluble Sep 22, 2026
c870895
fix: RevokeOrderV2Async 404→KeyNotFoundException and ValidateProductI…
spbsoluble Sep 22, 2026
0d149d9
fix: V2 enrollment never submitted CSR; CRLF log injection in Request…
spbsoluble Sep 22, 2026
a030192
fix(enroll): add DelegationInformation/TechnicalPointOfContact to Ren…
spbsoluble Sep 22, 2026
35fac40
fix(enroll): re-throw OperationCanceledException in PickUpEnrolledCer…
spbsoluble Sep 22, 2026
8005ce9
fix(dcv): decouple post-verify poll cadence from DcvPropagationDelayS…
spbsoluble Sep 22, 2026
393efc2
fix(dcv): add _dcvInFlight guard to PerformDcvV2IfNeededAsync
spbsoluble Sep 22, 2026
90805f1
fix(enroll): guard TrackOrderV2Async post-CSR-submission against tran…
spbsoluble Sep 22, 2026
8c79d54
fix(enroll): reject multi-SAN V2 enrollments with clear FAILED result
spbsoluble Sep 22, 2026
be135ed
test(v2): fix OAuth URL priority and wire lifecycle order ID to revok…
spbsoluble Sep 23, 2026
bc6f33d
test(v2): gate CERTINEXT_V2_RUN_BULK_TEST as opt-in, exclude V2DcvLif…
spbsoluble Sep 23, 2026
c83e0d0
test(v2): close catalog/products and TrackOrder structural coverage gaps
spbsoluble Sep 23, 2026
2edeb08
test(v2): add plugin-level V2 lifecycle test coverage (gaps 1-4, 9-11)
spbsoluble Sep 23, 2026
5ca26a5
test(v2): add plugin-level V2 DCV lifecycle coverage (gaps 5-8, 14-15)
spbsoluble Sep 23, 2026
6923895
test(v2): add opt-in /reports/orders and /domains live probes (Phase 0)
spbsoluble Sep 23, 2026
d603701
test(v2): make V2 integration tests honest (G5-G7, narrowed revoke ca…
spbsoluble Sep 23, 2026
e624be7
fix(v2): correct OAuth 401/403 hints, surface RFC 7807 field errors; …
spbsoluble Sep 23, 2026
5650937
fix(v2): kebab-case revoke reasons, single-family revoke with clear 4…
spbsoluble Sep 23, 2026
c2f3f3f
feat(v2): Synchronize via V2 /reports/orders, consolidate V2 config o…
spbsoluble Sep 24, 2026
c370d8e
test(v2): add read-only catalog/products shape probe (issue 0025 step 0)
spbsoluble Sep 24, 2026
4fdc58e
fix(client): flatten nested category envelope in V2 catalog parser (0…
spbsoluble Sep 24, 2026
f990638
fix(config): validate template ProductCode against V2 catalog (0025)
spbsoluble Sep 24, 2026
7a1d0e0
test(v2): live integration coverage for ValidateProductInfo (0025)
spbsoluble Sep 24, 2026
fd72ebf
docs: update TESTING.md and CHANGELOG for issue 0025/0016 fix
spbsoluble Sep 24, 2026
727064e
chore(docs): add CERTInext V1/V2 API Postman spec references
spbsoluble Sep 25, 2026
a4211c1
docs: document DcvSupport build flag and net10.0-only targeting
spbsoluble Sep 25, 2026
1ca3dcc
chore: stop tracking issues/ working log and local tooling config
spbsoluble Sep 25, 2026
70d21df
test(diagnostics): add kfclab field-probe and pending-DV diagnostic u…
spbsoluble Sep 25, 2026
b67e95e
fix(v2): correct status mapping, add UCC support, send org block, ret…
spbsoluble Sep 25, 2026
4b345f2
test(v2): regression coverage for 0031 status mapping
spbsoluble Sep 25, 2026
9c3c173
test(v2): regression coverage for multi-SAN UCC support (F3)
spbsoluble Sep 25, 2026
b3ca05f
test(v2): regression coverage for OV/EV organization block (0028)
spbsoluble Sep 25, 2026
d8c8906
docs: regenerate README and update migration guide for 0026, 0031, F3…
spbsoluble Sep 25, 2026
4153229
fix(v2): read the real DCV token field instead of the never-populated…
spbsoluble Sep 25, 2026
12a4b61
test(v2): regression coverage for live DCV response shape (0037)
spbsoluble Sep 25, 2026
9b4cbf0
fix(v2): resolve V2 product code from the live catalog by productType…
spbsoluble Sep 25, 2026
762ba67
test(v2): regression coverage for productTypeID-based product code re…
spbsoluble Sep 25, 2026
c32122b
fix(v2): send GroupNumber on V2 order create, catalog, and orders-rep…
spbsoluble Sep 25, 2026
3cce011
test(v2): regression coverage for GroupNumber on V2 order/catalog/rep…
spbsoluble Sep 25, 2026
cf741a8
fix(v2): send technicalPointOfContact on V2 SSL order create (0030)
spbsoluble Sep 25, 2026
69d697f
test(v2): regression coverage for technicalPointOfContact on V2 SSL o…
spbsoluble Sep 25, 2026
a06cff4
docs(v2): correct overstated Idempotency-Key comments (0032)
spbsoluble Sep 25, 2026
8a51bd0
test(v2): live probe for CERTInext idempotency-key dedup behavior (0032)
spbsoluble Sep 25, 2026
5745ada
fix(v2): populate RevocationDate/RevocationReason from V2's nested re…
spbsoluble Sep 25, 2026
711570b
test(v2): regression coverage for the nested V2 revocation DTO shape …
spbsoluble Sep 25, 2026
d7aaa51
test(v2): live probe for V2 revocation date/reason wire shape (0034)
spbsoluble Sep 25, 2026
4e71ce8
fix(sync): prefer report row's ProductCode, fall back to Track Order'…
spbsoluble Sep 26, 2026
c51bce7
test(v2): regression coverage for Synchronize's ProductID preference …
spbsoluble Sep 26, 2026
0235add
fix(v2): honor IgnoreExpired, DcvTxtRecordTemplate, and ISD-code comp…
spbsoluble Sep 26, 2026
e791cc6
test(v2): regression coverage for IgnoreExpired, DcvTxtRecordTemplate…
spbsoluble Sep 26, 2026
4355b3c
test(v2): live probe for V2 emailNotifications value vocabulary (0027)
spbsoluble Sep 26, 2026
4d6fc93
fix(v2): honor SubscriptionAutoRenew and SubscriptionRenewCriteriaDay…
spbsoluble Sep 28, 2026
80fd05c
test(v2): regression coverage for V2 Subscription AutoRenew/RenewBefo…
spbsoluble Sep 28, 2026
afd3cab
fix(v2): add RequestorDesignation config field, omit requestor.design…
spbsoluble Sep 28, 2026
2d0ce33
test(v2): regression coverage for RequestorDesignation on V1 and V2 o…
spbsoluble Sep 28, 2026
793534a
test(v2): real-inbox probe for V2 emailNotifications and requestor.de…
spbsoluble Sep 28, 2026
95458fd
fix(v2): honor EmailNotifications config in V2 order create instead o…
spbsoluble Sep 28, 2026
1cd6567
test(v2): regression coverage for V2 EmailNotifications mapping (0027)
spbsoluble Sep 28, 2026
9e0fe18
test(lab): opt-in BouncyCastle CSR emitter for Command-driven lab enr…
spbsoluble Sep 28, 2026
28b023f
test(v2): opt-in explicit-order superseded revoke for lab cleanup and…
spbsoluble Sep 28, 2026
51874aa
fix(audit): extract leaf serial from PEM chain, not whole blob (0050)
spbsoluble Sep 28, 2026
eee3c35
test(audit): regression coverage for leaf serial extraction from PEM …
spbsoluble Sep 28, 2026
1ffecb5
fix(v2): exempt UCC products from the V2 single-domain CSR-SAN guard …
spbsoluble Sep 28, 2026
f6e98ea
test(v2): regression coverage for UCC exemption from the CSR-SAN guar…
spbsoluble Sep 28, 2026
3ca2812
docs(changelog): note V2 audit serial fix (0050)
spbsoluble Sep 28, 2026
2716d25
fix(v2): never emit body-less REVOKED for certs the gateway doesn't h…
spbsoluble Sep 28, 2026
3726c9c
test(v2): regression coverage for the bodyless-REVOKED guard (0049)
spbsoluble Sep 28, 2026
6523a8b
test(v2): opt-in read-only UCC DCV shape probe (0042)
spbsoluble Sep 28, 2026
1e6a38b
test(v2): frame RenewOrReissue new-order test as intentional (0021)
spbsoluble Sep 28, 2026
18e05cc
docs(v2): renewal/reissue places a new order by design (0021, 0038)
spbsoluble Sep 28, 2026
6fc7b31
fix(v2): add synchronous certificate-pickup poll to V2 enrollment (0051)
spbsoluble Sep 29, 2026
b41a8b9
test(v2): regression coverage for the V2 pickup poll (0051)
spbsoluble Sep 29, 2026
eedfe2e
test(v2): live-probe support for issue 0042's pending-SAN DCV wire shape
spbsoluble Sep 29, 2026
02cf703
fix(v2): drive DCV for every SAN on a UCC order, not just the primary…
spbsoluble Sep 29, 2026
e8ed868
test(v2): cover the multi-domain V2 DCV path for UCC orders (0042)
spbsoluble Sep 29, 2026
a3c16ed
fix(v2): map a body-less REVOKED disposition to FAILED in V2 Enroll
spbsoluble Sep 29, 2026
59b83f7
test(v2): regression coverage for the V2 Enroll REVOKED->FAILED fix (…
spbsoluble Sep 29, 2026
4029026
fix(logging): redact requestor PII and CA payloads from gateway logs …
spbsoluble Sep 29, 2026
6c12174
test(logging): regression coverage for LogSensitiveRequestData (0040)
spbsoluble Sep 29, 2026
1baf1df
fix(logging): redact V2 subscriberAgreement.signedPlace in order resp…
spbsoluble Sep 29, 2026
121a425
test(logging): cover V2 order-response subscriberAgreement/orderedBy …
spbsoluble Sep 29, 2026
7bac264
docs(changelog): note V2 pickup poll (0051) and UCC per-SAN DCV (0042)
spbsoluble Sep 29, 2026
4c0eafa
chore(gitignore): anchor logs pattern to root dirs so Log*.cs sources…
spbsoluble Sep 29, 2026
88845bf
fix(v2): send family-specific create-order bodies for private-pki and…
spbsoluble Sep 29, 2026
4fb05df
test(v2): regression coverage for family-specific V2 create-order bod…
spbsoluble Sep 29, 2026
4b503ed
fix(client): include HTTP status and log redacted body for V1 non-2xx…
spbsoluble Sep 29, 2026
ec60e7e
test(client): regression coverage for V1 non-2xx error body handling …
spbsoluble Sep 29, 2026
895b9ce
docs(changelog): note V1 non-2xx error status/body logging (0044)
spbsoluble Sep 29, 2026
342f514
fix(v2): log UCC non-DNS SAN exclusion accurately instead of V1 wordi…
spbsoluble Sep 29, 2026
c1e0eec
test(v2): regression coverage for V2 UCC non-DNS SAN log wording (0046)
spbsoluble Sep 29, 2026
96a3cef
docs(v2): correct stale V2 migration, architecture, and config claims…
spbsoluble Sep 29, 2026
77f2ac8
docs(changelog): drop stale ord_ order-ID and revoke-probing claims (…
spbsoluble Sep 29, 2026
42af822
test(v2): let the V2 cancel probe target any product family (CERTINEX…
spbsoluble Sep 29, 2026
1d47088
fix(tests): stop V2 env loading from corrupting the V1 fixture ApiUrl…
spbsoluble Sep 29, 2026
77c5e8a
test(tests): offline regression coverage for the V1 fixture ApiUrl gu…
spbsoluble Sep 29, 2026
a4f9822
test(v2): opt-in live private-pki enroll+revoke integration test (0033)
spbsoluble Sep 29, 2026
83968ee
test(v2): never let the env file arm the order-placing private-pki li…
spbsoluble Sep 29, 2026
416094e
test(v2): read-only custom-fields and ledger probes for issue 0039 tr…
spbsoluble Sep 29, 2026
515eb40
fix(logging): mask email SAN values in log lines unless LogSensitiveR…
spbsoluble Sep 29, 2026
55e6673
test(logging): cover email SAN masking in log helpers and enrollment …
spbsoluble Sep 29, 2026
8d22dee
chore(scripts): move scripts/v2 helpers to CERTINEXT_API_URL + OAuth2…
spbsoluble Sep 29, 2026
6da53fa
fix(logging): mask email SANs in SAN arrays and V1 domainVerification…
spbsoluble Sep 29, 2026
e4f8bf1
test(logging): cover email masking in SAN arrays, domainVerification …
spbsoluble Sep 29, 2026
2378e8d
fix(v2): cancel the orphaned order once when Submit CSR fails, then r…
spbsoluble Sep 29, 2026
421d8d5
test(v2): cover orphaned-order cancel after Submit CSR failure and Ca…
spbsoluble Sep 29, 2026
cc3ad9b
test(v2): private-pki live test cancels a still-pending order in clea…
spbsoluble Sep 29, 2026
b0254bf
fix(config): require SignerPlace for V2 connectors and fail fast on b…
spbsoluble Sep 29, 2026
16b63cc
test(config): cover V2 SignerPlace requirement at validation and enro…
spbsoluble Sep 29, 2026
a995e35
fix(sync): map spec-documented V2 status 'unknown' to pending instead…
spbsoluble Sep 29, 2026
e09c502
test(sync): cover V2 'unknown' status mapping in the mapper, enroll a…
spbsoluble Sep 29, 2026
539a3a7
fix(v2): omit X-Product-Code header when product code is null or blan…
spbsoluble Sep 29, 2026
622190b
test(v2): cover X-Product-Code omission for null/blank product codes …
spbsoluble Sep 29, 2026
a9ddf67
fix(v2): reject SAN-dictionary-only multi-domain extras on non-UCC V2…
spbsoluble Sep 29, 2026
56e564f
test(v2): cover the non-UCC SAN-dictionary reject and its allowances …
spbsoluble Sep 29, 2026
f3d2a06
fix(v2): derive/validate SSL productVariant from the selected product…
spbsoluble Sep 29, 2026
f246532
test(v2): cover productVariant derivation/validation and update dv-de…
spbsoluble Sep 29, 2026
ce35e26
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Sep 29, 2026
de37de7
test(v2): add opt-in V2 gap probes P1-P5 (0058)
spbsoluble Oct 1, 2026
a5930c8
test(v2): sweep orphaned gap-probe orders; longer OV create timeout (…
spbsoluble Oct 1, 2026
420f057
test(v2): add opt-in V2 full-lifecycle coverage for DV UCC, OV, OV UC…
spbsoluble Oct 1, 2026
0858c7d
docs: changelog entry for new V2 full-lifecycle test coverage
spbsoluble Oct 1, 2026
91be5e6
fix(revoke): audit-log V2 revoke denials and retry outcomes
spbsoluble Oct 1, 2026
8eec683
fix(enroll): don't cancel a valid order on a transport-level CSR failure
spbsoluble Oct 1, 2026
c143643
chore(tests): fix license header whitespace in new test files
spbsoluble Oct 1, 2026
0f2268f
docs: fold revoke-audit and CSR-transport fixes into the 1.0.1 changelog
spbsoluble Oct 1, 2026
547d465
fix(tests): harden V2 full-lifecycle readiness suite assertions
spbsoluble Oct 1, 2026
a2992ea
test(v2): add opt-in orders-report window sweep for manual cleanup
spbsoluble Oct 1, 2026
4e2653a
fix(sync): map V2 order status 'expired' to GENERATED, not FAILED
spbsoluble Oct 1, 2026
a2c2422
fix(config): reject non-https ApiUrl except for loopback hosts
spbsoluble Oct 1, 2026
3d8e4c9
fix(revoke): generalize V2 revoke-reason retry beyond 'unspecified'
spbsoluble Oct 1, 2026
1c39518
fix(enroll): reject ambiguous V2 product catalog matches instead of f…
spbsoluble Oct 1, 2026
67ea2fb
fix(enroll): exempt wildcard apex from the V2 single-domain SAN guard
spbsoluble Oct 1, 2026
5a5b785
test(v2): set DefaultProductCode in lifecycle test configs so DV SSL …
spbsoluble Oct 1, 2026
40c5573
fix(crypto): derive DCV TXT hostname from the wildcard base domain
spbsoluble Oct 1, 2026
8183751
test(sync): target a genuinely unverified parent for V2 fresh-DCV tests
spbsoluble Oct 1, 2026
9166790
test(enroll): record SAN coverage for the wildcard+apex V2 test
spbsoluble Oct 1, 2026
596c53a
fix(dcv): key wildcard/apex TXT dedupe on hostname and token
spbsoluble Oct 1, 2026
142959b
test(dcv): skip V2 fresh-domain DCV tests when sandbox pre-validates …
spbsoluble Oct 1, 2026
e1b4fef
fix(config): close two https-enforcement gaps in ApiUrl/OAuthTokenUrl
spbsoluble Oct 1, 2026
5d95cc7
docs(v2): document product-code disambiguation, https requirement, re…
spbsoluble Oct 1, 2026
5b0d72b
docs(changelog): tighten 1.0.1 upgrade notes; cover token URL https a…
spbsoluble Oct 1, 2026
3c48d3d
docs: regenerate README via doctool [skip ci]
spbsoluble Oct 1, 2026
22eaca0
ci: trigger release-candidate build for 2.0.0
spbsoluble Oct 5, 2026
d87e677
build(v2): enable DcvSupport by default for 2.0.0
spbsoluble Oct 5, 2026
49d5bb3
build(v2): use IAnyCAPlugin 3.3.0 release instead of the prerelease
spbsoluble Oct 5, 2026
1878c8b
build(v2): require AnyCA Gateway framework 26.2.0
spbsoluble Oct 5, 2026
2a23a33
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Oct 5, 2026
b049506
feat(v2): DcvEnabled config defaults to true
spbsoluble Oct 5, 2026
0b6c2a8
docs: auto-generate README and documentation [skip ci]
github-actions[bot] Oct 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,10 @@ terraform/terraform.tfvars

# Analysis / scratch — never commit
analysis/

issues/
.claude/
.codex/
CLAUDE.md
AGENTS.md
/logs*/
*.log
17 changes: 10 additions & 7 deletions CERTInext.IntegrationTests/CERTInext.IntegrationTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,27 @@
<LangVersion>12.0</LangVersion>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<!-- Mirror the main project's DcvSupport flag. Default false → DCV test files are excluded
(matches the GA no-DCV build); -p:DcvSupport=true compiles them in with SUPPORTS_DCV. -->
<DcvSupport Condition="'$(DcvSupport)' == ''">false</DcvSupport>
<!-- Mirror the main project's DcvSupport flag. Default true → DCV test files are included
(matches the DCV-enabled default build); -p:DcvSupport=false excludes them. -->
<DcvSupport Condition="'$(DcvSupport)' == ''">true</DcvSupport>
<DefineConstants Condition="'$(DcvSupport)' == 'true'">$(DefineConstants);SUPPORTS_DCV</DefineConstants>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\CERTInext\CERTInext.csproj" />
</ItemGroup>

<!-- DCV integration tests + the DNS validator implementations use the v3.3-only
IDomainValidator / IDomainValidatorFactory and the factory constructor. On the
IAnyCAPlugin 3.2.0 (no-DCV) build those don't exist, so exclude these files unless
SUPPORTS_DCV is defined. See issue 0003. -->
<!-- DCV integration tests + the DNS validator implementations (real and recording/spy)
use the v3.3-only IDomainValidator / IDomainValidatorFactory and the factory
constructor. On the IAnyCAPlugin 3.2.0 (no-DCV) build those don't exist, so exclude
these files unless SUPPORTS_DCV is defined. See issue 0003. -->
<ItemGroup Condition="!$(DefineConstants.Contains('SUPPORTS_DCV'))">
<Compile Remove="DcvLifecycleTests.cs" />
<Compile Remove="V2DcvLifecycleTests.cs" />
<Compile Remove="V2FreshDomainDcvLifecycleTests.cs" />
<Compile Remove="CloudflareDomainValidator.cs" />
<Compile Remove="StubDomainValidator.cs" />
<Compile Remove="RecordingDomainValidator.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
10 changes: 7 additions & 3 deletions CERTInext.IntegrationTests/CloudflareDomainValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ namespace Keyfactor.Extensions.CAPlugin.CERTInext.IntegrationTests
/// Credentials are read from the <see cref="IntegrationTestFixture"/>:
/// <c>CERTINEXT_CF_API_TOKEN</c> and <c>CERTINEXT_CF_ZONE_ID</c>.
/// </summary>
internal sealed class CloudflareDomainValidator : IDomainValidator
internal sealed class CloudflareDomainValidator : IDomainValidator, IDisposable
{
private const string CfApiBase = "https://api.cloudflare.com/client/v4";

Expand Down Expand Up @@ -113,17 +113,21 @@ public async Task<DomainValidationResult> CleanupValidation(string key, Cancella
public Task ValidateConfiguration(Dictionary<string, object> configuration) => Task.CompletedTask;
public Dictionary<string, Keyfactor.AnyGateway.Extensions.PropertyConfigInfo> GetDomainValidatorAnnotations() => new();
public string GetValidationType() => "dns-01";

public void Dispose() => _http.Dispose();
}

internal sealed class CloudflareDomainValidatorFactory : IDomainValidatorFactory
internal sealed class CloudflareDomainValidatorFactory : IDomainValidatorFactory, IDisposable
{
private readonly IDomainValidator _validator;
private readonly CloudflareDomainValidator _validator;

public CloudflareDomainValidatorFactory(string apiToken, string zoneId)
{
_validator = new CloudflareDomainValidator(apiToken, zoneId);
}

public IDomainValidator ResolveDomainValidator(string domain, string validationType) => _validator;

public void Dispose() => _validator.Dispose();
}
}
58 changes: 45 additions & 13 deletions CERTInext.IntegrationTests/DcvLifecycleTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,25 @@ namespace Keyfactor.Extensions.CAPlugin.CERTInext.IntegrationTests
/// CERTINEXT_DCV_DOMAIN=&lt;subdomain to use, e.g. dcv-test.example.com&gt;
/// </code>
/// </summary>
public class DcvLifecycleTests : IClassFixture<IntegrationTestFixture>
public class DcvLifecycleTests : IClassFixture<IntegrationTestFixture>, IDisposable
{
private readonly IntegrationTestFixture _fixture;
private readonly ITestOutputHelper _output;
private readonly List<IDisposable> _toDispose = new List<IDisposable>();

public DcvLifecycleTests(IntegrationTestFixture fixture, ITestOutputHelper output)
{
_fixture = fixture;
_output = output;
}

public void Dispose()
{
foreach (var d in _toDispose)
d.Dispose();
_toDispose.Clear();
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
Expand All @@ -69,11 +77,17 @@ private static string GenerateCsrPem(string commonName)
+ "\n-----END CERTIFICATE REQUEST-----";
}

private IDomainValidatorFactory BuildDnsFactory() =>
_fixture.IsCloudflareConfigured
? (IDomainValidatorFactory)new CloudflareDomainValidatorFactory(
_fixture.CloudflareApiToken, _fixture.CloudflareZoneId)
: new StubDomainValidatorFactory();
private IDomainValidatorFactory BuildDnsFactory()
{
if (_fixture.IsCloudflareConfigured)
{
var factory = new CloudflareDomainValidatorFactory(
_fixture.CloudflareApiToken, _fixture.CloudflareZoneId);
_toDispose.Add(factory);
return factory;
}
return new StubDomainValidatorFactory();
}

/// <summary>
/// Runs <c>plugin.Synchronize</c> and returns every record that came out of the
Expand All @@ -88,6 +102,7 @@ private static async Task<List<AnyCAPluginCertificate>> RunSyncAsync(CERTInextCA
var syncTask = Task.Run(async () =>
{
await plugin.Synchronize(buffer, lastSync: null, fullSync: true, cancelToken: System.Threading.CancellationToken.None);
// Synchronize calls CompleteAdding() in its finally block; guard against double-call.
if (!buffer.IsAddingCompleted)
buffer.CompleteAdding();
});
Expand Down Expand Up @@ -655,7 +670,6 @@ public async Task BulkDvEnrollment_AllOrdersIssue_AndPaginationWorks()
List<AnyCAPluginCertificate> synced = null;
System.Diagnostics.Stopwatch syncPhaseSw = System.Diagnostics.Stopwatch.StartNew();
int passesUsed = 0;
int finalNotIssued = -1;

for (int pass = 1; pass <= maxSyncPasses; pass++)
{
Expand All @@ -664,13 +678,27 @@ public async Task BulkDvEnrollment_AllOrdersIssue_AndPaginationWorks()
synced = await RunSyncAsync(plugin);
passSw.Stop();

// Classify enrolled orders by their current status so that FAILED orders
// are not silently counted as still-pending, which would burn the full
// pass budget before producing a misleading "expected 0" assertion.
int generated = synced.Count(r => enrolledIds.Contains(r.CARequestID) && r.Status == (int)EndEntityStatus.GENERATED);
int pending = enrolledIds.Count - generated;
finalNotIssued = pending;
int failed = synced.Count(r => enrolledIds.Contains(r.CARequestID) && r.Status == (int)EndEntityStatus.FAILED);
int pending = enrolledIds.Count - generated - failed;

_output.WriteLine(
$"--- Sync pass #{pass}: returned {synced.Count} records, {generated}/{enrolledIds.Count} GENERATED, " +
$"{pending} still pending, elapsed={passSw.Elapsed:mm\\:ss} ---");
$"{failed} FAILED, {pending} still pending, elapsed={passSw.Elapsed:mm\\:ss} ---");

if (failed > 0)
{
var failedIds = synced
.Where(r => enrolledIds.Contains(r.CARequestID) && r.Status == (int)EndEntityStatus.FAILED)
.Select(r => r.CARequestID)
.Take(5);
Assert.Fail(
$"Pass #{pass}: {failed} order(s) reached FAILED status and will never issue: " +
string.Join(", ", failedIds));
}

if (pending == 0)
break;
Expand All @@ -696,10 +724,14 @@ public async Task BulkDvEnrollment_AllOrdersIssue_AndPaginationWorks()
$"{string.Join(", ", missing.Take(5))}{(missing.Count > 5 ? ", ..." : "")}");

// Final assertion — every enrolled order must be GENERATED after the polling window.
var lookup = synced.ToDictionary(r => r.CARequestID, r => r);
// Filter null CARequestIDs before building the lookup (guards against any CA response
// that omits the ID, which would otherwise throw ArgumentNullException in ToDictionary).
var lookup = synced
.Where(r => r.CARequestID != null)
.ToDictionary(r => r.CARequestID, r => r);
var notIssued = enrolledIds
.Where(id => lookup.TryGetValue(id, out var rec) && rec.Status != (int)EndEntityStatus.GENERATED)
.Select(id => lookup[id])
.Where(r => r.Status != (int)EndEntityStatus.GENERATED)
.ToList();

if (notIssued.Count > 0)
Expand All @@ -711,7 +743,7 @@ public async Task BulkDvEnrollment_AllOrdersIssue_AndPaginationWorks()

notIssued.Should().BeEmpty(
$"every enrolled DV order should auto-issue on the new sandbox after {maxSyncPasses} sync passes; " +
$"{notIssued.Count} did not (last pass: {finalNotIssued} pending).");
$"{notIssued.Count} did not.");

_output.WriteLine($"--- SUCCESS: {count}/{count} DV orders enrolled, synced, and issued in {passesUsed} sync pass(es). " +
$"Enroll={sw.Elapsed:mm\\:ss} SyncPhase={syncPhaseSw.Elapsed:mm\\:ss} Total={(sw.Elapsed + syncPhaseSw.Elapsed):mm\\:ss} ---");
Expand Down
Loading