fix(images): CTO review blockers on PR #9

Addresses four blockers from CTO review in
convo/prautoblogger/threads/2026-04-image-pipeline/04-cto-block-pr9.md:

1. PHPUnit red — 2 retry-path tests failing on bad mocks.
   The success-after-retry branch calls wp_remote_retrieve_header()
   only on the success attempt; the original mocks queued two mime
   values and popped the first (text/plain), which made the provider's
   normalize_response_body() throw "unexpected shape". Replaced the
   pop-queue with a single justReturn('image/png'). The retry-after
   header read on the 5xx/429 branch still works (coerces to int 0,
   falls back to exponential backoff).

2. XSS defense-in-depth — wrap untrusted API response bodies in
   esc_html() before they land in exception / user-facing messages.
   - class-cloudflare-image-provider.php: 4xx-throw and
     retry-exhaustion-throw now escape substr($raw, 0, 300) and
     $last_error respectively.
   - class-cloudflare-image-validator.php: network-error and
     HTTP-error-body messages now use esc_html__() on the template
     and esc_html() on the substituted values.

3. PHPCS hygiene on the 4 new files (CTO path A). Converted the
   one-line private-method docblocks to full multi-line format with
   @param / @return / @throws tags. Added a docblock to __construct.

4. File-size cap. The docblock expansion pushed
   class-cloudflare-image-provider.php over 300 lines. Extracted
   credential lookups + URL builder + log forwarders into a new
   class-cloudflare-image-support.php (101 lines). Provider is now
   271 lines, all new files under the 300-line cap.

No production behaviour changed beyond the escape wrapping. The
provider's public API, retry policy, and response handling are
identical to c823618.

File sizes after fix:
- interface-image-provider.php:         100
- class-cloudflare-image-provider.php:  271
- class-cloudflare-image-pricing.php:    78
- class-cloudflare-image-validator.php: 116
- class-cloudflare-image-support.php:   101

Agent-Session: cowork-2026-04-15-image-pipeline-commit-1a-cto-blockers
This commit is contained in:
peptiderepo
2026-04-15 14:16:39 +07:00
parent c8236189cd
commit 573520558c
4 changed files with 182 additions and 105 deletions

View File

@@ -308,39 +308,29 @@ class CloudflareImageProviderTest extends BaseTestCase {
public function test_generate_image_retries_on_5xx_then_succeeds(): void {
$this->with_token( 'test-token' );
$codes = [ 500, 200 ];
Functions\when( 'wp_remote_post' )->alias(
function () use ( &$codes ) {
$code = array_shift( $codes ) ?? 200;
return [
'body' => 500 === $code ? 'upstream hiccup' : self::FAKE_IMAGE_BYTES,
'response' => [ 'code' => $code ],
'headers' => [ 'content-type' => 500 === $code ? 'text/plain' : 'image/png' ],
];
}
);
$response_codes = [ 500, 200 ];
$response_bodies = [ 'upstream hiccup', self::FAKE_IMAGE_BYTES ];
$response_codes = [ 500, 200 ];
Functions\when( 'wp_remote_post' )->justReturn( [
'body' => self::FAKE_IMAGE_BYTES,
'response' => [ 'code' => 200 ],
'headers' => [ 'content-type' => 'image/png' ],
] );
Functions\when( 'wp_remote_retrieve_response_code' )->alias(
function () use ( &$response_codes ) {
return array_shift( $response_codes ) ?? 200;
}
);
$response_bodies = [ 'upstream hiccup', self::FAKE_IMAGE_BYTES ];
Functions\when( 'wp_remote_retrieve_body' )->alias(
function () use ( &$response_bodies ) {
return array_shift( $response_bodies ) ?? self::FAKE_IMAGE_BYTES;
}
);
$mime_types = [ 'text/plain', 'image/png' ];
Functions\when( 'wp_remote_retrieve_header' )->alias(
function ( $_resp, $name ) use ( &$mime_types ) {
if ( 'content-type' === strtolower( (string) $name ) ) {
return array_shift( $mime_types ) ?? 'image/png';
}
return '';
}
);
// wp_remote_retrieve_header is called only on the success attempt —
// the 5xx branch returns before any header lookup. One justReturn
// for the content-type is enough; retry-after lookups accept the
// same stub and will coerce to (int) 0.
Functions\when( 'wp_remote_retrieve_header' )->justReturn( 'image/png' );
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
$result = $provider->generate_image( 'a prompt', 1080, 1080 );
@@ -392,38 +382,28 @@ class CloudflareImageProviderTest extends BaseTestCase {
public function test_generate_image_retries_on_429(): void {
$this->with_token( 'test-token' );
$codes = [ 429, 200 ];
Functions\when( 'wp_remote_post' )->alias(
function () use ( &$codes ) {
$code = array_shift( $codes ) ?? 200;
return [
'body' => 429 === $code ? 'slow down' : self::FAKE_IMAGE_BYTES,
'response' => [ 'code' => $code ],
'headers' => [ 'content-type' => 429 === $code ? 'text/plain' : 'image/png' ],
];
}
);
$response_codes = [ 429, 200 ];
$response_codes = [ 429, 200 ];
$response_bodies = [ 'slow down', self::FAKE_IMAGE_BYTES ];
Functions\when( 'wp_remote_post' )->justReturn( [
'body' => self::FAKE_IMAGE_BYTES,
'response' => [ 'code' => 200 ],
'headers' => [ 'content-type' => 'image/png' ],
] );
Functions\when( 'wp_remote_retrieve_response_code' )->alias(
function () use ( &$response_codes ) {
return array_shift( $response_codes ) ?? 200;
}
);
$response_bodies = [ 'slow down', self::FAKE_IMAGE_BYTES ];
Functions\when( 'wp_remote_retrieve_body' )->alias(
function () use ( &$response_bodies ) {
return array_shift( $response_bodies ) ?? self::FAKE_IMAGE_BYTES;
}
);
$mime_types = [ 'text/plain', 'image/png' ];
Functions\when( 'wp_remote_retrieve_header' )->alias(
function ( $_resp, $name ) use ( &$mime_types ) {
if ( 'content-type' === strtolower( (string) $name ) ) {
return array_shift( $mime_types ) ?? 'image/png';
}
return '';
}
);
// Content-type is only read on the success attempt; retry-after is
// read on the 429 attempt and (int)-coerces anything non-numeric
// back to 0, falling through to exponential backoff.
Functions\when( 'wp_remote_retrieve_header' )->justReturn( 'image/png' );
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
$result = $provider->generate_image( 'a prompt', 1080, 1080 );