diff --git a/src/Cdn/Cache/Adapter/Cloudflare.php b/src/Cdn/Cache/Adapter/Cloudflare.php index b09c0d7..ea5b0ae 100644 --- a/src/Cdn/Cache/Adapter/Cloudflare.php +++ b/src/Cdn/Cache/Adapter/Cloudflare.php @@ -95,16 +95,25 @@ private function send(array $body): void } /** - * A 2xx is not enough: Cloudflare reports a rejected purge in the body. + * The status line is the primary signal, as it is for Fastly. Cloudflare's + * envelope carries its own verdict, and an explicit `success: false` inside + * a 2xx is still a rejected purge — but a 2xx without the envelope is + * acceptance, so a body a gateway rewrote or a test double never sent does + * not fail a purge the status line already confirmed. * * @param array{statusCode:int,response:array|string|null,error:string|null} $result */ private function isSuccess(array $result): bool { - return $result['statusCode'] >= 200 - && $result['statusCode'] < 300 - && \is_array($result['response']) - && ($result['response']['success'] ?? false) === true; + if ($result['statusCode'] < 200 || $result['statusCode'] >= 300) { + return false; + } + + if (\is_array($result['response']) && \array_key_exists('success', $result['response'])) { + return $result['response']['success'] === true; + } + + return true; } /** diff --git a/tests/Cdn/Cache/Adapter/CloudflareTest.php b/tests/Cdn/Cache/Adapter/CloudflareTest.php index 623981e..7c3e48e 100644 --- a/tests/Cdn/Cache/Adapter/CloudflareTest.php +++ b/tests/Cdn/Cache/Adapter/CloudflareTest.php @@ -68,7 +68,7 @@ public function testZonePurgeIsItsOwnOperation(): void public function testRejectsAPurgeTheBodyReportsAsFailed(): void { - // A 2xx alone does not mean the purge happened. + // The envelope's explicit verdict wins over the 2xx it travels in. $client = new TestClient([new Response(200, body: new Stream('{"success":false,"errors":[{"message":"Invalid zone"}]}'))]); $this->expectException(\RuntimeException::class); @@ -76,6 +76,27 @@ public function testRejectsAPurgeTheBodyReportsAsFailed(): void (new Cloudflare('zone', 'token', $client))->purgeDomain('example.com'); } + public function testAcceptsAPurgeWithoutTheEnvelope(): void + { + // A 2xx with no envelope is acceptance, like Fastly: the status line is + // the primary signal and only an explicit `success: false` overrides it. + $client = new TestClient([new Response(200, body: new Stream(''))]); + + (new Cloudflare('zone', 'token', $client))->purgeDomain('example.com'); + + $this->assertSame(['hosts' => ['example.com']], $client->calls[0]['body']); + } + + public function testRejectsAPurgeTheStatusReportsAsFailed(): void + { + // The reverse does not hold: no envelope softens a failing status. + $client = new TestClient([new Response(403, body: new Stream('{"success":false,"errors":[{"message":"Forbidden"}]}'))]); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Cloudflare purge failed with status 403: Forbidden'); + (new Cloudflare('zone', 'token', $client))->purgeDomain('example.com'); + } + public function testEmptyPurgesTouchNothing(): void { $client = new TestClient([]);