Commit e889a5b3bf8 for woocommerce

commit e889a5b3bf819f03f6683c653482dae39863048e
Author: Hannah Tinkler <hannah.tinkler@gmail.com>
Date:   Thu Sep 24 19:00:12 2026 +0100

    Return 404 for push token writes when push notifications are disabled (#69058)

    * Return 404 for push token writes when push notifications are disabled

    Registering or removing a push token on a disabled store returned 403 after
    the token list moved ahead of the enablement check, and the apps expect a 404
    to recognise the store as unavailable. The write routes are now only
    registered while the module is enabled.

    * Note when the push token write routes can stop depending on enablement

    * Drop the unneeded on_init call from the disabled token route tests

diff --git a/plugins/woocommerce/src/Internal/PushNotifications/Controllers/PushTokenRestController.php b/plugins/woocommerce/src/Internal/PushNotifications/Controllers/PushTokenRestController.php
index 34752d3fb14..b5d36d00a53 100644
--- a/plugins/woocommerce/src/Internal/PushNotifications/Controllers/PushTokenRestController.php
+++ b/plugins/woocommerce/src/Internal/PushNotifications/Controllers/PushTokenRestController.php
@@ -64,6 +64,14 @@ class PushTokenRestController extends RestApiControllerBase {
 	/**
 	 * Register the REST API endpoints handled by this controller.
 	 *
+	 * The token list is registered whatever the module's state. The write
+	 * endpoints are only registered while it is enabled, because the apps read
+	 * the 404 for a missing route as push notifications being unavailable.
+	 * Once the apps read that from {@see PushNotificationStatusRestController}
+	 * instead, the write endpoints can be registered unconditionally again and
+	 * left to their permission callbacks. Registering a second handler on an
+	 * existing route adds to it.
+	 *
 	 * @since 10.6.0
 	 *
 	 * @return void
@@ -111,12 +119,6 @@ class PushTokenRestController extends RestApiControllerBase {
 						),
 					),
 				),
-				array(
-					'methods'             => WP_REST_Server::CREATABLE,
-					'callback'            => fn ( WP_REST_Request $request ) => $this->run( $request, 'create' ),
-					'args'                => $this->get_args( 'create' ),
-					'permission_callback' => array( $this, 'authorize_as_authenticated' ),
-				),
 				'schema' => fn () => array_merge(
 					$this->get_base_schema(),
 					array(
@@ -135,6 +137,23 @@ class PushTokenRestController extends RestApiControllerBase {
 			)
 		);

+		if ( ! wc_get_container()->get( PushNotifications::class )->should_be_enabled() ) {
+			return;
+		}
+
+		register_rest_route(
+			$this->route_namespace,
+			$this->rest_base,
+			array(
+				array(
+					'methods'             => WP_REST_Server::CREATABLE,
+					'callback'            => fn ( WP_REST_Request $request ) => $this->run( $request, 'create' ),
+					'args'                => $this->get_args( 'create' ),
+					'permission_callback' => array( $this, 'authorize_as_authenticated' ),
+				),
+			)
+		);
+
 		register_rest_route(
 			$this->route_namespace,
 			$this->rest_base . '/(?P<id>[\d]+)',
diff --git a/plugins/woocommerce/src/Internal/PushNotifications/PushNotifications.php b/plugins/woocommerce/src/Internal/PushNotifications/PushNotifications.php
index 9e7a8cf3970..048c9bd55c5 100644
--- a/plugins/woocommerce/src/Internal/PushNotifications/PushNotifications.php
+++ b/plugins/woocommerce/src/Internal/PushNotifications/PushNotifications.php
@@ -82,8 +82,8 @@ class PushNotifications {
 		wc_get_container()->get( UserDataCleanupService::class )->register();

 		// Registered ahead of the enablement check so the token list can still be
-		// read on a store that has been switched off. The write routes stay gated
-		// in their permission callbacks, as does everything below.
+		// read on a store that has been switched off. The controller registers the
+		// write routes only while the module is enabled, like everything below.
 		( new PushTokenRestController() )->register();

 		if ( ! $this->should_be_enabled() ) {
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
index b8521bf2c64..a80465b20c7 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Controllers/PushTokenRestControllerTest.php
@@ -120,7 +120,11 @@ class PushTokenRestControllerTest extends WC_Unit_Test_Case {
 	public function setUp(): void {
 		parent::setUp();

-		$this->reset_push_notifications_cache();
+		/**
+		 * The write routes are only registered on an enabled store. Tests that
+		 * need it disabled re-mock the connection, which also resets the cache.
+		 */
+		$this->mock_jetpack_connection_manager_is_connected();

 		$this->controller = new PushTokenRestController();
 		$this->server     = $this->create_rest_server_with_routes(
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/PushNotificationsTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/PushNotificationsTest.php
index 6bc751d6bb8..242ac845694 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/PushNotificationsTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/PushNotificationsTest.php
@@ -19,6 +19,7 @@ use WC_Logger;
 use WC_Unit_Test_Case;
 use WP_Http;
 use WP_REST_Request;
+use WP_REST_Response;

 /**
  * PushNotifications test.
@@ -262,8 +263,7 @@ class PushNotificationsTest extends WC_Unit_Test_Case {
 			->method( 'is_connected' )
 			->willReturn( false );

-		$push_notifications = new PushNotifications();
-		$push_notifications->on_init();
+		wc_get_container()->get( PushNotifications::class )->on_init();

 		$registered = $this->get_registered_rest_controllers();

@@ -329,34 +329,57 @@ class PushNotificationsTest extends WC_Unit_Test_Case {
 	}

 	/**
-	 * @testdox Tests that registering a token is refused while the module is disabled.
+	 * @testdox Tests that registering a token returns a missing route while the module is disabled.
+	 */
+	public function test_token_registration_returns_no_route_when_disabled_via_filter() {
+		$request = new WP_REST_Request( 'POST', '/wc-push-notifications/push-tokens' );
+		$request->set_param( 'token', str_repeat( 'a', 64 ) );
+		$request->set_param( 'platform', PushToken::PLATFORM_APPLE );
+		$request->set_param( 'device_uuid', 'refused-device-uuid' );
+		$request->set_param( 'origin', PushToken::ORIGIN_WOOCOMMERCE_IOS );
+		$request->set_param( 'device_locale', 'en_US' );
+
+		$response = $this->dispatch_as_shop_manager_while_disabled( $request );
+
+		$this->assertSame( WP_Http::NOT_FOUND, $response->get_status() );
+		$this->assertSame( 'rest_no_route', $response->get_data()['code'] );
+	}
+
+	/**
+	 * @testdox Tests that deleting a token returns a missing route while the module is disabled.
+	 */
+	public function test_token_deletion_returns_no_route_when_disabled_via_filter() {
+		$response = $this->dispatch_as_shop_manager_while_disabled(
+			new WP_REST_Request( 'DELETE', '/wc-push-notifications/push-tokens/1' )
+		);
+
+		$this->assertSame( WP_Http::NOT_FOUND, $response->get_status() );
+		$this->assertSame( 'rest_no_route', $response->get_data()['code'] );
+	}
+
+	/**
+	 * Dispatches a request as a shop manager against the token routes, with the
+	 * module disabled through the filter.
+	 *
+	 * @param WP_REST_Request $request The request to dispatch.
+	 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
+	 * @return WP_REST_Response
 	 */
-	public function test_token_registration_is_refused_when_disabled_via_filter() {
+	private function dispatch_as_shop_manager_while_disabled( WP_REST_Request $request ): WP_REST_Response {
 		add_filter( 'woocommerce_enhanced_push_notifications_disabled', '__return_true' );

 		try {
 			wp_set_current_user( self::factory()->user->create( array( 'role' => 'shop_manager' ) ) );

-			( new PushNotifications() )->on_init();
-
 			$server = $this->create_rest_server_with_routes(
 				array( array( new PushTokenRestController(), 'register_routes' ) ),
 				true
 			);

-			$request = new WP_REST_Request( 'POST', '/wc-push-notifications/push-tokens' );
-			$request->set_param( 'token', str_repeat( 'a', 64 ) );
-			$request->set_param( 'platform', PushToken::PLATFORM_APPLE );
-			$request->set_param( 'device_uuid', 'refused-device-uuid' );
-			$request->set_param( 'origin', PushToken::ORIGIN_WOOCOMMERCE_IOS );
-			$request->set_param( 'device_locale', 'en_US' );
-
-			$response = $server->dispatch( $request );
+			return $server->dispatch( $request );
 		} finally {
 			remove_filter( 'woocommerce_enhanced_push_notifications_disabled', '__return_true' );
 		}
-
-		$this->assertSame( WP_Http::FORBIDDEN, $response->get_status() );
 	}

 	/**