Skip to content

Commit fc723ca

Browse files
committed
test(plugin): behavioral coverage for issue #7 factory-injection path
The earlier reflection tests in CERTInextCAPluginPublicSurfaceTests pin the SHAPE of the v3.2-safe public surface (no v3.3-only type leaks on any public constructor parameter), but did not exercise the end-to-end BEHAVIOR of the new SetDomainValidatorFactory injection hook. Adds three tests that close that gap: - Dcv_SilentlyNoOps_WhenNoFactoryInjected_AndDcvEnabledTrue — proves the v3.2 host scenario: parameterless construction, DcvEnabled=true in config, no factory injected. Enroll must not throw, must not call TrackOrder for DCV, must return the CA's pending response. - SetDomainValidatorFactory_AfterConstruction_WiresFactoryForSubsequentEnroll — proves the v3.3+ host scenario: parameterless construction, then SetDomainValidatorFactory(factory), then Enroll uses the injected factory end-to-end (DCV stages, verifies, returns issued cert). - SetDomainValidatorFactory_SecondCall_OverridesFirst — property-style setter semantics: the most recent call wins, important for hosts that may resolve a fresh factory per-initialize cycle. Also exposes PrimaryValidator on FakeDomainValidatorFactory so the third test can assert which factory is currently stored without resorting to reflection on internal fields. 145/145 unit tests pass.
1 parent ce8e02d commit fc723ca

2 files changed

Lines changed: 85 additions & 0 deletions

File tree

CERTInext.Tests/CERTInextCAPluginDcvTests.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,88 @@ public async Task Dcv_Skipped_WhenDcvEnabledFalse()
313313
mock.Verify(c => c.TrackOrderAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never);
314314
}
315315

316+
// ---------------------------------------------------------------------------
317+
// Issue #7 — IDomainValidatorFactory is optional / injected post-construction
318+
// ---------------------------------------------------------------------------
319+
320+
[Fact]
321+
public async Task Dcv_SilentlyNoOps_WhenNoFactoryInjected_AndDcvEnabledTrue()
322+
{
323+
// Simulates a v3.2 gateway host: plugin instantiated via the parameterless
324+
// public production constructor, DcvEnabled=true in the connector config,
325+
// but no IDomainValidatorFactory was injected via SetDomainValidatorFactory
326+
// (because the host's IAnyCAPlugin assembly doesn't even have that interface).
327+
// Enroll must:
328+
// * NOT throw (no missing-type / null-factory exception),
329+
// * NOT touch the CA's TrackOrder for DCV purposes,
330+
// * return the enrollment result the CA gave us (here: pending).
331+
var mock = NewMock();
332+
mock.Setup(c => c.EnrollCertificateAsync(It.IsAny<EnrollCertificateRequest>(), It.IsAny<CancellationToken>()))
333+
.ReturnsAsync(MockCertificateData.PendingEnrollResponse());
334+
335+
// Internal test ctor with factory = null AND DcvEnabled = true.
336+
var plugin = new CERTInextCAPlugin(mock.Object, domainValidatorFactory: null, DcvConfig(enabled: true));
337+
338+
var result = await Enroll(plugin);
339+
340+
result.Should().NotBeNull();
341+
result.Status.Should().Be((int)EndEntityStatus.EXTERNALVALIDATION,
342+
"with no factory the CA's pending response must be passed through unchanged");
343+
mock.Verify(c => c.TrackOrderAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Never,
344+
"EnrollNewAsync must short-circuit the DCV block when _domainValidatorFactory is null");
345+
}
346+
347+
[Fact]
348+
public async Task SetDomainValidatorFactory_AfterConstruction_WiresFactoryForSubsequentEnroll()
349+
{
350+
// The v3.3+ gateway path: host instantiates the plugin via the parameterless
351+
// public constructor, resolves an IDomainValidatorFactory from its own
352+
// service container, then calls SetDomainValidatorFactory(factory) before
353+
// Initialize. Subsequent Enroll() calls must use the injected factory.
354+
var (mock, validator) = HappyPathMocks();
355+
356+
// Plugin starts with NO factory — proves the setter does the wire-up, not
357+
// some prior constructor parameter.
358+
var plugin = new CERTInextCAPlugin(
359+
mock.Object,
360+
domainValidatorFactory: null,
361+
DcvConfig(dcvWaitForIssuanceSeconds: 10));
362+
363+
plugin.SetDomainValidatorFactory(new FakeDomainValidatorFactory(validator));
364+
365+
var result = await Enroll(plugin);
366+
367+
result.Status.Should().Be((int)EndEntityStatus.GENERATED,
368+
"the factory injected via SetDomainValidatorFactory must drive DCV end-to-end");
369+
validator.StagedRecords.Should().NotBeEmpty(
370+
"SetDomainValidatorFactory must populate _domainValidatorFactory so DCV staging runs");
371+
}
372+
373+
[Fact]
374+
public void SetDomainValidatorFactory_SecondCall_OverridesFirst()
375+
{
376+
// Property-style setter semantics: the most recent SetDomainValidatorFactory
377+
// call wins. Important for gateway hosts that may resolve a fresh factory
378+
// per-initialize cycle.
379+
var plugin = new CERTInextCAPlugin();
380+
var firstValidator = new FakeDomainValidator();
381+
var secondValidator = new FakeDomainValidator();
382+
383+
plugin.SetDomainValidatorFactory(new FakeDomainValidatorFactory(firstValidator));
384+
plugin.SetDomainValidatorFactory(new FakeDomainValidatorFactory(secondValidator));
385+
386+
// The plugin uses _domainValidatorFactory through internal methods; we reach
387+
// the field via reflection to assert the second factory is the one stored.
388+
var field = typeof(CERTInextCAPlugin)
389+
.GetField("_domainValidatorFactory",
390+
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
391+
field.Should().NotBeNull();
392+
var stored = field!.GetValue(plugin) as FakeDomainValidatorFactory;
393+
stored.Should().NotBeNull();
394+
stored!.PrimaryValidator.Should().BeSameAs(secondValidator,
395+
"the most recent SetDomainValidatorFactory call must replace the earlier one");
396+
}
397+
316398
// ---------------------------------------------------------------------------
317399
// Failure modes
318400
// ---------------------------------------------------------------------------

CERTInext.Tests/FakeDomainValidator.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,8 @@ internal sealed class FakeDomainValidatorFactory : IDomainValidatorFactory
6262
public FakeDomainValidatorFactory(IDomainValidator validator = null) => _validator = validator;
6363

6464
public IDomainValidator ResolveDomainValidator(string domain, string validationType) => _validator;
65+
66+
/// <summary>The validator this factory returns; exposed for assertions in tests.</summary>
67+
public IDomainValidator PrimaryValidator => _validator;
6568
}
6669
}

0 commit comments

Comments
 (0)