diff --git a/src/DNS/Zone/Resolver.php b/src/DNS/Zone/Resolver.php index ef1646b..6385795 100644 --- a/src/DNS/Zone/Resolver.php +++ b/src/DNS/Zone/Resolver.php @@ -42,6 +42,11 @@ public static function lookup(Message $query, Zone $zone): Message $records = self::selectBestRecords($query, $zone); if (empty($records)) { + // SOA is stored separately; if querying SOA at the zone apex, return it + if ($question->type === Record::TYPE_SOA && $question->name === $zone->name) { + return self::soaApexResponse($query, $zone); + } + return Message::response( header: $query->header, responseCode: Message::RCODE_NXDOMAIN, @@ -157,6 +162,11 @@ private static function handleExactMatch(array $records, Message $query, Zone $z $isAuthoritative = $zone->isAuthoritative($question->name); if ($isAuthoritative) { + // SOA is stored separately in Zone; handle SOA queries at the zone apex + if ($question->type === Record::TYPE_SOA && $question->name === $zone->name) { + return self::soaApexResponse($query, $zone); + } + // Path E1: Exact match of type $exactTypeRecords = array_filter( $records, @@ -215,6 +225,21 @@ private static function handleExactMatch(array $records, Message $query, Zone $z } } + /** + * Build an authoritative SOA answer for the zone apex. + */ + private static function soaApexResponse(Message $query, Zone $zone): Message + { + return Message::response( + header: $query->header, + responseCode: Message::RCODE_NOERROR, + questions: $query->questions, + answers: [$zone->soa], + authoritative: true, + recursionAvailable: false + ); + } + /** * Randomize RRSet order for load balancing. * diff --git a/tests/e2e/DNS/ClientTest.php b/tests/e2e/DNS/ClientTest.php index 7213f4e..ed7c6fb 100644 --- a/tests/e2e/DNS/ClientTest.php +++ b/tests/e2e/DNS/ClientTest.php @@ -213,16 +213,16 @@ public function testSoaRecords(): void $response = $client->query(Message::query( new Question('appwrite.io', Record::TYPE_SOA) )); - $this->assertCount(0, $response->answers); + $this->assertCount(0, $response->authority); - $authority = $response->authority; - $this->assertCount(1, $authority); - $this->assertSame('appwrite.io', $authority[0]->name); - $this->assertSame(Record::CLASS_IN, $authority[0]->class); - $this->assertSame(30, $authority[0]->ttl); - $this->assertSame(Record::TYPE_SOA, $authority[0]->type); + $answers = $response->answers; + $this->assertCount(1, $answers); + $this->assertSame('appwrite.io', $answers[0]->name); + $this->assertSame(Record::CLASS_IN, $answers[0]->class); + $this->assertSame(30, $answers[0]->ttl); + $this->assertSame(Record::TYPE_SOA, $answers[0]->type); - $rdata = $authority[0]->rdata; + $rdata = $answers[0]->rdata; $this->assertStringContainsString('ns1.appwrite.zone', $rdata); $this->assertStringContainsString('team.appwrite.io', $rdata); $this->assertStringContainsString('1 7200 1800 1209600 3600', $rdata); diff --git a/tests/unit/DNS/Zone/ResolverTest.php b/tests/unit/DNS/Zone/ResolverTest.php index 0146a09..3cf5c2e 100644 --- a/tests/unit/DNS/Zone/ResolverTest.php +++ b/tests/unit/DNS/Zone/ResolverTest.php @@ -465,6 +465,49 @@ public function testLookupReturnsApexAAAARecord(): void $this->assertSame($aaaaRecord, $response->answers[0]); } + public function testLookupReturnsSoaAnswerForApexSoaQueryWithRecords(): void + { + $soa = new Record( + 'example.com', + Record::TYPE_SOA, + ttl: 300, + rdata: 'ns1.appwrite.zone. team@appwrite.io. 1761705275 3600 600 86400 300' + ); + $aRecord = new Record('example.com', Record::TYPE_A, ttl: 3600, rdata: '1.1.1.1'); + $zone = new Zone('example.com', [$aRecord], $soa); + + $question = new Question('example.com', Record::TYPE_SOA); + $query = Message::query($question); + $response = Resolver::lookup($query, $zone); + + $this->assertSame(Message::RCODE_NOERROR, $response->header->responseCode); + $this->assertCount(1, $response->answers); + $this->assertSame($soa, $response->answers[0]); + $this->assertTrue($response->header->authoritative); + $this->assertFalse($response->header->recursionAvailable); + } + + public function testLookupReturnsSoaAnswerForApexSoaQueryWithNoRecords(): void + { + $soa = new Record( + 'example.com', + Record::TYPE_SOA, + ttl: 300, + rdata: 'ns1.appwrite.zone. team@appwrite.io. 1761705275 3600 600 86400 300' + ); + $zone = new Zone('example.com', [], $soa); + + $question = new Question('example.com', Record::TYPE_SOA); + $query = Message::query($question); + $response = Resolver::lookup($query, $zone); + + $this->assertSame(Message::RCODE_NOERROR, $response->header->responseCode); + $this->assertCount(1, $response->answers); + $this->assertSame($soa, $response->answers[0]); + $this->assertTrue($response->header->authoritative); + $this->assertFalse($response->header->recursionAvailable); + } + public function testLookupReturnsSoaInAuthorityForApexNonNSQuery(): void { $soa = new Record(