Commit 2f3f83a1d97 for woocommerce

commit 2f3f83a1d97d8b01a4858e563c475b990f081dc2
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date:   Fri Oct 9 11:33:31 2026 +0300

    Add tests for custom shipping provider AJAX handler and order filter (#69589)

diff --git a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
index c0f5712c61d..b7536ca0986 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-ajax-test.php
@@ -7,6 +7,7 @@

 declare( strict_types = 1 );

+use Automattic\WooCommerce\Admin\Features\Fulfillments\FulfillmentsController;
 use Automattic\WooCommerce\Enums\OrderStatus;
 use Automattic\WooCommerce\Internal\Orders\CouponsController;
 use Automattic\WooCommerce\Internal\Orders\TaxesController;
@@ -17,6 +18,11 @@ use Automattic\WooCommerce\Proxies\LegacyProxy;
  */
 class WC_AJAX_Test extends \WP_Ajax_UnitTestCase {

+	/**
+	 * Taxonomy that stores custom shipping providers.
+	 */
+	private const PROVIDER_TAXONOMY = 'wc_fulfillment_shipping_provider';
+
 	/**
 	 * Sets up the test fixture.
 	 */
@@ -3026,6 +3032,445 @@ class WC_AJAX_Test extends \WP_Ajax_UnitTestCase {
 		}
 	}

+	/**
+	 * @testdox Saving shipping providers is rejected when the fulfillments feature is disabled.
+	 */
+	public function test_shipping_providers_save_changes_rejects_when_feature_disabled(): void {
+		update_option( 'woocommerce_feature_fulfillments_enabled', 'no' );
+		$this->_setRole( 'administrator' );
+
+		$_POST    = array(
+			'wc_shipping_providers_nonce' => wp_create_nonce( 'wc_shipping_providers_nonce' ),
+			'changes'                     => array(),
+		);
+		$_REQUEST = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Test installs the real nonce immediately above.
+
+		$response = $this->do_ajax( 'woocommerce_shipping_providers_save_changes' );
+
+		$this->assertFalse( $response['success'] ?? true, 'A disabled feature should reject the request.' );
+		$this->assertSame( 'feature_disabled', $response['data'] ?? null );
+	}
+
+	/**
+	 * @testdox Saving shipping providers is rejected when the nonce or changes payload is missing.
+	 */
+	public function test_shipping_providers_save_changes_rejects_missing_fields(): void {
+		$this->enable_fulfillments_feature();
+		$this->_setRole( 'administrator' );
+
+		// Nonce present, but the changes payload is absent.
+		$_POST    = array(
+			'wc_shipping_providers_nonce' => wp_create_nonce( 'wc_shipping_providers_nonce' ),
+		);
+		$_REQUEST = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Test installs the real nonce immediately above.
+
+		$response = $this->do_ajax( 'woocommerce_shipping_providers_save_changes' );
+
+		$this->assertFalse( $response['success'] ?? true, 'A missing changes payload should reject the request.' );
+		$this->assertSame( 'missing_fields', $response['data'] ?? null );
+	}
+
+	/**
+	 * @testdox Saving shipping providers is rejected when the nonce is invalid.
+	 */
+	public function test_shipping_providers_save_changes_rejects_bad_nonce(): void {
+		$this->enable_fulfillments_feature();
+		$this->_setRole( 'administrator' );
+
+		$_POST    = array(
+			'wc_shipping_providers_nonce' => 'not-a-valid-nonce',
+			'changes'                     => array(),
+		);
+		$_REQUEST = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Test asserts the invalid nonce is rejected.
+
+		$response = $this->do_ajax( 'woocommerce_shipping_providers_save_changes' );
+
+		$this->assertFalse( $response['success'] ?? true, 'An invalid nonce should reject the request.' );
+		$this->assertSame( 'bad_nonce', $response['data'] ?? null );
+	}
+
+	/**
+	 * @testdox Saving shipping providers is rejected for a user without the manage_woocommerce capability.
+	 */
+	public function test_shipping_providers_save_changes_rejects_without_capability(): void {
+		$this->enable_fulfillments_feature();
+
+		// A customer passes the nonce check (it is tied to the current user) but lacks manage_woocommerce.
+		$this->_setRole( 'customer' );
+
+		$_POST    = array(
+			'wc_shipping_providers_nonce' => wp_create_nonce( 'wc_shipping_providers_nonce' ),
+			'changes'                     => array(),
+		);
+		$_REQUEST = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Test installs the real nonce immediately above.
+
+		$response = $this->do_ajax( 'woocommerce_shipping_providers_save_changes' );
+
+		$this->assertFalse( $response['success'] ?? true, 'A user without manage_woocommerce should be rejected.' );
+		$this->assertSame( 'missing_capabilities', $response['data'] ?? null );
+	}
+
+	/**
+	 * @testdox The nonce is checked before the capability, so a bad nonce is rejected even without manage_woocommerce.
+	 */
+	public function test_shipping_providers_save_changes_checks_nonce_before_capability(): void {
+		$this->enable_fulfillments_feature();
+		$this->_setRole( 'customer' );
+
+		$_POST    = array(
+			'wc_shipping_providers_nonce' => 'not-a-valid-nonce',
+			'changes'                     => array(),
+		);
+		$_REQUEST = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Test asserts the invalid nonce is rejected.
+
+		$response = $this->do_ajax( 'woocommerce_shipping_providers_save_changes' );
+
+		$this->assertFalse( $response['success'] ?? true, 'A bad nonce should reject the request.' );
+		$this->assertSame( 'bad_nonce', $response['data'] ?? null, 'The nonce guard runs before the capability guard.' );
+	}
+
+	/**
+	 * @testdox Saving shipping providers is rejected when the changes payload is not an array.
+	 */
+	public function test_shipping_providers_save_changes_rejects_non_array_changes(): void {
+		$this->enable_fulfillments_feature();
+		$this->_setRole( 'administrator' );
+
+		$_POST    = array(
+			'wc_shipping_providers_nonce' => wp_create_nonce( 'wc_shipping_providers_nonce' ),
+			'changes'                     => 'not-an-array',
+		);
+		$_REQUEST = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Test installs the real nonce immediately above.
+
+		$response = $this->do_ajax( 'woocommerce_shipping_providers_save_changes' );
+
+		$this->assertFalse( $response['success'] ?? true, 'A non-array changes payload should reject the request.' );
+		$this->assertSame( 'invalid_changes', $response['data'] ?? null );
+	}
+
+	/**
+	 * @testdox Creating a new custom provider persists its term, slug, and URL meta, and returns it in the response.
+	 */
+	public function test_shipping_providers_save_changes_creates_new_provider(): void {
+		$this->enable_fulfillments_feature();
+		$name          = 'Acme Couriers ' . wp_unique_id();
+		$expected_slug = sanitize_title( $name );
+
+		$response = $this->post_provider_changes(
+			array(
+				'new-row' => array(
+					'newRow'                => true,
+					'name'                  => $name,
+					'tracking_url_template' => 'https://acme.test/track/__PLACEHOLDER__',
+					'icon'                  => 'https://acme.test/logo.png',
+				),
+			)
+		);
+
+		$this->assertTrue( $response['success'] ?? false, 'Creating a provider should succeed.' );
+		$this->assertArrayNotHasKey( 'error', $response['data'], 'A valid creation should not return an error.' );
+
+		$term = get_term_by( 'slug', $expected_slug, self::PROVIDER_TAXONOMY );
+		$this->assertInstanceOf( WP_Term::class, $term, 'The provider term should be created.' );
+		if ( ! $term instanceof WP_Term ) {
+			throw new RuntimeException( 'The provider term could not be reloaded.' );
+		}
+
+		$this->assertSame( $name, $term->name );
+		$this->assertSame( 'https://acme.test/track/__PLACEHOLDER__', get_term_meta( $term->term_id, 'tracking_url_template', true ) );
+		$this->assertSame( 'https://acme.test/logo.png', get_term_meta( $term->term_id, 'icon', true ) );
+
+		$matching_rows = array_values(
+			array_filter(
+				$response['data']['shipping_providers'] ?? array(),
+				static fn ( array $row ): bool => (int) ( $row['term_id'] ?? 0 ) === $term->term_id
+			)
+		);
+		$this->assertCount( 1, $matching_rows, 'The response should contain the new provider exactly once.' );
+		$this->assertSame( $expected_slug, $matching_rows[0]['slug'] );
+		$this->assertSame( $name, $matching_rows[0]['name'] );
+	}
+
+	/**
+	 * @testdox A new provider whose explicit slug matches a built-in provider key is rejected and not created.
+	 */
+	public function test_shipping_providers_save_changes_rejects_explicit_builtin_slug(): void {
+		$this->enable_fulfillments_feature();
+
+		$response = $this->post_provider_changes(
+			array(
+				'new-row' => array(
+					'newRow' => true,
+					'name'   => 'Pretend Amazon',
+					'slug'   => 'amazon-logistics',
+				),
+			)
+		);
+
+		$this->assertTrue( $response['success'] ?? false, 'The handler reports validation problems via the error field, not a failed request.' );
+		$this->assertStringContainsString( 'built-in shipping provider', $response['data']['error'] ?? '' );
+		$this->assertFalse( get_term_by( 'name', 'Pretend Amazon', self::PROVIDER_TAXONOMY ), 'No term should be created for a reserved slug.' );
+	}
+
+	/**
+	 * @testdox A new provider whose auto-generated slug collides with a built-in key is rolled back and reported.
+	 */
+	public function test_shipping_providers_save_changes_rejects_autogenerated_builtin_slug(): void {
+		$this->enable_fulfillments_feature();
+
+		// No explicit slug: sanitize_title( 'Amazon Logistics' ) === 'amazon-logistics', a built-in key.
+		$response = $this->post_provider_changes(
+			array(
+				'new-row' => array(
+					'newRow' => true,
+					'name'   => 'Amazon Logistics',
+				),
+			)
+		);
+
+		$this->assertTrue( $response['success'] ?? false, 'The handler reports validation problems via the error field, not a failed request.' );
+		$this->assertStringContainsString( 'auto-generated slug conflicts', $response['data']['error'] ?? '' );
+		$this->assertFalse( get_term_by( 'slug', 'amazon-logistics', self::PROVIDER_TAXONOMY ), 'The colliding term should be deleted after detection.' );
+	}
+
+	/**
+	 * @testdox Updating an existing provider changes its name but leaves the slug immutable.
+	 */
+	public function test_shipping_providers_save_changes_slug_is_immutable_on_update(): void {
+		$this->enable_fulfillments_feature();
+		$term_id = $this->create_custom_provider_term( 'Original Name', 'original-slug' );
+
+		$response = $this->post_provider_changes(
+			array(
+				(string) $term_id => array(
+					'name' => 'Renamed Provider',
+					'slug' => 'hacked-slug',
+				),
+			)
+		);
+
+		$this->assertTrue( $response['success'] ?? false, 'Updating a provider should succeed.' );
+
+		$term = get_term( $term_id, self::PROVIDER_TAXONOMY );
+		$this->assertInstanceOf( WP_Term::class, $term );
+		$this->assertSame( 'Renamed Provider', $term->name, 'The name should be updated.' );
+		$this->assertSame( 'original-slug', $term->slug, 'The slug should be ignored on update.' );
+	}
+
+	/**
+	 * @testdox Deleting a provider referenced by a fulfillment is blocked and reported.
+	 */
+	public function test_shipping_providers_save_changes_blocks_deleting_provider_in_use(): void {
+		$this->enable_fulfillments_feature();
+		$term_id = $this->create_custom_provider_term( 'In Use Provider', 'in-use-provider' );
+		$this->seed_fulfillment_for_provider( 'in-use-provider' );
+
+		$response = $this->post_provider_changes(
+			array(
+				(string) $term_id => array( 'deleted' => true ),
+			)
+		);
+
+		$this->assertTrue( $response['success'] ?? false, 'The handler reports the block via the error field, not a failed request.' );
+		$this->assertStringContainsString( 'used by existing fulfillments', $response['data']['error'] ?? '' );
+		$this->assertInstanceOf( WP_Term::class, get_term( $term_id, self::PROVIDER_TAXONOMY ), 'The in-use provider should not be deleted.' );
+	}
+
+	/**
+	 * @testdox Deleting a provider that no fulfillment references removes the term.
+	 */
+	public function test_shipping_providers_save_changes_deletes_unused_provider(): void {
+		$this->enable_fulfillments_feature();
+		$term_id = $this->create_custom_provider_term( 'Unused Provider', 'unused-provider' );
+
+		$response = $this->post_provider_changes(
+			array(
+				(string) $term_id => array( 'deleted' => true ),
+			)
+		);
+
+		$this->assertTrue( $response['success'] ?? false, 'Deleting an unused provider should succeed.' );
+		$this->assertArrayNotHasKey( 'error', $response['data'], 'Deleting an unused provider should not report an error.' );
+		$this->assertNull( get_term( $term_id, self::PROVIDER_TAXONOMY ), 'The unused provider should be deleted.' );
+	}
+
+	/**
+	 * @testdox An invalid tracking URL is rejected and the existing value is preserved.
+	 * @testWith ["javascript:alert(document.domain)"]
+	 *           ["ftp://example.test/track/__PLACEHOLDER__"]
+	 *           ["not-a-url"]
+	 *
+	 * @param string $invalid_url The rejected tracking URL template.
+	 */
+	public function test_shipping_providers_save_changes_rejects_invalid_tracking_url( string $invalid_url ): void {
+		$this->enable_fulfillments_feature();
+		$term_id = $this->create_custom_provider_term(
+			'URL Provider',
+			'url-provider',
+			array( 'tracking_url_template' => 'https://existing.test/track/__PLACEHOLDER__' )
+		);
+
+		$response = $this->post_provider_changes(
+			array(
+				(string) $term_id => array( 'tracking_url_template' => $invalid_url ),
+			)
+		);
+
+		$this->assertTrue( $response['success'] ?? false, 'The handler reports URL problems via the error field, not a failed request.' );
+		$this->assertStringContainsString( 'valid HTTP or HTTPS URL', $response['data']['error'] ?? '' );
+		$this->assertSame(
+			'https://existing.test/track/__PLACEHOLDER__',
+			get_term_meta( $term_id, 'tracking_url_template', true ),
+			'A rejected URL should leave the stored value untouched.'
+		);
+	}
+
+	/**
+	 * @testdox An empty tracking URL clears the stored value.
+	 */
+	public function test_shipping_providers_save_changes_clears_tracking_url_with_empty_string(): void {
+		$this->enable_fulfillments_feature();
+		$term_id = $this->create_custom_provider_term(
+			'Clearable Provider',
+			'clearable-provider',
+			array( 'tracking_url_template' => 'https://existing.test/track/__PLACEHOLDER__' )
+		);
+
+		$response = $this->post_provider_changes(
+			array(
+				(string) $term_id => array( 'tracking_url_template' => '' ),
+			)
+		);
+
+		$this->assertTrue( $response['success'] ?? false, 'Clearing a tracking URL should succeed.' );
+		$this->assertSame( '', get_term_meta( $term_id, 'tracking_url_template', true ), 'An empty string should clear the stored URL.' );
+	}
+
+	/**
+	 * @testdox A wp_insert_term failure while creating a provider is surfaced in the response error.
+	 */
+	public function test_shipping_providers_save_changes_surfaces_insert_term_error(): void {
+		$this->enable_fulfillments_feature();
+
+		$fail_insert = static function () {
+			return new WP_Error( 'insert_failed', 'Could not insert the term.' );
+		};
+		add_filter( 'pre_insert_term', $fail_insert );
+
+		try {
+			$response = $this->post_provider_changes(
+				array(
+					'new-row' => array(
+						'newRow' => true,
+						'name'   => 'Doomed Provider',
+					),
+				)
+			);
+		} finally {
+			remove_filter( 'pre_insert_term', $fail_insert );
+		}
+
+		$this->assertTrue( $response['success'] ?? false, 'The handler reports insert failures via the error field, not a failed request.' );
+		$this->assertSame( 'Could not insert the term.', $response['data']['error'] ?? '' );
+		$this->assertFalse( get_term_by( 'name', 'Doomed Provider', self::PROVIDER_TAXONOMY ), 'No term should remain after an insert failure.' );
+	}
+
+	/**
+	 * @testdox A wp_update_term failure for an unknown term id is surfaced in the response error.
+	 */
+	public function test_shipping_providers_save_changes_surfaces_update_term_error(): void {
+		$this->enable_fulfillments_feature();
+
+		// A numeric, non-newRow key for a term that does not exist makes wp_update_term return a WP_Error.
+		$response = $this->post_provider_changes(
+			array(
+				'999999' => array( 'name' => 'Ghost Provider' ),
+			)
+		);
+
+		$this->assertTrue( $response['success'] ?? false, 'The handler reports update failures via the error field, not a failed request.' );
+		$this->assertNotEmpty( $response['data']['error'] ?? '', 'An update failure should be reported in the error field.' );
+		$this->assertFalse( get_term_by( 'name', 'Ghost Provider', self::PROVIDER_TAXONOMY ), 'A failed update should not create a term.' );
+	}
+
+	/**
+	 * Enable the fulfillments feature and register its taxonomy and classes.
+	 */
+	private function enable_fulfillments_feature(): void {
+		update_option( 'woocommerce_feature_fulfillments_enabled', 'yes' );
+		wc_get_container()->get( FulfillmentsController::class )->initialize_fulfillments();
+	}
+
+	/**
+	 * Build the request payload with a valid admin nonce and fire the save-changes action.
+	 *
+	 * @param array $changes The changes payload to submit.
+	 * @return array|null The decoded AJAX response.
+	 */
+	private function post_provider_changes( array $changes ) {
+		$this->_setRole( 'administrator' );
+
+		$_POST    = array(
+			'wc_shipping_providers_nonce' => wp_create_nonce( 'wc_shipping_providers_nonce' ),
+			'changes'                     => $changes,
+		);
+		$_REQUEST = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Test installs the real nonce immediately above.
+
+		return $this->do_ajax( 'woocommerce_shipping_providers_save_changes' );
+	}
+
+	/**
+	 * Create a custom shipping provider term with optional meta.
+	 *
+	 * @param string               $name The provider name.
+	 * @param string               $slug The provider slug.
+	 * @param array<string, mixed> $meta Term meta to set after creation.
+	 * @return int The created term id.
+	 */
+	private function create_custom_provider_term( string $name, string $slug, array $meta = array() ): int {
+		$term = wp_insert_term( $name, self::PROVIDER_TAXONOMY, array( 'slug' => $slug ) );
+		$this->assertIsArray( $term, 'The fixture provider term should be created.' );
+
+		$term_id = (int) $term['term_id'];
+		foreach ( $meta as $key => $value ) {
+			update_term_meta( $term_id, $key, $value );
+		}
+
+		return $term_id;
+	}
+
+	/**
+	 * Insert a non-deleted fulfillment that references a provider slug, as the in-use check expects.
+	 *
+	 * @param string $provider_slug The provider slug the fulfillment references.
+	 */
+	private function seed_fulfillment_for_provider( string $provider_slug ): void {
+		global $wpdb;
+		$now = current_time( 'mysql', true );
+
+		$wpdb->insert(
+			$wpdb->prefix . 'wc_order_fulfillments',
+			array(
+				'entity_type'  => WC_Order::class,
+				'entity_id'    => 1,
+				'status'       => 'unfulfilled',
+				'is_fulfilled' => 0,
+				'date_updated' => $now,
+			)
+		);
+
+		$wpdb->insert(
+			$wpdb->prefix . 'wc_order_fulfillment_meta',
+			// Column names of a custom fulfillments table, not a slow postmeta query.
+			array(
+				'fulfillment_id' => (int) $wpdb->insert_id,
+				'meta_key'       => '_shipment_provider', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+				'meta_value'     => wp_json_encode( $provider_slug ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
+				'date_updated'   => $now,
+			)
+		);
+	}
+
 	/**
 	 * Does the 'hard work' of triggering an ajax endpoint and capturing the response.
 	 *
diff --git a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentsRendererTest.php b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentsRendererTest.php
index b7e0bdd3492..af19ae074b5 100644
--- a/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentsRendererTest.php
+++ b/plugins/woocommerce/tests/php/src/Admin/Features/Fulfillments/FulfillmentsRendererTest.php
@@ -6,10 +6,12 @@ use Automattic\WooCommerce\Admin\Features\Fulfillments\DataStore\FulfillmentsDat
 use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
 use Automattic\WooCommerce\Admin\Features\Fulfillments\Fulfillment;
 use Automattic\WooCommerce\Admin\Features\Fulfillments\FulfillmentsRenderer;
+use Automattic\WooCommerce\Admin\Features\Fulfillments\Providers\AmazonLogisticsShippingProvider;
 use Automattic\WooCommerce\RestApi\UnitTests\Helpers\OrderHelper;
 use WC_Helper_Order;
 use WC_Helper_Product;
 use WC_Order;
+use WP_Query;

 /**
  * Tests for Fulfillment object.
@@ -392,4 +394,284 @@ class FulfillmentsRendererTest extends \WC_Unit_Test_Case {
 		$this->assertStringContainsString( 'wc-admin-fulfillments-js', $output );
 		$this->assertStringContainsString( 'var wcFulfillmentSettings', $output );
 	}
+
+	/**
+	 * @testdox Filtering by a specific provider returns only the orders whose fulfillment uses it.
+	 */
+	public function test_get_order_ids_matches_specific_provider(): void {
+		$this->seed_fulfillment( 101, 'acme-couriers' );
+		$this->seed_fulfillment( 102, 'other-co' );
+
+		$this->assertSame( array( 101 ), $this->get_order_ids( 'acme-couriers' ) );
+	}
+
+	/**
+	 * @testdox The filter finds an order whose provider was saved through the fulfillment CRUD path.
+	 */
+	public function test_get_order_ids_matches_provider_saved_through_crud(): void {
+		$order = WC_Helper_Order::create_order( get_current_user_id() );
+
+		$fulfillment = new Fulfillment();
+		$fulfillment->set_entity_type( WC_Order::class );
+		$fulfillment->set_entity_id( (string) $order->get_id() );
+		$fulfillment->set_shipment_provider( 'acme-couriers' );
+		$fulfillment->set_items(
+			array(
+				array(
+					'item_id' => 1,
+					'qty'     => 1,
+				),
+			)
+		);
+		$fulfillment->set_status( 'unfulfilled' );
+		$fulfillment->save();
+
+		$this->assertSame(
+			array( $order->get_id() ),
+			$this->get_order_ids( 'acme-couriers' ),
+			'The filter must match the provider meta the data store actually persists.'
+		);
+
+		WC_Helper_Order::delete_order( $order->get_id() );
+	}
+
+	/**
+	 * @testdox A soft-deleted fulfillment is excluded from the provider filter.
+	 */
+	public function test_get_order_ids_excludes_soft_deleted_fulfillment(): void {
+		$this->seed_fulfillment( 103, 'acme-couriers' );
+		$this->seed_fulfillment( 104, 'acme-couriers', true );
+
+		$this->assertSame( array( 103 ), $this->get_order_ids( 'acme-couriers' ) );
+	}
+
+	/**
+	 * @testdox A fulfillment whose provider meta row is soft-deleted is excluded from the filter.
+	 */
+	public function test_get_order_ids_excludes_soft_deleted_meta(): void {
+		$this->seed_fulfillment( 105, 'acme-couriers' );
+		$this->seed_fulfillment( 106, 'acme-couriers', false, true );
+
+		$this->assertSame( array( 105 ), $this->get_order_ids( 'acme-couriers' ) );
+	}
+
+	/**
+	 * @testdox The __other__ sentinel returns orders whose provider is not a known built-in or custom key.
+	 */
+	public function test_get_order_ids_other_excludes_known_providers(): void {
+		// Register a known provider so the query uses the NOT IN branch rather than the
+		// empty-known-keys fallback, which would otherwise return every providered order.
+		$register_known = function ( array $providers ): array {
+			$providers[] = AmazonLogisticsShippingProvider::class;
+			return $providers;
+		};
+		add_filter( 'woocommerce_fulfillment_shipping_providers', $register_known );
+
+		try {
+			$this->seed_fulfillment( 201, 'amazon-logistics' );
+			$this->seed_fulfillment( 202, 'ghost-provider' );
+
+			$this->assertSame( array( 202 ), $this->get_order_ids( '__other__' ) );
+		} finally {
+			remove_filter( 'woocommerce_fulfillment_shipping_providers', $register_known );
+		}
+	}
+
+	/**
+	 * @testdox The HPOS orders query is filtered to the matching order ids when no post__in exists yet.
+	 */
+	public function test_filter_orders_sets_post_in_when_absent(): void {
+		$this->seed_fulfillment( 301, 'acme-couriers' );
+
+		$this->with_provider_param(
+			'acme-couriers',
+			function () {
+				$args = $this->renderer->filter_orders_by_shipping_provider( array() );
+				$this->assertSame( array( 301 ), $args['post__in'] );
+			}
+		);
+	}
+
+	/**
+	 * @testdox The HPOS filter intersects an existing post__in with the provider matches.
+	 */
+	public function test_filter_orders_intersects_existing_post_in(): void {
+		$this->seed_fulfillment( 401, 'acme-couriers' );
+		$this->seed_fulfillment( 402, 'acme-couriers' );
+
+		$this->with_provider_param(
+			'acme-couriers',
+			function () {
+				$args = $this->renderer->filter_orders_by_shipping_provider( array( 'post__in' => array( 401, 999 ) ) );
+				$this->assertSame( array( 401 ), array_values( $args['post__in'] ), 'Only the id present in both sets should remain.' );
+			}
+		);
+	}
+
+	/**
+	 * @testdox The HPOS filter returns a no-match sentinel when the provider has no orders.
+	 */
+	public function test_filter_orders_returns_zero_when_no_match(): void {
+		$this->with_provider_param(
+			'provider-with-no-orders',
+			function () {
+				$args = $this->renderer->filter_orders_by_shipping_provider( array() );
+				$this->assertSame( array( 0 ), $args['post__in'], 'An unmatched provider should force an empty result set.' );
+			}
+		);
+	}
+
+	/**
+	 * @testdox The HPOS filter leaves the query arguments untouched when no provider is requested.
+	 */
+	public function test_filter_orders_is_noop_without_param(): void {
+		$original = array( 'post__in' => array( 7, 8 ) );
+
+		$this->assertSame( $original, $this->renderer->filter_orders_by_shipping_provider( $original ) );
+	}
+
+	/**
+	 * @testdox The legacy orders query is filtered to the matching order ids.
+	 */
+	public function test_filter_legacy_orders_sets_post_in(): void {
+		$this->seed_fulfillment( 501, 'acme-couriers' );
+
+		$query = new WP_Query();
+		$query->set( 'post_type', 'shop_order' );
+
+		$this->with_main_query(
+			$query,
+			function () use ( $query ) {
+				$this->with_provider_param(
+					'acme-couriers',
+					function () use ( $query ) {
+						$this->renderer->filter_legacy_orders_by_shipping_provider( $query );
+						$this->assertSame( array( 501 ), $query->get( 'post__in' ) );
+					}
+				);
+			}
+		);
+	}
+
+	/**
+	 * @testdox The legacy filter does nothing when the query is not the admin main orders query.
+	 */
+	public function test_filter_legacy_orders_skips_non_main_query(): void {
+		$this->seed_fulfillment( 601, 'acme-couriers' );
+
+		// A secondary query: not registered as the main query, so the guard must bail.
+		$query = new WP_Query();
+		$query->set( 'post_type', 'shop_order' );
+
+		// Set the admin screen so is_admin() passes and the bail is isolated to the is_main_query() guard.
+		$original_screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
+		set_current_screen( 'edit-shop_order' );
+
+		try {
+			$this->with_provider_param(
+				'acme-couriers',
+				function () use ( $query ) {
+					$this->renderer->filter_legacy_orders_by_shipping_provider( $query );
+					$this->assertEmpty( $query->get( 'post__in' ), 'A non-main query should not be filtered.' );
+				}
+			);
+		} finally {
+			if ( $original_screen ) {
+				$GLOBALS['current_screen'] = $original_screen; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+			} else {
+				unset( $GLOBALS['current_screen'] );
+			}
+		}
+	}
+
+	/**
+	 * Invoke the private provider-to-order-ids query.
+	 *
+	 * @param string $provider The provider key or the __other__ sentinel.
+	 * @return array<int> The matching order ids.
+	 */
+	private function get_order_ids( string $provider ): array {
+		$method = new \ReflectionMethod( FulfillmentsRenderer::class, 'get_order_ids_by_shipping_provider' );
+		$method->setAccessible( true );
+
+		return $method->invoke( $this->renderer, $provider );
+	}
+
+	/**
+	 * Run a callback with the shipping_provider request parameter set, then restore it.
+	 *
+	 * @param string   $provider The provider value to place in the request.
+	 * @param callable $callback The assertions to run while the parameter is set.
+	 */
+	private function with_provider_param( string $provider, callable $callback ): void {
+		$_GET['shipping_provider'] = $provider;
+		try {
+			$callback();
+		} finally {
+			unset( $_GET['shipping_provider'] );
+		}
+	}
+
+	/**
+	 * Run a callback with the given query registered as the admin main orders query, then restore state.
+	 *
+	 * @param WP_Query $query    The query to treat as the main query.
+	 * @param callable $callback The assertions to run while the query is active.
+	 */
+	private function with_main_query( WP_Query $query, callable $callback ): void {
+		$original_main_query = $GLOBALS['wp_the_query'] ?? null;
+		$original_screen     = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
+
+		// is_main_query() compares against $wp_the_query, and is_admin() reads the current screen;
+		// the test restores both in the finally block.
+		$GLOBALS['wp_the_query'] = $query; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+		set_current_screen( 'edit-shop_order' );
+		try {
+			$callback();
+		} finally {
+			$GLOBALS['wp_the_query'] = $original_main_query; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+			if ( $original_screen ) {
+				$GLOBALS['current_screen'] = $original_screen; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+			} else {
+				unset( $GLOBALS['current_screen'] );
+			}
+		}
+	}
+
+	/**
+	 * Insert a fulfillment referencing a provider slug, as the filter query expects.
+	 *
+	 * @param int    $entity_id     The order id the fulfillment belongs to.
+	 * @param string $provider_slug The provider slug the fulfillment references.
+	 * @param bool   $deleted       Whether the fulfillment row is soft-deleted.
+	 * @param bool   $meta_deleted  Whether the provider meta row is soft-deleted.
+	 */
+	private function seed_fulfillment( int $entity_id, string $provider_slug, bool $deleted = false, bool $meta_deleted = false ): void {
+		global $wpdb;
+		$now = current_time( 'mysql', true );
+
+		$wpdb->insert(
+			$wpdb->prefix . 'wc_order_fulfillments',
+			array(
+				'entity_type'  => WC_Order::class,
+				'entity_id'    => $entity_id,
+				'status'       => 'unfulfilled',
+				'is_fulfilled' => 0,
+				'date_updated' => $now,
+				'date_deleted' => $deleted ? $now : null,
+			)
+		);
+
+		// Column names of a custom fulfillments table, not a slow postmeta query.
+		$wpdb->insert(
+			$wpdb->prefix . 'wc_order_fulfillment_meta',
+			array(
+				'fulfillment_id' => (int) $wpdb->insert_id,
+				'meta_key'       => '_shipment_provider', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+				'meta_value'     => wp_json_encode( $provider_slug ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
+				'date_updated'   => $now,
+				'date_deleted'   => $meta_deleted ? $now : null,
+			)
+		);
+	}
 }