chore: initial snapshot of 2meet-data-optimizer v0.1.0
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
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Integration tests for WPDO_REST_API against real MariaDB.
|
||||
*
|
||||
* Creates a dedicated wp_itest_wpdo_hot_restapi table, seeds data,
|
||||
* and exercises all four REST handlers end-to-end.
|
||||
*/
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class RestApiIntegrationTest extends TestCase {
|
||||
|
||||
private static WPDO_REST_API $api;
|
||||
|
||||
/** Table name for this test suite (avoids collisions with other tests). */
|
||||
private static string $hot_table;
|
||||
private static string $warm_table;
|
||||
|
||||
public static function setUpBeforeClass(): void {
|
||||
global $wpdb;
|
||||
|
||||
self::$api = new WPDO_REST_API();
|
||||
self::$hot_table = $wpdb->prefix . 'wpdo_hot_restapi';
|
||||
self::$warm_table = $wpdb->prefix . 'wpdo_warm';
|
||||
|
||||
// Register fields for the fake 'restapi' post type.
|
||||
$registry = WPDO_Schema_Registry::instance();
|
||||
$registry->register( 'integration_rest', [
|
||||
'post_type' => 'restapi',
|
||||
'meta_key' => 'rp_price',
|
||||
'zone' => 'hot',
|
||||
'column' => 'rp_price',
|
||||
'type' => 'decimal',
|
||||
] );
|
||||
$registry->register( 'integration_rest', [
|
||||
'post_type' => 'restapi',
|
||||
'meta_key' => 'rp_featured',
|
||||
'zone' => 'hot',
|
||||
'column' => 'rp_featured',
|
||||
'type' => 'tinyint',
|
||||
] );
|
||||
$registry->register( 'integration_rest', [
|
||||
'post_type' => 'restapi',
|
||||
'meta_key' => 'rp_description',
|
||||
'zone' => 'cold',
|
||||
] );
|
||||
|
||||
// Create hot table.
|
||||
$wpdb->query(
|
||||
"CREATE TABLE IF NOT EXISTS `" . self::$hot_table . "` (
|
||||
post_id BIGINT UNSIGNED NOT NULL,
|
||||
rp_price DECIMAL(10,2) DEFAULT NULL,
|
||||
rp_featured TINYINT(1) DEFAULT NULL,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (post_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
||||
);
|
||||
|
||||
// Create warm table (needed by WPDO_Listing_Stats::get_view_count).
|
||||
$wpdb->query(
|
||||
"CREATE TABLE IF NOT EXISTS `" . self::$warm_table . "` (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
post_id BIGINT UNSIGNED NOT NULL,
|
||||
meta_key VARCHAR(255) NOT NULL,
|
||||
meta_value LONGTEXT,
|
||||
expires_at DATETIME DEFAULT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY post_meta (post_id, meta_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"
|
||||
);
|
||||
|
||||
// Seed 5 rows.
|
||||
for ( $i = 1; $i <= 5; $i++ ) {
|
||||
$price = $i * 100;
|
||||
$featured = $i % 2;
|
||||
$wpdb->query( "INSERT INTO `" . self::$hot_table . "` (post_id, rp_price, rp_featured) VALUES ($i, $price, $featured)" );
|
||||
}
|
||||
|
||||
// Set module to cutover so REST API reads from Zone A.
|
||||
WPDO_Feature_Flags::set( 'hot_restapi', 'cutover' );
|
||||
}
|
||||
|
||||
public static function tearDownAfterClass(): void {
|
||||
global $wpdb;
|
||||
$wpdb->query( "DROP TABLE IF EXISTS `" . self::$hot_table . "`" );
|
||||
$wpdb->query( "DROP TABLE IF EXISTS `" . self::$warm_table . "`" );
|
||||
WPDO_Feature_Flags::reset( 'hot_restapi' );
|
||||
}
|
||||
|
||||
protected function setUp(): void {
|
||||
$GLOBALS['_wp_cache'] = [];
|
||||
$GLOBALS['_wp_post_types'] = [];
|
||||
$GLOBALS['_wp_postmeta'] = [];
|
||||
$GLOBALS['_wp_current_user_can'] = [];
|
||||
$GLOBALS['_wp_valid_nonces'] = [];
|
||||
$GLOBALS['_wp_transients'] = []; // Reset rate-limit transients between tests.
|
||||
// Note: do NOT reset _wp_options here — Feature Flags state is stored there.
|
||||
|
||||
// Invalidate Feature Flags static request cache so each test reads fresh.
|
||||
$ref = new ReflectionClass( WPDO_Feature_Flags::class );
|
||||
$prop = $ref->getProperty( 'cache' );
|
||||
$prop->setAccessible( true );
|
||||
$prop->setValue( null, null );
|
||||
}
|
||||
|
||||
// ── GET /listings (Zone A path) ──────────────────────────────────────────
|
||||
|
||||
public function test_listings_returns_all_rows(): void {
|
||||
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings' );
|
||||
$req->set_param( 'post_type', 'restapi' );
|
||||
$req->set_param( 'per_page', 10 );
|
||||
|
||||
$response = self::$api->get_listings( $req );
|
||||
|
||||
$this->assertSame( 200, $response->get_status() );
|
||||
$data = $response->get_data();
|
||||
$this->assertCount( 5, $data );
|
||||
$this->assertSame( '5', $response->get_headers()['X-WP-Total'] );
|
||||
}
|
||||
|
||||
public function test_listings_returns_correct_fields(): void {
|
||||
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings' );
|
||||
$req->set_param( 'post_type', 'restapi' );
|
||||
$req->set_param( 'per_page', 1 );
|
||||
$req->set_param( 'orderby', 'post_id' );
|
||||
$req->set_param( 'order', 'ASC' );
|
||||
|
||||
$response = self::$api->get_listings( $req );
|
||||
$data = $response->get_data();
|
||||
|
||||
$this->assertSame( 1, $data[0]['id'] );
|
||||
$this->assertSame( 'restapi', $data[0]['post_type'] );
|
||||
$this->assertArrayHasKey( 'rp_price', $data[0] );
|
||||
$this->assertArrayHasKey( 'rp_featured', $data[0] );
|
||||
$this->assertArrayNotHasKey( 'post_id', $data[0] );
|
||||
$this->assertArrayNotHasKey( 'updated_at', $data[0] );
|
||||
}
|
||||
|
||||
public function test_listings_pagination(): void {
|
||||
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings' );
|
||||
$req->set_param( 'post_type', 'restapi' );
|
||||
$req->set_param( 'per_page', 2 );
|
||||
$req->set_param( 'page', 2 );
|
||||
$req->set_param( 'orderby', 'post_id' );
|
||||
$req->set_param( 'order', 'ASC' );
|
||||
|
||||
$response = self::$api->get_listings( $req );
|
||||
$data = $response->get_data();
|
||||
|
||||
$this->assertCount( 2, $data );
|
||||
$this->assertSame( 3, $data[0]['id'] ); // page 2 offset 2 → post_id 3
|
||||
$this->assertSame( '3', $response->get_headers()['X-WP-TotalPages'] );
|
||||
}
|
||||
|
||||
public function test_listings_per_page_clamped_to_max_100(): void {
|
||||
// PR-0 R-4: defense-in-depth — per_page=99999 must clamp to 100, not DoS the DB.
|
||||
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings' );
|
||||
$req->set_param( 'post_type', 'restapi' );
|
||||
$req->set_param( 'per_page', 99999 );
|
||||
|
||||
$response = self::$api->get_listings( $req );
|
||||
|
||||
// Should return at most 100 items (real dataset is 5 — capped by total).
|
||||
$data = $response->get_data();
|
||||
$this->assertLessThanOrEqual( 100, count( $data ) );
|
||||
}
|
||||
|
||||
public function test_listings_per_page_max_filter_overridable(): void {
|
||||
// PR-0 R-4: site owners can lower the cap via wpdo_rest_max_per_page filter.
|
||||
// We can't fully test add_filter() in this stubbed env, but we verify the
|
||||
// constant value is correctly read in the code path (above test exercises 100).
|
||||
$this->assertTrue( true );
|
||||
}
|
||||
|
||||
public function test_listings_filter_price_min(): void {
|
||||
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings' );
|
||||
$req->set_param( 'post_type', 'restapi' );
|
||||
$req->set_param( 'rp_price_min', 300 );
|
||||
|
||||
$response = self::$api->get_listings( $req );
|
||||
$data = $response->get_data();
|
||||
|
||||
// Prices are 100,200,300,400,500 → ≥300 = 3 rows.
|
||||
$this->assertSame( '3', $response->get_headers()['X-WP-Total'] );
|
||||
foreach ( $data as $item ) {
|
||||
$this->assertGreaterThanOrEqual( 300.0, (float) $item['rp_price'] );
|
||||
}
|
||||
}
|
||||
|
||||
public function test_listings_filter_price_range(): void {
|
||||
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings' );
|
||||
$req->set_param( 'post_type', 'restapi' );
|
||||
$req->set_param( 'rp_price_min', 200 );
|
||||
$req->set_param( 'rp_price_max', 400 );
|
||||
|
||||
$response = self::$api->get_listings( $req );
|
||||
|
||||
$this->assertSame( '3', $response->get_headers()['X-WP-Total'] );
|
||||
}
|
||||
|
||||
public function test_listings_filter_exact_value(): void {
|
||||
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings' );
|
||||
$req->set_param( 'post_type', 'restapi' );
|
||||
$req->set_param( 'rp_featured', 1 );
|
||||
|
||||
$response = self::$api->get_listings( $req );
|
||||
$data = $response->get_data();
|
||||
|
||||
// featured=1 for post_id 1,3,5 → 3 rows.
|
||||
$this->assertSame( '3', $response->get_headers()['X-WP-Total'] );
|
||||
foreach ( $data as $item ) {
|
||||
$this->assertSame( '1', (string) $item['rp_featured'] );
|
||||
}
|
||||
}
|
||||
|
||||
public function test_listings_order_asc(): void {
|
||||
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings' );
|
||||
$req->set_param( 'post_type', 'restapi' );
|
||||
$req->set_param( 'orderby', 'rp_price' );
|
||||
$req->set_param( 'order', 'ASC' );
|
||||
$req->set_param( 'per_page', 5 );
|
||||
|
||||
$response = self::$api->get_listings( $req );
|
||||
$data = $response->get_data();
|
||||
|
||||
$prices = array_column( $data, 'rp_price' );
|
||||
$sorted = $prices;
|
||||
sort( $sorted );
|
||||
$this->assertSame( $sorted, $prices );
|
||||
}
|
||||
|
||||
// ── GET /listings/{id} ───────────────────────────────────────────────────
|
||||
|
||||
public function test_get_listing_404_for_unknown_post(): void {
|
||||
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings/9999' );
|
||||
$req->set_param( 'id', 9999 );
|
||||
|
||||
$response = self::$api->get_listing( $req );
|
||||
$this->assertSame( 404, $response->get_status() );
|
||||
}
|
||||
|
||||
public function test_get_listing_merges_hot_and_postmeta_cold(): void {
|
||||
// post_id 1 is in hot table (rp_price=100); cold zone idle → postmeta fallback.
|
||||
$GLOBALS['_wp_post_types'][1] = 'restapi';
|
||||
$GLOBALS['_wp_postmeta'][1]['rp_description'] = 'Integration test';
|
||||
|
||||
$req = new WP_REST_Request( 'GET', '/wpdo/v1/listings/1' );
|
||||
$req->set_param( 'id', 1 );
|
||||
|
||||
$response = self::$api->get_listing( $req );
|
||||
$this->assertSame( 200, $response->get_status() );
|
||||
|
||||
$data = $response->get_data();
|
||||
$this->assertSame( 1, $data['id'] );
|
||||
$this->assertSame( '100.00', $data['rp_price'] );
|
||||
$this->assertSame( 'Integration test', $data['rp_description'] );
|
||||
}
|
||||
|
||||
// ── GET /stats/{id} ──────────────────────────────────────────────────────
|
||||
|
||||
public function test_get_stats_returns_zero_for_unknown_post(): void {
|
||||
$GLOBALS['_wp_post_types'][77] = 'restapi';
|
||||
|
||||
$req = new WP_REST_Request( 'GET', '/wpdo/v1/stats/77' );
|
||||
$req->set_param( 'id', 77 );
|
||||
|
||||
$response = self::$api->get_stats( $req );
|
||||
$this->assertSame( 200, $response->get_status() );
|
||||
|
||||
$data = $response->get_data();
|
||||
$this->assertSame( 77, $data['post_id'] );
|
||||
$this->assertSame( 0, $data['view_count'] );
|
||||
}
|
||||
|
||||
// ── GET /status ──────────────────────────────────────────────────────────
|
||||
|
||||
public function test_get_status_requires_manage_options(): void {
|
||||
$GLOBALS['_wp_current_user_can']['manage_options'] = false;
|
||||
$this->assertFalse( self::$api->require_manage_options() );
|
||||
}
|
||||
|
||||
public function test_get_status_returns_correct_engine(): void {
|
||||
$req = new WP_REST_Request( 'GET', '/wpdo/v1/status' );
|
||||
$response = self::$api->get_status( $req );
|
||||
|
||||
$this->assertSame( 200, $response->get_status() );
|
||||
$data = $response->get_data();
|
||||
$this->assertSame( 'mysql', $data['engine'] );
|
||||
$this->assertArrayHasKey( 'modules', $data );
|
||||
$this->assertSame( 'cutover', $data['modules']['hot_restapi'] );
|
||||
}
|
||||
|
||||
// ── POST /listings/{id}/view ──────────────────────────────────────────────
|
||||
|
||||
public function test_post_view_403_without_nonce(): void {
|
||||
$GLOBALS['_wp_post_types'][1] = 'restapi';
|
||||
$GLOBALS['_wp_valid_nonces'] = [];
|
||||
|
||||
$req = new WP_REST_Request( 'POST', '/wpdo/v1/listings/1/view' );
|
||||
$req->set_param( 'id', 1 );
|
||||
// No nonce.
|
||||
|
||||
$response = self::$api->post_view( $req );
|
||||
$this->assertSame( 403, $response->get_status() );
|
||||
}
|
||||
|
||||
public function test_post_view_404_for_unknown_post(): void {
|
||||
$nonce = wp_create_nonce( 'wp_rest' );
|
||||
|
||||
$req = new WP_REST_Request( 'POST', '/wpdo/v1/listings/8888/view' );
|
||||
$req->set_param( 'id', 8888 );
|
||||
$req->set_header( 'X-WP-Nonce', $nonce );
|
||||
|
||||
$response = self::$api->post_view( $req );
|
||||
$this->assertSame( 404, $response->get_status() );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user