@@ -1779,4 +1779,166 @@ public function test_collaboration_idle_poll_query_count(): void {
17791779 sprintf ( 'Idle poll should use at most 3 queries per room, used %d. ' , $ query_count )
17801780 );
17811781 }
1782+
1783+ /**
1784+ * Verifies that the collaboration_storage filter allows replacing the
1785+ * default storage backend with a custom implementation.
1786+ *
1787+ * @ticket 64696
1788+ */
1789+ public function test_collaboration_storage_filter_replaces_backend () {
1790+ wp_set_current_user ( self ::$ editor_id );
1791+
1792+ $ custom_storage = new WP_Test_In_Memory_Collaboration_Storage ();
1793+
1794+ add_filter (
1795+ 'collaboration_storage ' ,
1796+ static function () use ( $ custom_storage ) {
1797+ return $ custom_storage ;
1798+ }
1799+ );
1800+
1801+ // Reset the REST server so routes are re-registered with the filtered storage.
1802+ global $ wp_rest_server ;
1803+ $ wp_rest_server = null ;
1804+ rest_get_server ();
1805+
1806+ $ room = $ this ->get_post_room ();
1807+ $ update = array (
1808+ 'type ' => 'update ' ,
1809+ 'data ' => base64_encode ( 'custom-storage-test ' ),
1810+ );
1811+
1812+ // Client 1 sends an update.
1813+ $ response = $ this ->dispatch_collaboration (
1814+ array (
1815+ $ this ->build_room ( $ room , 1 , 0 , array ( 'user ' => 'c1 ' ), array ( $ update ) ),
1816+ )
1817+ );
1818+
1819+ $ this ->assertSame ( 200 , $ response ->get_status (), 'Custom storage should handle updates. ' );
1820+
1821+ // Client 2 polls and should receive client 1's update.
1822+ $ response = $ this ->dispatch_collaboration (
1823+ array (
1824+ $ this ->build_room ( $ room , 2 , 0 , array ( 'user ' => 'c2 ' ) ),
1825+ )
1826+ );
1827+
1828+ $ data = $ response ->get_data ();
1829+ $ update_data = wp_list_pluck ( $ data ['rooms ' ][0 ]['updates ' ], 'data ' );
1830+
1831+ $ this ->assertContains (
1832+ base64_encode ( 'custom-storage-test ' ),
1833+ $ update_data ,
1834+ 'Client 2 should receive the update stored by the custom backend. '
1835+ );
1836+
1837+ $ this ->assertTrue ( $ custom_storage ->was_used , 'The custom storage backend should have been called. ' );
1838+
1839+ remove_all_filters ( 'collaboration_storage ' );
1840+ }
1841+ }
1842+
1843+ /**
1844+ * In-memory collaboration storage for testing the collaboration_storage filter.
1845+ *
1846+ * Stores all data in arrays with no database interaction.
1847+ */
1848+ class WP_Test_In_Memory_Collaboration_Storage implements WP_Collaboration_Storage {
1849+ public $ was_used = false ;
1850+
1851+ private $ updates = array ();
1852+ private $ awareness = array ();
1853+ private $ next_id = 1 ;
1854+ private $ cursors = array ();
1855+ private $ counts = array ();
1856+
1857+ public function add_update ( string $ room , $ update ): bool {
1858+ $ this ->was_used = true ;
1859+ $ this ->updates [ $ room ][] = array (
1860+ 'id ' => $ this ->next_id ++,
1861+ 'update ' => $ update ,
1862+ );
1863+ return true ;
1864+ }
1865+
1866+ public function get_awareness_state ( string $ room , int $ timeout = 30 ): array {
1867+ $ this ->was_used = true ;
1868+ $ cutoff = time () - $ timeout ;
1869+ $ entries = array ();
1870+ foreach ( $ this ->awareness [ $ room ] ?? array () as $ entry ) {
1871+ if ( $ entry ['time ' ] >= $ cutoff ) {
1872+ $ entries [] = array (
1873+ 'client_id ' => $ entry ['client_id ' ],
1874+ 'state ' => $ entry ['state ' ],
1875+ 'wp_user_id ' => $ entry ['wp_user_id ' ],
1876+ );
1877+ }
1878+ }
1879+ return $ entries ;
1880+ }
1881+
1882+ public function get_cursor ( string $ room ): int {
1883+ return $ this ->cursors [ $ room ] ?? 0 ;
1884+ }
1885+
1886+ public function get_update_count ( string $ room ): int {
1887+ return $ this ->counts [ $ room ] ?? 0 ;
1888+ }
1889+
1890+ public function get_updates_after_cursor ( string $ room , int $ cursor ): array {
1891+ $ this ->was_used = true ;
1892+ $ updates = array ();
1893+ $ max_id = 0 ;
1894+ $ total = count ( $ this ->updates [ $ room ] ?? array () );
1895+
1896+ foreach ( $ this ->updates [ $ room ] ?? array () as $ entry ) {
1897+ if ( $ entry ['id ' ] > $ max_id ) {
1898+ $ max_id = $ entry ['id ' ];
1899+ }
1900+ if ( $ entry ['id ' ] > $ cursor ) {
1901+ $ updates [] = $ entry ['update ' ];
1902+ }
1903+ }
1904+
1905+ $ this ->cursors [ $ room ] = $ max_id ;
1906+ $ this ->counts [ $ room ] = $ total ;
1907+ return $ updates ;
1908+ }
1909+
1910+ public function remove_updates_before_cursor ( string $ room , int $ cursor ): bool {
1911+ if ( ! isset ( $ this ->updates [ $ room ] ) ) {
1912+ return true ;
1913+ }
1914+ $ this ->updates [ $ room ] = array_values (
1915+ array_filter (
1916+ $ this ->updates [ $ room ],
1917+ static function ( $ entry ) use ( $ cursor ) {
1918+ return $ entry ['id ' ] > $ cursor ;
1919+ }
1920+ )
1921+ );
1922+ return true ;
1923+ }
1924+
1925+ public function set_awareness_state ( string $ room , int $ client_id , array $ state , int $ wp_user_id ): bool {
1926+ $ this ->was_used = true ;
1927+ // Remove existing entry for this client.
1928+ $ this ->awareness [ $ room ] = array_values (
1929+ array_filter (
1930+ $ this ->awareness [ $ room ] ?? array (),
1931+ static function ( $ entry ) use ( $ client_id ) {
1932+ return $ entry ['client_id ' ] !== $ client_id ;
1933+ }
1934+ )
1935+ );
1936+ $ this ->awareness [ $ room ][] = array (
1937+ 'client_id ' => $ client_id ,
1938+ 'state ' => $ state ,
1939+ 'wp_user_id ' => $ wp_user_id ,
1940+ 'time ' => time (),
1941+ );
1942+ return true ;
1943+ }
17821944}
0 commit comments