Skip to content

Testing

Testing

Every capability in this library resolves DNS through the Cbox\Dns\Contracts\Resolver contract. Cbox\Dns\Testing\FakeResolver is an in-memory implementation of that contract, so you can drive the entire package — including the DNSSEC chain walk — without a single network call.

Stubbing simple answers

stub() records the values a host + type (optionally per nameserver) resolves to:

use Cbox\Dns\Dns;
use Cbox\Dns\Enums\RecordType;
use Cbox\Dns\Testing\FakeResolver;

$fake = (new FakeResolver)
    ->stub('example.com', RecordType::A, ['93.184.216.34']);

$dns = new Dns($fake);

expect($dns->lookup('example.com', RecordType::A)->values())
    ->toBe(['93.184.216.34']);

Each stub() call returns the fake, so calls chain. stub() also takes ttl: and priority: for records that need them; stubRecords() takes fully-formed DnsRecords when you need per-record control (e.g. several MX hosts with different preferences).

Failures, strict mode, and assertions

stubFailure() models a non-success RCODE (SERVFAIL, NXDOMAIN, …) so you can drive the code paths a bare empty answer cannot:

use Cbox\Dns\Enums\Rcode;

$fake->stubFailure('example.com', RecordType::A, Rcode::ServFail);

strict() makes an unstubbed query throw instead of returning an empty answer, so a fixture typo fails loudly. Every query is recorded — assert what was asked with assertQueried(), or inspect queries():

$fake->assertQueried('example.com', RecordType::A, '8.8.8.8');

The InteractsWithDns trait

Compose Cbox\Dns\Testing\InteractsWithDns into a test case to skip the boilerplate. It provides fakeDns() (a shared fake), fakeDnsFacade() (a Dns wired to it, with internal nameservers allowed for convenience), and stubZone() for the NS-chain setup:

use Cbox\Dns\Testing\InteractsWithDns;

uses(InteractsWithDns::class);

it('verifies ownership', function () {
    $this->stubZone('example.com', ['ns1.example.com' => '203.0.113.1'])
        ->stub('_cbox-challenge.example.com', RecordType::TXT, ['token'], nameserver: '203.0.113.1');

    expect($this->fakeDnsFacade()->verifyDomain('example.com', 'token'))->toBeTrue();
});

Modelling authoritative vs. recursive

The nameserver argument lets you give different servers different answers — which is exactly how you exercise domain verification and propagation offline. A stub with no nameserver acts as the wildcard fallback.

$fake = (new FakeResolver)
    // The zone's NS set, and those NS hostnames resolved to IPs:
    ->stub('example.com', RecordType::NS, ['ns1.example.com'])
    ->stub('ns1.example.com', RecordType::A, ['203.0.113.1'])
    // The challenge TXT, served authoritatively by ns1's IP:
    ->stub('_cbox-challenge.example.com', RecordType::TXT, ['secret-token'], nameserver: '203.0.113.1');

$dns = new Dns($fake);

expect($dns->verifyDomain('example.com', 'secret-token'))->toBeTrue();

AuthoritativeResolver reads the NS set, resolves each nameserver to an IP, then queries the target record against that IP with recursion off — so stubbing those three layers is all it takes.

Full responses (DNSSEC and denial-of-existence)

stubResponse() stores a complete DnsResponse, including raw DNSSEC RDATA and an authority section. This is the seam the package's own DNSSEC chain-walk tests use to drive each zone level offline:

use Cbox\Dns\ValueObjects\DnsResponse;

$fake->stubResponse('com', RecordType::DNSKEY, $dnskeyResponse, nameserver: null);

Constructing signed fixtures by hand is involved; for realistic DNSSEC testing, see how the suite builds chains under tests/Support/ (a test zone-signer plus real captured vectors). The point for your own tests is that no network is required.

Why this works everywhere

AuthoritativeResolver, DomainVerifier, PropagationChecker, DnssecValidator, and the whole Diagnostics engine all take a Resolver (directly or via the facade). Inject a FakeResolver and the entire library runs deterministically and offline.