@@ -1817,4 +1817,121 @@ public function test_ability_invoked_action_fires_on_validation_failure() {
18171817
18181818 $ this ->assertSame ( 1 , $ action ->get_call_count (), 'wp_ability_invoked should fire before input validation failure. ' );
18191819 }
1820+
1821+ /**
1822+ * Tests that a `validate_callback` in an input schema is ignored.
1823+ *
1824+ * The REST API invokes a `validate_callback` per request argument, so it is a
1825+ * reasonable thing to expect here too — but abilities do not reuse that
1826+ * request-layer machinery, and a server-only PHP callback could not be honored
1827+ * by the clients that consume the schema anyway. Custom validation belongs in
1828+ * the `wp_ability_validate_input` filter.
1829+ *
1830+ * @ticket 64098
1831+ */
1832+ public function test_validate_input_ignores_schema_validate_callback () {
1833+ $ callback_invoked = false ;
1834+
1835+ $ args = array_merge (
1836+ self ::$ test_ability_properties ,
1837+ array (
1838+ 'input_schema ' => array (
1839+ 'type ' => 'string ' ,
1840+ 'validate_callback ' => static function () use ( &$ callback_invoked ) {
1841+ $ callback_invoked = true ;
1842+ return new WP_Error ( 'should_not_run ' , 'Schema validate_callback must not be invoked. ' );
1843+ },
1844+ ),
1845+ )
1846+ );
1847+
1848+ $ ability = new WP_Ability ( self ::$ test_ability_name , $ args );
1849+
1850+ // 'hello' satisfies the JSON Schema (type string); the validate_callback would
1851+ // reject every value if it were ever invoked.
1852+ $ result = $ ability ->validate_input ( 'hello ' );
1853+
1854+ $ this ->assertTrue ( $ result , 'Input should pass on JSON Schema alone. ' );
1855+ $ this ->assertFalse ( $ callback_invoked , 'Schema validate_callback must not run during input validation. ' );
1856+ }
1857+
1858+ /**
1859+ * Tests that a `validate_callback` in an output schema is ignored.
1860+ *
1861+ * Output is validated the same way as input, so the same reasoning applies: the
1862+ * schema callback never runs. Custom output validation belongs in the
1863+ * `wp_ability_validate_output` filter.
1864+ *
1865+ * @ticket 64098
1866+ */
1867+ public function test_validate_output_ignores_schema_validate_callback () {
1868+ $ callback_invoked = false ;
1869+
1870+ $ args = array_merge (
1871+ self ::$ test_ability_properties ,
1872+ array (
1873+ 'output_schema ' => array (
1874+ 'type ' => 'string ' ,
1875+ 'validate_callback ' => static function () use ( &$ callback_invoked ) {
1876+ $ callback_invoked = true ;
1877+ return new WP_Error ( 'should_not_run ' , 'Schema validate_callback must not be invoked. ' );
1878+ },
1879+ ),
1880+ 'execute_callback ' => static function (): string {
1881+ return 'result ' ;
1882+ },
1883+ )
1884+ );
1885+
1886+ $ ability = new WP_Ability ( self ::$ test_ability_name , $ args );
1887+
1888+ // The execute callback returns a valid string; the output validate_callback would
1889+ // reject it if it ran, so a returned result proves the callback was ignored.
1890+ $ result = $ ability ->execute ();
1891+
1892+ $ this ->assertSame ( 'result ' , $ result , 'Output should pass on JSON Schema alone, so execute() returns the result. ' );
1893+ $ this ->assertFalse ( $ callback_invoked , 'Schema validate_callback must not run during output validation. ' );
1894+ }
1895+
1896+ /**
1897+ * Tests that a `sanitize_callback` is ignored and input is never sanitized.
1898+ *
1899+ * REST cleans and type-coerces arguments in a sanitization step; abilities have
1900+ * no such step, so a `sanitize_callback` never runs and a mistyped value is
1901+ * rejected rather than coerced. This is the easiest REST assumption to carry
1902+ * over by mistake, so it is pinned explicitly.
1903+ *
1904+ * @ticket 64098
1905+ */
1906+ public function test_execute_ignores_schema_sanitize_callback () {
1907+ $ callback_invoked = false ;
1908+
1909+ $ args = array_merge (
1910+ self ::$ test_ability_properties ,
1911+ array (
1912+ 'input_schema ' => array (
1913+ 'type ' => 'string ' ,
1914+ 'sanitize_callback ' => static function ( $ value ) use ( &$ callback_invoked ) {
1915+ $ callback_invoked = true ;
1916+ return 'sanitized ' ;
1917+ },
1918+ ),
1919+ 'output_schema ' => array (
1920+ 'type ' => 'string ' ,
1921+ ),
1922+ 'execute_callback ' => static function ( $ input ): string {
1923+ return $ input ;
1924+ },
1925+ )
1926+ );
1927+
1928+ $ ability = new WP_Ability ( self ::$ test_ability_name , $ args );
1929+
1930+ // The execute callback echoes its input, so an unmodified return value proves
1931+ // the sanitize_callback never ran and no sanitization pass took place.
1932+ $ result = $ ability ->execute ( 'raw value ' );
1933+
1934+ $ this ->assertSame ( 'raw value ' , $ result , 'Input should reach the execute callback unmodified (no sanitization). ' );
1935+ $ this ->assertFalse ( $ callback_invoked , 'Schema sanitize_callback must not run. ' );
1936+ }
18201937}
0 commit comments