Skip to content

Commit a9efc74

Browse files
committed
feat(domain): Simplify renew API and expiry handling
1 parent d3aaebc commit a9efc74

3 files changed

Lines changed: 178 additions & 30 deletions

File tree

src/Domain/DomainService.php

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -247,30 +247,32 @@ public function register(
247247
}
248248

249249
/**
250-
* @param array{
251-
* name?: mixed,
252-
* currentExpirationDate?: mixed,
253-
* period?: mixed,
254-
* periodUnit?: mixed
255-
* }|non-empty-string $request
256-
* @param int|null $years Renewal period used by the simplified API variant.
250+
* Renews a domain for the requested number of years.
251+
*
252+
* If the current expiry date is not provided, it is resolved from {@see self::info()}.
253+
*
254+
* @param non-empty-string $domain
255+
* @param int<1,10> $years
256+
* @param null|string|\DateTimeInterface $expiry Current expiration date used for EPP renew matching.
257257
*
258-
* @return array{name: string|null, expirationDate: \DateTimeImmutable|null}
258+
* @return array{domain: string, expiryDate: \DateTimeImmutable|null}
259259
*/
260-
public function renew(string|array $request, ?int $years = null): array
260+
public function renew(string $domain, int $years = 1, null|string|\DateTimeInterface $expiry = null): array
261261
{
262-
$normalizedRequest = $this->inputNormalizer->normalizeRenewRequest(
263-
$request,
264-
$years,
265-
fn(string $name): string => $this->resolveCurrentExpirationDate($name),
266-
);
262+
$name = $this->inputNormalizer->requireDomainName($domain);
263+
264+
if ($years < 1 || $years > 10) {
265+
throw new \InvalidArgumentException('Domain renew years must be between 1 and 10.');
266+
}
267+
268+
$resolvedExpiry = $this->resolveRenewExpiryDate($name, $expiry);
267269

268270
$xml = $this->renewRequestBuilder->build(
269271
new DomainRenewRequest(
270-
$this->inputNormalizer->requireName($normalizedRequest),
271-
$this->inputNormalizer->requireCurrentExpirationDate($normalizedRequest),
272-
$this->inputNormalizer->optionalPositiveInt($normalizedRequest, 'period'),
273-
$this->inputNormalizer->optionalPeriodUnit($normalizedRequest),
272+
$name,
273+
$resolvedExpiry,
274+
$years,
275+
'y',
274276
),
275277
$this->tridGenerator->nextId(),
276278
);
@@ -281,7 +283,12 @@ public function renew(string|array $request, ?int $years = null): array
281283
$this->renewResponseParser->parse($responseXml, $metadata),
282284
);
283285

284-
return $this->responseMapper->mapRenewResponse($response);
286+
$renewData = $this->responseMapper->mapRenewResponse($response);
287+
288+
return [
289+
'domain' => $name,
290+
'expiryDate' => $renewData['expirationDate'],
291+
];
285292
}
286293

