d36bb954d1
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
297 lines
11 KiB
PHP
297 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* Unit tests for WPDO_REST_API.
|
|
*
|
|
* Covers route registration, handler logic, permission callback,
|
|
* filter param extraction, and both zone-active + fallback paths.
|
|
*/
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class RestApiTest extends TestCase {
|
|
|
|
private WPDO_REST_API $api;
|
|
|
|
protected function setUp(): void {
|
|
$this->api = new WPDO_REST_API();
|
|
|
|
// Reset globals.
|
|
$GLOBALS['_wp_options'] = [];
|
|
$GLOBALS['_wp_postmeta'] = [];
|
|
$GLOBALS['_wp_post_types'] = [];
|
|
$GLOBALS['_wp_current_user_can'] = [];
|
|
$GLOBALS['_wp_valid_nonces'] = [];
|
|
$_COOKIE = [];
|
|
|
|
// Reset Feature Flags cache via reflection.
|
|
$ff_ref = new ReflectionClass( WPDO_Feature_Flags::class );
|
|
$ff_prop = $ff_ref->getProperty( 'cache' );
|
|
$ff_prop->setAccessible( true );
|
|
$ff_prop->setValue( null, null );
|
|
|
|
// Reset Schema Registry singleton.
|
|
$ref = new ReflectionClass( WPDO_Schema_Registry::class );
|
|
$prop = $ref->getProperty( 'instance' );
|
|
$prop->setAccessible( true );
|
|
$prop->setValue( null, null );
|
|
}
|
|
|
|
// ── Route registration ────────────────────────────────────────────────────
|
|
|
|
public function test_register_routes_calls_register_rest_route(): void {
|
|
// register_rest_route is stubbed to return true — just confirm no exception.
|
|
$this->api->register_routes();
|
|
$this->assertTrue( true );
|
|
}
|
|
|
|
// ── Permission callback ───────────────────────────────────────────────────
|
|
|
|
public function test_require_manage_options_false_when_not_admin(): void {
|
|
$GLOBALS['_wp_current_user_can']['manage_options'] = false;
|
|
$this->assertFalse( $this->api->require_manage_options() );
|
|
}
|
|
|
|
public function test_require_manage_options_true_when_admin(): void {
|
|
$GLOBALS['_wp_current_user_can']['manage_options'] = true;
|
|
$this->assertTrue( $this->api->require_manage_options() );
|
|
}
|
|
|
|
// ── get_status ────────────────────────────────────────────────────────────
|
|
|
|
public function test_get_status_returns_version_and_engine(): void {
|
|
$req = new WP_REST_Request( 'GET', '/wpdo/v1/status' );
|
|
$response = $this->api->get_status( $req );
|
|
|
|
$this->assertSame( 200, $response->get_status() );
|
|
$data = $response->get_data();
|
|
$this->assertSame( WPDO_VERSION, $data['version'] );
|
|
$this->assertSame( 'mysql', $data['engine'] );
|
|
$this->assertArrayHasKey( 'fields', $data );
|
|
$this->assertArrayHasKey( 'modules', $data );
|
|
}
|
|
|
|
// ── get_listing (single) ──────────────────────────────────────────────────
|
|
|
|
public function test_get_listing_404_when_post_not_found(): void {
|
|
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings/9999' );
|
|
$req->set_param( 'id', 9999 );
|
|
|
|
$response = $this->api->get_listing( $req );
|
|
$this->assertSame( 404, $response->get_status() );
|
|
}
|
|
|
|
public function test_get_listing_returns_postmeta_when_zones_idle(): void {
|
|
$GLOBALS['_wp_post_types'][42] = 'hp_listing';
|
|
$GLOBALS['_wp_postmeta'][42]['hp_price'] = '500';
|
|
$GLOBALS['_wp_postmeta'][42]['hp_description'] = 'Test desc';
|
|
|
|
// Register hot + cold fields.
|
|
$registry = WPDO_Schema_Registry::instance();
|
|
$registry->register( 'test', [
|
|
'post_type' => 'hp_listing',
|
|
'meta_key' => 'hp_price',
|
|
'zone' => 'hot',
|
|
'column' => 'hp_price',
|
|
'type' => 'decimal',
|
|
] );
|
|
$registry->register( 'test', [
|
|
'post_type' => 'hp_listing',
|
|
'meta_key' => 'hp_description',
|
|
'zone' => 'cold',
|
|
] );
|
|
|
|
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings/42' );
|
|
$req->set_param( 'id', 42 );
|
|
|
|
$response = $this->api->get_listing( $req );
|
|
$this->assertSame( 200, $response->get_status() );
|
|
|
|
$data = $response->get_data();
|
|
$this->assertSame( 42, $data['id'] );
|
|
$this->assertSame( 'hp_listing', $data['post_type'] );
|
|
$this->assertSame( '500', $data['hp_price'] );
|
|
$this->assertSame( 'Test desc', $data['hp_description'] );
|
|
}
|
|
|
|
// ── get_stats ─────────────────────────────────────────────────────────────
|
|
|
|
public function test_get_stats_404_when_post_not_found(): void {
|
|
$req = new WP_REST_Request( 'GET', '/wpdo/v1/stats/9999' );
|
|
$req->set_param( 'id', 9999 );
|
|
|
|
$response = $this->api->get_stats( $req );
|
|
$this->assertSame( 404, $response->get_status() );
|
|
}
|
|
|
|
public function test_get_stats_returns_view_count(): void {
|
|
$GLOBALS['_wp_post_types'][55] = 'hp_listing';
|
|
// Warm zone idle, falls back to postmeta.
|
|
$GLOBALS['_wp_postmeta'][55]['hp_view_count'] = '17';
|
|
|
|
$req = new WP_REST_Request( 'GET', '/wpdo/v1/stats/55' );
|
|
$req->set_param( 'id', 55 );
|
|
|
|
$response = $this->api->get_stats( $req );
|
|
$this->assertSame( 200, $response->get_status() );
|
|
|
|
$data = $response->get_data();
|
|
$this->assertSame( 55, $data['post_id'] );
|
|
$this->assertIsInt( $data['view_count'] );
|
|
}
|
|
|
|
// ── get_listings (WP_Query fallback) ─────────────────────────────────────
|
|
|
|
public function test_get_listings_returns_200_via_wp_query_fallback(): void {
|
|
// Zone idle → WP_Query path.
|
|
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings' );
|
|
$req->set_param( 'post_type', 'hp_listing' );
|
|
$req->set_param( 'per_page', 10 );
|
|
$req->set_param( 'page', 1 );
|
|
|
|
$response = $this->api->get_listings( $req );
|
|
$this->assertSame( 200, $response->get_status() );
|
|
$this->assertIsArray( $response->get_data() );
|
|
}
|
|
|
|
// ── Pagination headers ────────────────────────────────────────────────────
|
|
|
|
public function test_listings_fallback_sets_pagination_headers(): void {
|
|
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings' );
|
|
$req->set_param( 'post_type', 'hp_listing' );
|
|
$req->set_param( 'per_page', 10 );
|
|
$req->set_param( 'page', 1 );
|
|
|
|
$response = $this->api->get_listings( $req );
|
|
$headers = $response->get_headers();
|
|
|
|
$this->assertArrayHasKey( 'X-WP-Total', $headers );
|
|
$this->assertArrayHasKey( 'X-WP-TotalPages', $headers );
|
|
}
|
|
|
|
// ── post_view ─────────────────────────────────────────────────────────────
|
|
|
|
public function test_post_view_403_without_nonce(): void {
|
|
$GLOBALS['_wp_post_types'][10] = 'hp_listing';
|
|
|
|
$req = new WP_REST_Request( 'POST', '/wpdo/v1/listings/10/view' );
|
|
$req->set_param( 'id', 10 );
|
|
// No nonce set.
|
|
|
|
$response = $this->api->post_view( $req );
|
|
$this->assertSame( 403, $response->get_status() );
|
|
}
|
|
|
|
public function test_post_view_403_with_invalid_nonce(): void {
|
|
$GLOBALS['_wp_post_types'][11] = 'hp_listing';
|
|
|
|
$req = new WP_REST_Request( 'POST', '/wpdo/v1/listings/11/view' );
|
|
$req->set_param( 'id', 11 );
|
|
$req->set_header( 'X-WP-Nonce', 'bad_nonce' );
|
|
|
|
$response = $this->api->post_view( $req );
|
|
$this->assertSame( 403, $response->get_status() );
|
|
}
|
|
|
|
public function test_post_view_404_when_post_not_found(): void {
|
|
$nonce = wp_create_nonce( 'wp_rest' );
|
|
|
|
$req = new WP_REST_Request( 'POST', '/wpdo/v1/listings/9999/view' );
|
|
$req->set_param( 'id', 9999 );
|
|
$req->set_header( 'X-WP-Nonce', $nonce );
|
|
|
|
$response = $this->api->post_view( $req );
|
|
$this->assertSame( 404, $response->get_status() );
|
|
}
|
|
|
|
public function test_post_view_returns_view_count(): void {
|
|
$GLOBALS['_wp_post_types'][20] = 'hp_listing';
|
|
$GLOBALS['_wp_postmeta'][20]['hp_view_count'] = '5';
|
|
$nonce = wp_create_nonce( 'wp_rest' );
|
|
|
|
$req = new WP_REST_Request( 'POST', '/wpdo/v1/listings/20/view' );
|
|
$req->set_param( 'id', 20 );
|
|
$req->set_header( 'X-WP-Nonce', $nonce );
|
|
|
|
$response = $this->api->post_view( $req );
|
|
$this->assertSame( 200, $response->get_status() );
|
|
|
|
$data = $response->get_data();
|
|
$this->assertSame( 20, $data['post_id'] );
|
|
$this->assertIsInt( $data['view_count'] );
|
|
}
|
|
|
|
// ── post_view: rate limiting ──────────────────────────────────────────────
|
|
|
|
public function test_post_view_success_sets_set_cookie_header(): void {
|
|
$GLOBALS['_wp_post_types'][25] = 'hp_listing';
|
|
$nonce = wp_create_nonce( 'wp_rest' );
|
|
|
|
$req = new WP_REST_Request( 'POST', '/wpdo/v1/listings/25/view' );
|
|
$req->set_param( 'id', 25 );
|
|
$req->set_header( 'X-WP-Nonce', $nonce );
|
|
|
|
$response = $this->api->post_view( $req );
|
|
$this->assertSame( 200, $response->get_status() );
|
|
$this->assertArrayHasKey( 'Set-Cookie', $response->get_headers() );
|
|
$this->assertStringContainsString( 'wpdo_view_25', $response->get_headers()['Set-Cookie'] );
|
|
}
|
|
|
|
public function test_post_view_429_when_ip_rate_limited(): void {
|
|
$GLOBALS['_wp_post_types'][30] = 'hp_listing';
|
|
$nonce = wp_create_nonce( 'wp_rest' );
|
|
|
|
// First call succeeds and sets IP transient.
|
|
$req1 = new WP_REST_Request( 'POST', '/wpdo/v1/listings/30/view' );
|
|
$req1->set_param( 'id', 30 );
|
|
$req1->set_header( 'X-WP-Nonce', $nonce );
|
|
$resp1 = $this->api->post_view( $req1 );
|
|
$this->assertSame( 200, $resp1->get_status() );
|
|
|
|
// Second call (same IP, within TTL) must be rate-limited.
|
|
$req2 = new WP_REST_Request( 'POST', '/wpdo/v1/listings/30/view' );
|
|
$req2->set_param( 'id', 30 );
|
|
$req2->set_header( 'X-WP-Nonce', $nonce );
|
|
$resp2 = $this->api->post_view( $req2 );
|
|
$this->assertSame( 429, $resp2->get_status() );
|
|
$this->assertSame( 'too_many_requests', $resp2->get_data()['code'] );
|
|
}
|
|
|
|
public function test_post_view_429_when_cookie_present(): void {
|
|
$GLOBALS['_wp_post_types'][35] = 'hp_listing';
|
|
$_COOKIE['wpdo_view_35'] = '1';
|
|
$nonce = wp_create_nonce( 'wp_rest' );
|
|
|
|
$req = new WP_REST_Request( 'POST', '/wpdo/v1/listings/35/view' );
|
|
$req->set_param( 'id', 35 );
|
|
$req->set_header( 'X-WP-Nonce', $nonce );
|
|
|
|
$response = $this->api->post_view( $req );
|
|
$this->assertSame( 429, $response->get_status() );
|
|
$this->assertSame( 'too_many_requests', $response->get_data()['code'] );
|
|
}
|
|
|
|
public function test_post_view_429_increments_rate_limit_stats(): void {
|
|
$GLOBALS['_wp_post_types'][40] = 'hp_listing';
|
|
$_COOKIE['wpdo_view_40'] = '1'; // Trigger cookie block.
|
|
$nonce = wp_create_nonce( 'wp_rest' );
|
|
|
|
$req = new WP_REST_Request( 'POST', '/wpdo/v1/listings/40/view' );
|
|
$req->set_param( 'id', 40 );
|
|
$req->set_header( 'X-WP-Nonce', $nonce );
|
|
$this->api->post_view( $req );
|
|
|
|
$stats = get_option( 'wpdo_rl_stats', [] );
|
|
$this->assertSame( 1, (int) ( $stats['40'] ?? 0 ) );
|
|
}
|
|
|
|
// ── get_status: rate_limit_stats ─────────────────────────────────────────
|
|
|
|
public function test_get_status_includes_rate_limit_stats(): void {
|
|
$req = new WP_REST_Request( 'GET', '/wpdo/v1/status' );
|
|
$data = $this->api->get_status( $req )->get_data();
|
|
|
|
$this->assertArrayHasKey( 'rate_limit_stats', $data );
|
|
$this->assertIsArray( $data['rate_limit_stats'] );
|
|
}
|
|
}
|