From e3d48f238316708503fd8165b20ac4faf01c7ab6 Mon Sep 17 00:00:00 2001 From: Keith Solomon Date: Tue, 28 Apr 2026 13:00:31 -0500 Subject: [PATCH] feat: add rest package endpoints --- src/Rest/RestPackageController.php | 110 ++++++++++++ tests/Unit/Rest/RestPackageControllerTest.php | 163 ++++++++++++++++++ tests/bootstrap.php | 28 +++ 3 files changed, 301 insertions(+) create mode 100644 src/Rest/RestPackageController.php create mode 100644 tests/Unit/Rest/RestPackageControllerTest.php diff --git a/src/Rest/RestPackageController.php b/src/Rest/RestPackageController.php new file mode 100644 index 0000000..4e4dcd4 --- /dev/null +++ b/src/Rest/RestPackageController.php @@ -0,0 +1,110 @@ +validator = $validator; + } + + public function register(): void { + add_action( 'rest_api_init', array( $this, 'registerRoutes' ) ); + } + + public function registerRoutes(): void { + register_rest_route( + 'wp-content-sync/v1', + '/status', + array( + 'methods' => 'GET', + 'callback' => array( $this, 'status' ), + 'permission_callback' => array( $this, 'canReceivePackage' ), + ) + ); + + register_rest_route( + 'wp-content-sync/v1', + '/package', + array( + 'methods' => 'POST', + 'callback' => array( $this, 'receivePackage' ), + 'permission_callback' => array( $this, 'canReceivePackage' ), + ) + ); + } + + public function canReceivePackage(): bool { + return current_user_can( 'manage_options' ); + } + + /** + * @return array + */ + public function status(): array { + return array( + 'ok' => true, + 'plugin' => 'wp-content-sync', + 'version' => WPCS_VERSION, + ); + } + + /** + * @param mixed $request REST request or decoded request array. + * @return array + */ + public function receivePackage( $request ): array { + $data = $this->requestData( $request ); + + if ( ! isset( $data['package'] ) || ! is_array( $data['package'] ) ) { + return array( + 'accepted' => false, + 'errors' => array( 'package is required and must be an object.' ), + ); + } + + $result = $this->validator->validate( $data['package'] ); + + if ( ! $result->isValid() ) { + return array( + 'accepted' => false, + 'errors' => $result->errors(), + ); + } + + $package = ContentPackage::fromArray( $data['package'] ); + + return array( + 'accepted' => true, + 'schema_version' => $package->schemaVersion(), + 'manifest' => $package->manifest(), + ); + } + + /** + * @param mixed $request REST request or decoded request array. + * @return array + */ + private function requestData( $request ): array { + if ( is_array( $request ) ) { + return $request; + } + + if ( is_object( $request ) && method_exists( $request, 'get_json_params' ) ) { + $params = $request->get_json_params(); + + return is_array( $params ) ? $params : array(); + } + + return array(); + } +} diff --git a/tests/Unit/Rest/RestPackageControllerTest.php b/tests/Unit/Rest/RestPackageControllerTest.php new file mode 100644 index 0000000..646b6aa --- /dev/null +++ b/tests/Unit/Rest/RestPackageControllerTest.php @@ -0,0 +1,163 @@ +register(); + + self::assertSame( + array( $controller, 'registerRoutes' ), + $GLOBALS['wpcs_test_actions']['rest_api_init'][0] + ); + } + + public function test_it_registers_status_and_package_routes(): void { + $controller = new RestPackageController( new PackageValidator() ); + $controller->registerRoutes(); + + self::assertArrayHasKey( 'wp-content-sync/v1/status', $GLOBALS['wpcs_rest_routes'] ); + self::assertArrayHasKey( 'wp-content-sync/v1/package', $GLOBALS['wpcs_rest_routes'] ); + } + + public function test_it_requires_manage_options_permission(): void { + $GLOBALS['wpcs_current_user_can']['manage_options'] = false; + $controller = new RestPackageController( new PackageValidator() ); + + self::assertFalse( $controller->canReceivePackage() ); + } + + public function test_it_returns_status_payload(): void { + $controller = new RestPackageController( new PackageValidator() ); + + self::assertSame( + array( + 'ok' => true, + 'plugin' => 'wp-content-sync', + 'version' => WPCS_VERSION, + ), + $controller->status() + ); + } + + public function test_it_accepts_valid_packages(): void { + $controller = new RestPackageController( new PackageValidator() ); + + self::assertSame( + $this->acceptedResponse(), + $controller->receivePackage( + array( + 'package' => $this->validPackage(), + ) + ) + ); + } + + public function test_it_accepts_rest_request_like_objects(): void { + $controller = new RestPackageController( new PackageValidator() ); + $request = new class( + array( + 'package' => $this->validPackage(), + ) + ) { + /** @var array */ + private array $params; + + /** + * @param array $params Request params. + */ + public function __construct( array $params ) { + $this->params = $params; + } + + /** + * @return array + */ + public function get_json_params(): array { + return $this->params; + } + }; + + self::assertSame( $this->acceptedResponse(), $controller->receivePackage( $request ) ); + } + + public function test_it_rejects_invalid_package_shapes(): void { + $controller = new RestPackageController( new PackageValidator() ); + + self::assertSame( + array( + 'accepted' => false, + 'errors' => array( 'package is required and must be an object.' ), + ), + $controller->receivePackage( array() ) + ); + } + + /** + * @return array + */ + private function acceptedResponse(): array { + return array( + 'accepted' => true, + 'schema_version' => '1.0', + 'manifest' => array( + 'posts' => 0, + 'terms' => 0, + 'media' => 0, + 'custom_post_types' => 0, + ), + ); + } + + /** + * @return array + */ + private function validPackage(): array { + $records = array( + 'posts' => array(), + 'terms' => array(), + 'media' => array(), + 'custom_post_types' => array(), + ); + + return array( + 'schema_version' => '1.0', + 'generated_at' => '2026-04-28T12:00:00+00:00', + 'source' => array( + 'site_url' => 'https://example.test', + 'name' => 'Example', + ), + 'destination' => array( + 'site_url' => 'https://destination.test', + 'name' => 'Destination', + ), + 'manifest' => array( + 'posts' => 0, + 'terms' => 0, + 'media' => 0, + 'custom_post_types' => 0, + ), + 'records' => $records, + 'checksums' => array( + 'records' => PackageChecksum::records( $records ), + ), + ); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 7c30eea..d6903a4 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -466,6 +466,34 @@ if ( ! function_exists( 'is_wp_error' ) ) { } } +if ( ! function_exists( 'register_rest_route' ) ) { + /** + * Minimal REST route registrar for unit tests. + * + * @param string $rest_namespace REST namespace. + * @param string $route REST route. + * @param array $args Route arguments. + * @return bool + */ + function register_rest_route( $rest_namespace, $route, array $args ) { + $GLOBALS['wpcs_rest_routes'][ $rest_namespace . $route ] = $args; + + return true; + } +} + +if ( ! function_exists( 'rest_ensure_response' ) ) { + /** + * Minimal REST response wrapper for unit tests. + * + * @param mixed $response Response value. + * @return mixed + */ + function rest_ensure_response( $response ) { + return $response; + } +} + if ( ! function_exists( 'admin_url' ) ) { /** * Minimal admin URL helper for unit tests.