287294
/**
@@ -448,6 +455,19 @@ private function resolveCurrentExpirationDate(string $name): string
448455
return $expirationDate->format('Y-m-d');
449456
}
450457

458+
private function resolveRenewExpiryDate(string $name, null|string|\DateTimeInterface $expiry): string
459+
{
460+
if (null === $expiry) {
461+
return $this->resolveCurrentExpirationDate($name);
462+
}
463+
464+
if ($expiry instanceof \DateTimeInterface) {
465+
return $expiry->format('Y-m-d');
466+
}
467+
468+
return $this->inputNormalizer->normalizeExpirationDateForRenew($expiry);
469+
}
470+
451471
/**
452472
* @param array{
453473
* name?: mixed,

tests/Unit/Contact/ContactRequestFactoryPolicyTest.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,21 @@ public function testCreateAllowsEmptyNameForLegalEntityWithOrganization(): void
179179
self::assertSame('1', $request->extension?->isLegalEntity);
180180
}
181181

182+
public function testCreateAllowsNonEmptyNameForLegalEntity(): void
183+
{
184+
$factory = new ContactRequestFactory();
185+
$payload = $this->validCreatePayload();
186+
$payload['postalInfo']['name'] = 'Legal Contact Person';
187+
$payload['extension'] = [
188+
'isLegalEntity' => '1',
189+
];
190+
191+
$request = $factory->createFromArray($payload);
192+
193+
self::assertSame('Legal Contact Person', $request->postalInfo->name);
194+
self::assertSame('1', $request->extension?->isLegalEntity);
195+
}
196+
182197
public function testCreateRejectsEmptyNameForLegalEntityWithoutOrganization(): void
183198
{
184199
$factory = new ContactRequestFactory();

tests/Unit/Domain/DomainServiceTest.php

Lines changed: 124 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -616,21 +616,16 @@ public function nextId(): string
616616
};
617617

618618
$service = new DomainService($transport, null, $generator);
619-
$result = $service->renew([
620-
'currentExpirationDate' => '2027-02-01',
621-
'name' => 'example.rs',
622-
'period' => 1,
623-
'periodUnit' => 'y',
624-
]);
619+
$result = $service->renew('example.rs', 1, '2027-02-01');
625620

626621
self::assertStringContainsString('<domain:renew', $transport->writtenPayload);
627622
self::assertStringContainsString(
628623
'<domain:curExpDate>2027-02-01</domain:curExpDate>',
629624
$transport->writtenPayload,
630625
);
631-
self::assertSame('example.rs', $result['name']);
632-
self::assertInstanceOf(\DateTimeImmutable::class, $result['expirationDate']);
633-
self::assertSame('2028-02-01T00:00:00+00:00', $result['expirationDate']?->format('c'));
626+
self::assertSame('example.rs', $result['domain']);
627+
self::assertInstanceOf(\DateTimeImmutable::class, $result['expiryDate']);
628+
self::assertSame('2028-02-01T00:00:00+00:00', $result['expiryDate']?->format('c'));
634629
}
635630

636631
public function testDeleteSendsDomainDeleteCommandAndMapsParsedResponse(): void
@@ -751,8 +746,126 @@ public function readFrame(): string
751746
'<domain:period unit="y">1</domain:period>',
752747
$transport->writtenPayload,
753748
);
754-
self::assertInstanceOf(\DateTimeImmutable::class, $result['expirationDate']);
755-
self::assertSame('2028-02-01T00:00:00+00:00', $result['expirationDate']?->format('c'));
749+
self::assertSame('example.rs', $result['domain']);
750+
self::assertInstanceOf(\DateTimeImmutable::class, $result['expiryDate']);
751+
self::assertSame('2028-02-01T00:00:00+00:00', $result['expiryDate']?->format('c'));
752+
}
753+
754+
public function testRenewAcceptsDateTimeInterfaceExpiry(): void
755+
{
756+
$transport = new class () implements Transport {
757+
public string $writtenPayload = '';
758+
759+
public function connect(): void
760+
{
761+
// Not needed for this unit test.
762+
}
763+
764+
public function disconnect(): void
765+
{
766+
// Not needed for this unit test.
767+
}
768+
769+
public function writeFrame(string $payload): void
770+
{
771+
$this->writtenPayload = $payload;
772+
}
773+
774+
public function readFrame(): string
775+
{
776+
return '<?xml version="1.0" encoding="UTF-8"?>'
777+
. '<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">'
778+
. '<response>'
779+
. '<result code="1000"><msg>Command completed successfully</msg></result>'
780+
. '<resData>'
781+
. '<domain:renData xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">'
782+
. '<domain:name>example.rs</domain:name>'
783+
. '<domain:exDate>2028-02-01T00:00:00.0Z</domain:exDate>'
784+
. '</domain:renData>'
785+
. '</resData>'
786+
. '<trID><clTRID>DOMAIN-00000012</clTRID><svTRID>SV-12</svTRID></trID>'
787+
. '</response>'
788+
. '</epp>';
789+
}
790+
};
791+
792+
$service = new DomainService($transport);
793+
$service->renew('example.rs', 1, new \DateTimeImmutable('2027-02-01T08:00:00+01:00'));
794+
795+
self::assertStringContainsString(
796+
'<domain:curExpDate>2027-02-01</domain:curExpDate>',
797+
$transport->writtenPayload,
798+
);
799+
}
800+
801+
public function testRenewThrowsWhenYearsAreOutsideAllowedRange(): void
802+
{
803+
$transport = new class () implements Transport {
804+
public function connect(): void
805+
{
806+
// Not needed for this unit test.
807+
}
808+
809+
public function disconnect(): void
810+
{
811+
// Not needed for this unit test.
812+
}
813+
814+
public function writeFrame(string $payload): void
815+
{
816+
// Not needed for this unit test.
817+
}
818+
819+
public function readFrame(): string
820+
{
821+
return '';
822+
}
823+
};
824+
825+
$service = new DomainService($transport);
826+
827+
$this->expectException(\InvalidArgumentException::class);
828+
$this->expectExceptionMessage('Domain renew years must be between 1 and 10.');
829+
830+
$service->renew('example.rs', 11, '2027-02-01');
831+
}
832+
833+
public function testRenewThrowsProtocolExceptionWhenExpiryDoesNotMatchRegistryState(): void
834+
{
835+
$transport = new class () implements Transport {
836+
public function connect(): void
837+
{
838+
// Not needed for this unit test.
839+
}
840+
841+
public function disconnect(): void
842+
{
843+
// Not needed for this unit test.
844+
}
845+
846+
public function writeFrame(string $payload): void
847+
{
848+
// Not needed for this unit test.
849+
}
850+
851+
public function readFrame(): string
852+
{
853+
return '<?xml version="1.0" encoding="UTF-8"?>'
854+
. '<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">'
855+
. '<response>'
856+
. '<result code="2306"><msg>Current expiration date does not match object state</msg></result>'
857+
. '<trID><clTRID>DOMAIN-00000013</clTRID><svTRID>SV-13</svTRID></trID>'
858+
. '</response>'
859+
. '</epp>';
860+
}
861+
};
862+
863+
$service = new DomainService($transport);
864+
865+
$this->expectException(\RNIDS\Exception\ProtocolException::class);
866+
$this->expectExceptionMessage('EPP command failed with result code 2306');
867+
868+
$service->renew('example.rs', 1, '2027-02-01');
756869
}
757870

758871
public function testTransferApprovesDomainTransferAndMapsParsedResponse(): void

0 commit comments

Comments
 (0)