Changeset 62064 for trunk/tests/phpunit/tests/rest-api/rest-sync-server.php
- Timestamp:
- 03/19/2026 01:13:06 PM (4 months ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/tests/phpunit/tests/rest-api/rest-sync-server.php
r62058 r62064 335 335 $data = $response->get_data(); 336 336 $this->assertIsInt( $data['rooms'][0]['end_cursor'] ); 337 $this->assertGreaterThan ( 0, $data['rooms'][0]['end_cursor'] );337 $this->assertGreaterThanOrEqual( 0, $data['rooms'][0]['end_cursor'] ); 338 338 } 339 339 … … 564 564 $data = $response->get_data(); 565 565 $this->assertSame( 3, $data['rooms'][0]['total_updates'] ); 566 } 567 568 public function test_sync_cursor_does_not_skip_update_inserted_during_fetch_window() { 569 global $wpdb; 570 571 wp_set_current_user( self::$editor_id ); 572 573 $room = $this->get_post_room(); 574 $storage = new WP_Sync_Post_Meta_Storage(); 575 576 $seed_update = array( 577 'client_id' => 1, 578 'type' => 'update', 579 'data' => 'c2VlZA==', 580 ); 581 582 $this->assertTrue( $storage->add_update( $room, $seed_update ) ); 583 584 $initial_updates = $storage->get_updates_after_cursor( $room, 0 ); 585 $baseline_cursor = $storage->get_cursor( $room ); 586 587 $this->assertCount( 1, $initial_updates ); 588 $this->assertSame( $seed_update, $initial_updates[0] ); 589 $this->assertGreaterThan( 0, $baseline_cursor ); 590 591 $storage_posts = get_posts( 592 array( 593 'post_type' => WP_Sync_Post_Meta_Storage::POST_TYPE, 594 'posts_per_page' => 1, 595 'post_status' => 'publish', 596 'name' => md5( $room ), 597 'fields' => 'ids', 598 ) 599 ); 600 $storage_post_id = array_first( $storage_posts ); 601 602 $this->assertIsInt( $storage_post_id ); 603 604 $injected_update = array( 605 'client_id' => 9999, 606 'type' => 'update', 607 'data' => base64_encode( 'injected-during-fetch' ), 608 ); 609 610 $original_wpdb = $wpdb; 611 $proxy_wpdb = new class( $original_wpdb, $storage_post_id, $injected_update ) { 612 private $wpdb; 613 private $storage_post_id; 614 private $injected_update; 615 public $postmeta; 616 public $did_inject = false; 617 618 public function __construct( $wpdb, int $storage_post_id, array $injected_update ) { 619 $this->wpdb = $wpdb; 620 $this->storage_post_id = $storage_post_id; 621 $this->injected_update = $injected_update; 622 $this->postmeta = $wpdb->postmeta; 623 } 624 625 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Proxy forwards fully prepared core queries. 626 public function prepare( ...$args ) { 627 return $this->wpdb->prepare( ...$args ); 628 } 629 630 public function get_row( $query = null, $output = OBJECT, $y = 0 ) { 631 $result = $this->wpdb->get_row( $query, $output, $y ); 632 633 $this->maybe_inject_after_sync_query( $query ); 634 635 return $result; 636 } 637 638 public function get_var( $query = null, $x = 0, $y = 0 ) { 639 $result = $this->wpdb->get_var( $query, $x, $y ); 640 641 $this->maybe_inject_after_sync_query( $query ); 642 643 return $result; 644 } 645 646 public function get_results( $query = null, $output = OBJECT ) { 647 return $this->wpdb->get_results( $query, $output ); 648 } 649 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared 650 651 public function __call( $name, $arguments ) { 652 return $this->wpdb->$name( ...$arguments ); 653 } 654 655 public function __get( $name ) { 656 return $this->wpdb->$name; 657 } 658 659 public function __set( $name, $value ) { 660 $this->wpdb->$name = $value; 661 } 662 663 private function inject_update(): void { 664 if ( $this->did_inject ) { 665 return; 666 } 667 668 $this->did_inject = true; 669 670 add_post_meta( 671 $this->storage_post_id, 672 WP_Sync_Post_Meta_Storage::SYNC_UPDATE_META_KEY, 673 $this->injected_update, 674 false 675 ); 676 } 677 678 private function maybe_inject_after_sync_query( $query ): void { 679 if ( $this->did_inject || ! is_string( $query ) ) { 680 return; 681 } 682 683 $targets_postmeta = false !== strpos( $query, $this->postmeta ); 684 $targets_post_id = 1 === preg_match( '/\bpost_id\s*=\s*' . (int) $this->storage_post_id . '\b/', $query ); 685 $targets_meta_key = 1 === preg_match( 686 "/\bmeta_key\s*=\s*'" . preg_quote( WP_Sync_Post_Meta_Storage::SYNC_UPDATE_META_KEY, '/' ) . "'/", 687 $query 688 ); 689 690 if ( $targets_postmeta && $targets_post_id && $targets_meta_key ) { 691 $this->inject_update(); 692 } 693 } 694 }; 695 696 $wpdb = $proxy_wpdb; 697 try { 698 $race_updates = $storage->get_updates_after_cursor( $room, $baseline_cursor ); 699 $race_cursor = $storage->get_cursor( $room ); 700 } finally { 701 $wpdb = $original_wpdb; 702 } 703 704 $this->assertTrue( $proxy_wpdb->did_inject, 'Expected race-window update injection to occur.' ); 705 $this->assertEmpty( $race_updates ); 706 $this->assertSame( $baseline_cursor, $race_cursor ); 707 708 $follow_up_updates = $storage->get_updates_after_cursor( $room, $race_cursor ); 709 $follow_up_cursor = $storage->get_cursor( $room ); 710 711 $this->assertCount( 1, $follow_up_updates ); 712 $this->assertSame( $injected_update, $follow_up_updates[0] ); 713 $this->assertGreaterThan( $race_cursor, $follow_up_cursor ); 566 714 } 567 715 … … 707 855 } 708 856 857 public function test_sync_compaction_does_not_delete_update_inserted_during_delete() { 858 global $wpdb; 859 860 wp_set_current_user( self::$editor_id ); 861 862 $room = $this->get_post_room(); 863 $storage = new WP_Sync_Post_Meta_Storage(); 864 865 // Seed three updates so there's something to compact. 866 for ( $i = 1; $i <= 3; $i++ ) { 867 $this->assertTrue( 868 $storage->add_update( 869 $room, 870 array( 871 'client_id' => $i, 872 'type' => 'update', 873 'data' => base64_encode( "seed-$i" ), 874 ) 875 ) 876 ); 877 } 878 879 // Capture the cursor after all seeds are in place. 880 $storage->get_updates_after_cursor( $room, 0 ); 881 $compaction_cursor = $storage->get_cursor( $room ); 882 $this->assertGreaterThan( 0, $compaction_cursor ); 883 884 $storage_posts = get_posts( 885 array( 886 'post_type' => WP_Sync_Post_Meta_Storage::POST_TYPE, 887 'posts_per_page' => 1, 888 'post_status' => 'publish', 889 'name' => md5( $room ), 890 'fields' => 'ids', 891 ) 892 ); 893 $storage_post_id = array_first( $storage_posts ); 894 $this->assertIsInt( $storage_post_id ); 895 896 $concurrent_update = array( 897 'client_id' => 9999, 898 'type' => 'update', 899 'data' => base64_encode( 'arrived-during-compaction' ), 900 ); 901 902 $original_wpdb = $wpdb; 903 $proxy_wpdb = new class( $original_wpdb, $storage_post_id, $concurrent_update ) { 904 private $wpdb; 905 private $storage_post_id; 906 private $concurrent_update; 907 public $did_inject = false; 908 909 public function __construct( $wpdb, int $storage_post_id, array $concurrent_update ) { 910 $this->wpdb = $wpdb; 911 $this->storage_post_id = $storage_post_id; 912 $this->concurrent_update = $concurrent_update; 913 } 914 915 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Proxy forwards fully prepared core queries. 916 public function prepare( ...$args ) { 917 return $this->wpdb->prepare( ...$args ); 918 } 919 920 public function query( $query ) { 921 $result = $this->wpdb->query( $query ); 922 923 // After the DELETE executes, inject a concurrent update via 924 // raw SQL through the real $wpdb to avoid metadata cache 925 // interactions while the proxy is active. 926 if ( ! $this->did_inject 927 && is_string( $query ) 928 && 0 === strpos( $query, "DELETE FROM {$this->wpdb->postmeta}" ) 929 && false !== strpos( $query, "post_id = {$this->storage_post_id}" ) 930 ) { 931 $this->did_inject = true; 932 $this->wpdb->insert( 933 $this->wpdb->postmeta, 934 array( 935 'post_id' => $this->storage_post_id, 936 'meta_key' => WP_Sync_Post_Meta_Storage::SYNC_UPDATE_META_KEY, 937 'meta_value' => maybe_serialize( $this->concurrent_update ), 938 ), 939 array( '%d', '%s', '%s' ) 940 ); 941 } 942 943 return $result; 944 } 945 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared 946 947 public function __call( $name, $arguments ) { 948 return $this->wpdb->$name( ...$arguments ); 949 } 950 951 public function __get( $name ) { 952 return $this->wpdb->$name; 953 } 954 955 public function __set( $name, $value ) { 956 $this->wpdb->$name = $value; 957 } 958 }; 959 960 // Run compaction through the proxy so the concurrent update 961 // is injected immediately after the DELETE executes. 962 $wpdb = $proxy_wpdb; 963 try { 964 $result = $storage->remove_updates_before_cursor( $room, $compaction_cursor ); 965 } finally { 966 $wpdb = $original_wpdb; 967 } 968 969 $this->assertTrue( $result ); 970 $this->assertTrue( $proxy_wpdb->did_inject, 'Expected concurrent update injection to occur.' ); 971 972 // The concurrent update must survive the compaction delete. 973 $updates = $storage->get_updates_after_cursor( $room, 0 ); 974 975 $update_data = wp_list_pluck( $updates, 'data' ); 976 $this->assertContains( 977 $concurrent_update['data'], 978 $update_data, 979 'Concurrent update should survive compaction.' 980 ); 981 } 982 709 983 /* 710 984 * Awareness tests.
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)