Cloudflare's Workers AI FLUX.1 schnell content filter sometimes rejects peptide-comic prompts as NSFW (HTTP 400, errors[].code === 3030). On 2026-04-20 this produced a silent publish-without-featured-image for post #650 ("Semaglutide vs Tirzepatide") — the LLM-rewritten scene tripped the filter and we had no recovery path. New behaviour: on a 3030 response, the Cloudflare provider throws a typed PRAutoBlogger_Image_NSFW_Blocked exception. The batch wrapper catches it and tags the per-slot result with `error_type: nsfw_blocked`. Image_Pipeline hands the results to a new PRAutoBlogger_Image_NSFW_Retry helper, which reruns the blocked slot once with a sanitized fallback prompt (title + style suffix, no LLM rewrite). On success the retry result replaces the error entry; on a second block, a WARNING is logged and the pipeline publishes without that image — same terminal behaviour we had before, just explicit. Files: - New providers/class-image-nsfw-blocked.php — typed exception. - providers/class-cloudflare-image-support.php — is_nsfw_error() + throw_if_nsfw() helpers. - providers/class-cloudflare-image-provider.php — calls throw_if_nsfw inside the 4xx branch; batch catches the typed exception and marks the result with error_type. - core/class-image-prompt-builder.php — new public build_fallback_prompt shim so the retry layer can reach the rule-based fallback without going through the LLM path. - New core/class-image-nsfw-retry.php — single-shot retry helper. - core/class-image-pipeline.php — invokes the retry between batch completion and per-slot processing. - admin/class-settings-fields-extended.php — new toggle `prautoblogger_image_nsfw_retry` (default on) in the Images section. Tests: - CloudflareImageProviderTest — 3030 response throws NSFW exception; batch tags the per-slot result with `error_type: nsfw_blocked`. - ImagePipelineTest — retry succeeds when provider returns a good result on the second call; retry is skipped when the setting is off. Cost impact: worst-case an extra Cloudflare call per NSFW-blocked slot (~$0.000834). Current incidence ~2% of generations; ~+$0.00002 per generation amortized. Immaterial. Uninstall: new option covered by the existing prautoblogger_% LIKE sweep in uninstall.php. Part of the v0.8.0 image bundle (issues from thread image-mime-bug). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
484 lines
18 KiB
PHP
484 lines
18 KiB
PHP
<?php
|
|
/**
|
|
* Tests for PRAutoBlogger_Cloudflare_Image_Provider.
|
|
*
|
|
* Validates the image provider interface implementation methods.
|
|
* All HTTP calls are mocked — no real API calls.
|
|
*
|
|
* @package PRAutoBlogger\Tests\Providers
|
|
*/
|
|
|
|
namespace PRAutoBlogger\Tests\Providers;
|
|
|
|
use PRAutoBlogger\Tests\BaseTestCase;
|
|
use Brain\Monkey\Functions;
|
|
|
|
class CloudflareImageProviderTest extends BaseTestCase {
|
|
|
|
/**
|
|
* Fake PNG payload — bytes don't need to be a real PNG, just non-empty.
|
|
* Using a short string keeps failure messages readable.
|
|
*/
|
|
private const FAKE_IMAGE_BYTES = 'FAKE_PNG_BYTES';
|
|
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
|
|
// PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL constant is defined in
|
|
// prautoblogger.php — define here for test isolation.
|
|
if ( ! defined( 'PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL' ) ) {
|
|
define( 'PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL', 'flux-1-schnell' );
|
|
}
|
|
|
|
$this->stub_get_option( [
|
|
'prautoblogger_cloudflare_ai_token' => '',
|
|
'prautoblogger_cloudflare_account_id' => 'test-account-uuid',
|
|
'prautoblogger_image_model' => 'flux-1-schnell',
|
|
'prautoblogger_log_level' => 'info',
|
|
] );
|
|
|
|
Functions\when( 'wp_salt' )->justReturn( 'test_salt_key_for_unit_tests' );
|
|
Functions\when( 'is_wp_error' )->justReturn( false );
|
|
Functions\when( 'wp_remote_retrieve_header' )->justReturn( '' );
|
|
}
|
|
|
|
/**
|
|
* Helper: configure the plaintext API token the provider will eventually
|
|
* see after decryption. We round-trip through the real Encryption class
|
|
* (rather than stubbing it) so this test catches any regressions in the
|
|
* encrypt → option → decrypt flow at the same time.
|
|
*/
|
|
private function with_token( string $plaintext ): void {
|
|
$ciphertext = \PRAutoBlogger_Encryption::encrypt( $plaintext );
|
|
$this->stub_get_option( [
|
|
'prautoblogger_cloudflare_ai_token' => $ciphertext,
|
|
'prautoblogger_cloudflare_account_id' => 'test-account-uuid',
|
|
'prautoblogger_image_model' => 'flux-1-schnell',
|
|
'prautoblogger_log_level' => 'info',
|
|
] );
|
|
}
|
|
|
|
/**
|
|
* Provider implements the interface.
|
|
*/
|
|
public function test_provider_implements_interface(): void {
|
|
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
|
|
$this->assertInstanceOf( \PRAutoBlogger_Image_Provider_Interface::class, $provider );
|
|
}
|
|
|
|
/**
|
|
* get_provider_name returns the stable machine identifier.
|
|
*/
|
|
public function test_provider_name_is_stable_identifier(): void {
|
|
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
|
|
$this->assertSame( 'cloudflare_workers_ai', $provider->get_provider_name() );
|
|
}
|
|
|
|
/**
|
|
* Dimension validation rejects inputs outside the [64, 2048] envelope
|
|
* before any HTTP call is made.
|
|
*/
|
|
public function test_generate_image_rejects_out_of_range_dimensions(): void {
|
|
$this->with_token( 'test-token' );
|
|
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
|
|
|
|
$this->expectException( \InvalidArgumentException::class );
|
|
$provider->generate_image( 'a prompt', 32, 1080 );
|
|
}
|
|
|
|
/**
|
|
* Empty prompts are rejected — an empty string burns budget on
|
|
* Workers AI for a deterministic "bad request" result.
|
|
*/
|
|
public function test_generate_image_rejects_empty_prompt(): void {
|
|
$this->with_token( 'test-token' );
|
|
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
|
|
|
|
$this->expectException( \InvalidArgumentException::class );
|
|
$provider->generate_image( ' ', 1080, 1080 );
|
|
}
|
|
|
|
/**
|
|
* Happy path — raw image bytes come back with expected metadata shape.
|
|
*/
|
|
public function test_generate_image_returns_raw_bytes_and_metadata(): void {
|
|
$this->with_token( 'test-token' );
|
|
|
|
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' )->justReturn( 200 );
|
|
Functions\when( 'wp_remote_retrieve_body' )->justReturn( self::FAKE_IMAGE_BYTES );
|
|
Functions\when( 'wp_remote_retrieve_header' )->alias(
|
|
function ( $_response, $name ) {
|
|
return 'content-type' === strtolower( (string) $name ) ? 'image/png' : '';
|
|
}
|
|
);
|
|
|
|
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
|
|
$result = $provider->generate_image( 'a prompt', 1080, 1080, [ 'seed' => 42 ] );
|
|
|
|
$this->assertSame( self::FAKE_IMAGE_BYTES, $result['bytes'] );
|
|
$this->assertSame( 'image/png', $result['mime_type'] );
|
|
$this->assertSame( 1080, $result['width'] );
|
|
$this->assertSame( 1080, $result['height'] );
|
|
$this->assertSame( 42, $result['seed'] );
|
|
$this->assertSame( '@cf/black-forest-labs/flux-1-schnell', $result['model'] );
|
|
$this->assertGreaterThan( 0.0, $result['cost_usd'] );
|
|
$this->assertGreaterThanOrEqual( 0, $result['latency_ms'] );
|
|
}
|
|
|
|
/**
|
|
* Happy path where Workers AI wraps the image in a JSON envelope with
|
|
* base64-encoded bytes under `result.image`. The provider must decode
|
|
* and return the raw bytes so the pipeline never has to branch.
|
|
*/
|
|
public function test_generate_image_decodes_json_envelope(): void {
|
|
$this->with_token( 'test-token' );
|
|
|
|
$encoded = base64_encode( self::FAKE_IMAGE_BYTES );
|
|
$body = wp_json_encode( [ 'result' => [ 'image' => $encoded ], 'success' => true ] );
|
|
|
|
Functions\when( 'wp_remote_post' )->justReturn( [
|
|
'body' => $body,
|
|
'response' => [ 'code' => 200 ],
|
|
'headers' => [ 'content-type' => 'application/json' ],
|
|
] );
|
|
Functions\when( 'wp_remote_retrieve_response_code' )->justReturn( 200 );
|
|
Functions\when( 'wp_remote_retrieve_body' )->justReturn( $body );
|
|
Functions\when( 'wp_remote_retrieve_header' )->alias(
|
|
function ( $_response, $name ) {
|
|
return 'content-type' === strtolower( (string) $name ) ? 'application/json' : '';
|
|
}
|
|
);
|
|
|
|
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
|
|
$result = $provider->generate_image( 'a prompt', 600, 600 );
|
|
|
|
$this->assertSame( self::FAKE_IMAGE_BYTES, $result['bytes'] );
|
|
$this->assertSame( 'image/png', $result['mime_type'] );
|
|
}
|
|
|
|
/**
|
|
* A 401 must NOT retry — auth errors are deterministic and retrying
|
|
* burns budget. The provider should throw on the first response.
|
|
*
|
|
* We can't assert "exactly one HTTP call" without injecting a call
|
|
* counter, so we assert that the throw message contains the status.
|
|
*/
|
|
public function test_generate_image_fails_loudly_on_401(): void {
|
|
$this->with_token( 'test-token' );
|
|
|
|
Functions\when( 'wp_remote_post' )->justReturn( [
|
|
'body' => '{"errors":[{"message":"bad token"}]}',
|
|
'response' => [ 'code' => 401 ],
|
|
'headers' => [],
|
|
] );
|
|
Functions\when( 'wp_remote_retrieve_response_code' )->justReturn( 401 );
|
|
Functions\when( 'wp_remote_retrieve_body' )->justReturn( '{"errors":[{"message":"bad token"}]}' );
|
|
|
|
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
|
|
|
|
try {
|
|
$provider->generate_image( 'a prompt', 1080, 1080 );
|
|
$this->fail( 'Expected RuntimeException for HTTP 401.' );
|
|
} catch ( \RuntimeException $e ) {
|
|
$this->assertStringContainsString( '401', $e->getMessage() );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A 400 carrying Cloudflare's NSFW code (3030) must throw the typed
|
|
* PRAutoBlogger_Image_NSFW_Blocked exception, not a generic
|
|
* RuntimeException. Image_NSFW_Retry keys off the typed exception to
|
|
* decide whether to rebuild the prompt and retry.
|
|
*/
|
|
public function test_generate_image_throws_nsfw_blocked_on_code_3030(): void {
|
|
$this->with_token( 'test-token' );
|
|
|
|
$nsfw_body = '{"errors":[{"message":"AiError: Input prompt contains NSFW content.","code":3030}]}';
|
|
|
|
Functions\when( 'wp_remote_post' )->justReturn( [
|
|
'body' => $nsfw_body,
|
|
'response' => [ 'code' => 400 ],
|
|
'headers' => [],
|
|
] );
|
|
Functions\when( 'wp_remote_retrieve_response_code' )->justReturn( 400 );
|
|
Functions\when( 'wp_remote_retrieve_body' )->justReturn( $nsfw_body );
|
|
|
|
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
|
|
$this->expectException( \PRAutoBlogger_Image_NSFW_Blocked::class );
|
|
$provider->generate_image( 'some peptide prompt', 1080, 1080 );
|
|
}
|
|
|
|
/**
|
|
* A non-3030 4xx (e.g. 401 bad token) must NOT be tagged NSFW — the
|
|
* retry path would be wasted work there.
|
|
*/
|
|
public function test_generate_image_batch_tags_nsfw_errors_distinctly(): void {
|
|
$this->with_token( 'test-token' );
|
|
|
|
$nsfw_body = '{"errors":[{"message":"AiError: Input prompt contains NSFW content.","code":3030}]}';
|
|
|
|
Functions\when( 'wp_remote_post' )->justReturn( [
|
|
'body' => $nsfw_body,
|
|
'response' => [ 'code' => 400 ],
|
|
'headers' => [],
|
|
] );
|
|
Functions\when( 'wp_remote_retrieve_response_code' )->justReturn( 400 );
|
|
Functions\when( 'wp_remote_retrieve_body' )->justReturn( $nsfw_body );
|
|
|
|
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
|
|
$results = $provider->generate_image_batch( [
|
|
'image_a' => [ 'prompt' => 'some prompt', 'width' => 1080, 'height' => 1080 ],
|
|
] );
|
|
|
|
$this->assertArrayHasKey( 'image_a', $results );
|
|
$this->assertSame( 'nsfw_blocked', $results['image_a']['error_type'] ?? null );
|
|
$this->assertArrayHasKey( 'error', $results['image_a'] );
|
|
}
|
|
|
|
/**
|
|
* Response shape must be one of: image/* bytes OR JSON envelope with
|
|
* base64 image. Anything else is a contract breach — fail loudly.
|
|
*/
|
|
public function test_generate_image_throws_on_unexpected_response_shape(): void {
|
|
$this->with_token( 'test-token' );
|
|
|
|
Functions\when( 'wp_remote_post' )->justReturn( [
|
|
'body' => '<!doctype html><html><body>oops</body></html>',
|
|
'response' => [ 'code' => 200 ],
|
|
'headers' => [ 'content-type' => 'text/html' ],
|
|
] );
|
|
Functions\when( 'wp_remote_retrieve_response_code' )->justReturn( 200 );
|
|
Functions\when( 'wp_remote_retrieve_body' )->justReturn( '<!doctype html><html><body>oops</body></html>' );
|
|
Functions\when( 'wp_remote_retrieve_header' )->alias(
|
|
function ( $_response, $name ) {
|
|
return 'content-type' === strtolower( (string) $name ) ? 'text/html' : '';
|
|
}
|
|
);
|
|
|
|
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
|
|
$this->expectException( \RuntimeException::class );
|
|
$provider->generate_image( 'a prompt', 1080, 1080 );
|
|
}
|
|
|
|
/**
|
|
* Cost estimation is positive for in-range dimensions and scales with
|
|
* area — a 1080² image costs more than a 600² image on the same model.
|
|
*/
|
|
public function test_estimate_cost_scales_with_area(): void {
|
|
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
|
|
|
|
$small = $provider->estimate_cost( 600, 600, [ 'model' => 'flux-1-schnell' ] );
|
|
$large = $provider->estimate_cost( 1080, 1080, [ 'model' => 'flux-1-schnell' ] );
|
|
|
|
$this->assertGreaterThan( 0.0, $small );
|
|
$this->assertGreaterThan( $small, $large );
|
|
}
|
|
|
|
/**
|
|
* [dev] is meaningfully pricier than [schnell] at the same dimensions —
|
|
* the ~4x multiplier is the reason we default to schnell.
|
|
*/
|
|
public function test_estimate_cost_dev_more_expensive_than_schnell(): void {
|
|
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
|
|
|
|
$schnell = $provider->estimate_cost( 1080, 1080, [ 'model' => 'flux-1-schnell' ] );
|
|
$dev = $provider->estimate_cost( 1080, 1080, [ 'model' => 'flux-1-dev' ] );
|
|
|
|
$this->assertGreaterThan( $schnell, $dev );
|
|
}
|
|
|
|
/**
|
|
* validate_credentials_detailed reports 'token_empty' debug tag when
|
|
* no token is configured, without making an HTTP call.
|
|
*/
|
|
public function test_validate_credentials_reports_missing_token(): void {
|
|
// Default setUp leaves token empty.
|
|
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
|
|
$result = $provider->validate_credentials_detailed();
|
|
|
|
$this->assertSame( 'error', $result['status'] );
|
|
$this->assertSame( 'token_empty', $result['debug'] );
|
|
}
|
|
|
|
/**
|
|
* validate_credentials_detailed reports 'account_id_empty' when the
|
|
* account ID option is blank, without touching the network.
|
|
*/
|
|
public function test_validate_credentials_reports_missing_account_id(): void {
|
|
$this->stub_get_option( [
|
|
'prautoblogger_cloudflare_ai_token' => 'not-empty',
|
|
'prautoblogger_cloudflare_account_id' => '',
|
|
'prautoblogger_log_level' => 'info',
|
|
] );
|
|
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
|
|
$result = $provider->validate_credentials_detailed();
|
|
|
|
$this->assertSame( 'error', $result['status'] );
|
|
$this->assertSame( 'account_id_empty', $result['debug'] );
|
|
}
|
|
|
|
/**
|
|
* validate_credentials_detailed must never invoke the generation endpoint.
|
|
* Asserted by tracking every wp_remote_post call: a real-image call is
|
|
* the only thing that POSTs to `/ai/run/...`. Validator uses GET on
|
|
* `/ai/models/search`, which is the free read-only path.
|
|
*/
|
|
public function test_validate_credentials_never_calls_generate_endpoint(): void {
|
|
$this->with_token( 'test-token' );
|
|
|
|
$post_calls = [];
|
|
Functions\when( 'wp_remote_post' )->alias(
|
|
function ( $url, $args ) use ( &$post_calls ) {
|
|
$post_calls[] = $url;
|
|
return [ 'body' => '', 'response' => [ 'code' => 500 ] ];
|
|
}
|
|
);
|
|
Functions\when( 'wp_remote_get' )->justReturn( [
|
|
'body' => '{"result":[]}',
|
|
'response' => [ 'code' => 200 ],
|
|
] );
|
|
Functions\when( 'wp_remote_retrieve_response_code' )->justReturn( 200 );
|
|
Functions\when( 'wp_remote_retrieve_body' )->justReturn( '{"result":[]}' );
|
|
|
|
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
|
|
$provider->validate_credentials_detailed();
|
|
|
|
$this->assertSame( [], $post_calls, 'validate_credentials_detailed() must not POST to /ai/run/ endpoints.' );
|
|
}
|
|
|
|
/**
|
|
* Retry path: a 500 followed by a 200 must yield the 200 payload.
|
|
* Exercises the exponential-backoff loop end-to-end without sleeping
|
|
* (bootstrap sets PRAUTOBLOGGER_RETRY_BASE_DELAY_SECONDS to 0).
|
|
*/
|
|
public function test_generate_image_retries_on_5xx_then_succeeds(): void {
|
|
$this->with_token( 'test-token' );
|
|
|
|
$response_codes = [ 500, 200 ];
|
|
$response_bodies = [ 'upstream hiccup', 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;
|
|
}
|
|
);
|
|
Functions\when( 'wp_remote_retrieve_body' )->alias(
|
|
function () use ( &$response_bodies ) {
|
|
return array_shift( $response_bodies ) ?? self::FAKE_IMAGE_BYTES;
|
|
}
|
|
);
|
|
// 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 );
|
|
|
|
$this->assertSame( self::FAKE_IMAGE_BYTES, $result['bytes'] );
|
|
}
|
|
|
|
/**
|
|
* Retry exhaustion: persistent 502s must throw after MAX_RETRIES attempts
|
|
* rather than hanging indefinitely.
|
|
*/
|
|
public function test_generate_image_fails_after_max_retries_on_persistent_5xx(): void {
|
|
$this->with_token( 'test-token' );
|
|
|
|
$call_count = 0;
|
|
Functions\when( 'wp_remote_post' )->alias(
|
|
function () use ( &$call_count ) {
|
|
++$call_count;
|
|
return [
|
|
'body' => 'bad gateway',
|
|
'response' => [ 'code' => 502 ],
|
|
'headers' => [],
|
|
];
|
|
}
|
|
);
|
|
Functions\when( 'wp_remote_retrieve_response_code' )->justReturn( 502 );
|
|
Functions\when( 'wp_remote_retrieve_body' )->justReturn( 'bad gateway' );
|
|
|
|
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
|
|
|
|
try {
|
|
$provider->generate_image( 'a prompt', 1080, 1080 );
|
|
$this->fail( 'Expected RuntimeException after retry exhaustion.' );
|
|
} catch ( \RuntimeException $e ) {
|
|
$this->assertStringContainsString( '502', $e->getMessage() );
|
|
}
|
|
$this->assertSame(
|
|
PRAUTOBLOGGER_MAX_RETRIES,
|
|
$call_count,
|
|
'Provider must attempt exactly PRAUTOBLOGGER_MAX_RETRIES times before giving up.'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 429 triggers the same retry loop as 5xx (rate-limited, not deterministic
|
|
* error). The provider must retry and eventually succeed if the second
|
|
* attempt returns 200.
|
|
*/
|
|
public function test_generate_image_retries_on_429(): void {
|
|
$this->with_token( 'test-token' );
|
|
|
|
$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;
|
|
}
|
|
);
|
|
Functions\when( 'wp_remote_retrieve_body' )->alias(
|
|
function () use ( &$response_bodies ) {
|
|
return array_shift( $response_bodies ) ?? self::FAKE_IMAGE_BYTES;
|
|
}
|
|
);
|
|
// 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 );
|
|
|
|
$this->assertSame( self::FAKE_IMAGE_BYTES, $result['bytes'] );
|
|
}
|
|
|
|
/**
|
|
* validate_credentials_detailed returns 'ok' on a 200 from the models
|
|
* listing endpoint.
|
|
*/
|
|
public function test_validate_credentials_ok_on_200(): void {
|
|
$this->with_token( 'test-token' );
|
|
Functions\when( 'wp_remote_get' )->justReturn( [
|
|
'body' => '{"result":[{"id":"@cf/black-forest-labs/flux-1-schnell"}]}',
|
|
'response' => [ 'code' => 200 ],
|
|
] );
|
|
Functions\when( 'wp_remote_retrieve_response_code' )->justReturn( 200 );
|
|
Functions\when( 'wp_remote_retrieve_body' )->justReturn( '{"result":[]}' );
|
|
|
|
$provider = new \PRAutoBlogger_Cloudflare_Image_Provider();
|
|
$result = $provider->validate_credentials_detailed();
|
|
|
|
$this->assertSame( 'ok', $result['status'] );
|
|
}
|
|
}
|