Commit 122520849d8 for woocommerce
commit 122520849d8e3ae5906dee48074174491c13e786
Author: Luis Felipe Zaguini <26530524+zaguiini@users.noreply.github.com>
Date: Fri Oct 16 15:36:55 2026 -0300
Fix the order abilities for order types that extensions register (#69692)
* Let the orders query list order types that extensions register
The orders query accepts a type, such as shop_subscription, and lists
orders of that type. Its status input and output accept the statuses
that extensions register with the wc- prefix, such as active. The query
adds the wc- prefix to these statuses, because the order query adds it
only to order statuses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Let order-update-status set the statuses of other order types
The status input accepts the statuses that extensions register, such as
active for subscriptions. Before the save, the ability sets the status
on a copy of the order. The order class changes a status that is not
valid for its type to pending, so the ability refuses that status with
a 400 error. An exception from update_status(), such as a refused
subscription move, becomes a 400 error with its message.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Describe order types and statuses without naming an extension
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Check order permissions against the actual order type
Order abilities checked the shop_order permission for every order ID.
They now check the permission of the order's own type, and refuse an
ID whose type the abilities do not support, such as a refund.
The order type test now restores the shop_order meta caps. Removing
its post type also removed them, and later permission tests failed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Return a 400 error when the status check of an order throws
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Check a status with WC_Abstract_Order::is_valid_status(), and accept a list of statuses
The order status ability asks the order whether it accepts a status, with a
new public is_valid_status() method, instead of setting it on a clone. The
order abilities accept every registered order type except refunds and the
HPOS placeholder. The default status list of orders-query stays as on trunk
for shop_order, and statuses registered with exclude_from_search are never
queryable. orders-query takes one status or a list.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Keep hidden statuses in the output schema, and keep the "0" status slug
A status registered with exclude_from_search stays out of the orders-query
inputs, but an order with that status can still be read by ID. Its status
now stays in the output schema, so the read does not fail output validation.
A status slug "0" in a status list no longer gets dropped by array_filter().
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Use a boolean predicate when filtering empty status slugs
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* Rename get_order_type_status_slugs to get_registered_order_status_slugs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/add-orders-query-order-types b/plugins/woocommerce/changelog/add-orders-query-order-types
new file mode 100644
index 00000000000..8d77f8cdc80
--- /dev/null
+++ b/plugins/woocommerce/changelog/add-orders-query-order-types
@@ -0,0 +1,4 @@
+Significance: minor
+Type: add
+
+Let the order abilities list and change orders of the types that extensions register, and let orders-query take a list of statuses.
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
index 98dab092d03..acc87665401 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
@@ -816,6 +816,18 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
return array_keys( wc_get_order_statuses() );
}
+ /**
+ * Whether the order can have a status.
+ *
+ * @since 11.3.0
+ *
+ * @param string $status Status slug, with or without the `wc-` prefix.
+ * @return bool
+ */
+ public function is_valid_status( string $status ): bool {
+ return in_array( 'wc-' . OrderUtil::remove_status_prefix( $status ), $this->get_valid_statuses(), true );
+ }
+
/**
* Get user ID. Used by orders, not other order types like refunds.
*
diff --git a/plugins/woocommerce/src/Internal/Abilities/Domain/OrderUpdateStatus.php b/plugins/woocommerce/src/Internal/Abilities/Domain/OrderUpdateStatus.php
index 688c084941f..ea5274f21cc 100644
--- a/plugins/woocommerce/src/Internal/Abilities/Domain/OrderUpdateStatus.php
+++ b/plugins/woocommerce/src/Internal/Abilities/Domain/OrderUpdateStatus.php
@@ -90,7 +90,7 @@ class OrderUpdateStatus extends AbstractDomainAbility implements AbilityDefiniti
$status = OrderUtil::remove_status_prefix( sanitize_key( $input['status'] ) );
- if ( ! in_array( $status, self::get_allowed_order_status_slugs(), true ) ) {
+ if ( ! in_array( $status, self::get_query_order_status_slugs(), true ) ) {
return new \WP_Error(
'woocommerce_order_status_invalid',
__( 'Order status is invalid.', 'woocommerce' ),
@@ -109,11 +109,28 @@ class OrderUpdateStatus extends AbstractDomainAbility implements AbilityDefiniti
);
}
- $updated = $order->update_status(
- $status,
- isset( $input['note'] ) ? wp_kses_post( $input['note'] ) : '',
- true
- );
+ if ( ! $order->is_valid_status( $status ) ) {
+ return new \WP_Error(
+ 'woocommerce_order_status_invalid',
+ /* translators: %s: order status slug. */
+ sprintf( __( 'This order cannot have the "%s" status.', 'woocommerce' ), $status ),
+ array( 'status' => 400 )
+ );
+ }
+
+ try {
+ $updated = $order->update_status(
+ $status,
+ isset( $input['note'] ) ? wp_kses_post( $input['note'] ) : '',
+ true
+ );
+ } catch ( \Exception $e ) {
+ return new \WP_Error(
+ 'woocommerce_order_status_update_failed',
+ $e->getMessage(),
+ array( 'status' => 400 )
+ );
+ }
if ( ! $updated ) {
return new \WP_Error(
@@ -143,8 +160,8 @@ class OrderUpdateStatus extends AbstractDomainAbility implements AbilityDefiniti
),
'status' => array(
'type' => 'string',
- 'description' => __( 'Order status slug without the wc- prefix.', 'woocommerce' ),
- 'enum' => self::get_allowed_order_status_slugs(),
+ 'description' => __( 'Order status slug without the wc- prefix. Custom order types may add their own statuses.', 'woocommerce' ),
+ 'enum' => self::get_query_order_status_slugs(),
),
'note' => array(
'type' => 'string',
diff --git a/plugins/woocommerce/src/Internal/Abilities/Domain/OrdersQuery.php b/plugins/woocommerce/src/Internal/Abilities/Domain/OrdersQuery.php
index e0ede9e7f9d..dd67e105ca9 100644
--- a/plugins/woocommerce/src/Internal/Abilities/Domain/OrdersQuery.php
+++ b/plugins/woocommerce/src/Internal/Abilities/Domain/OrdersQuery.php
@@ -98,19 +98,28 @@ class OrdersQuery extends AbstractDomainAbility implements AbilityDefinition {
'page' => $page,
'paginate' => true,
'return' => 'objects',
- 'type' => 'shop_order',
+ 'type' => self::get_order_type_from_input( $input ),
);
- foreach ( array( 'status', 'billing_email', 'order' ) as $field ) {
+ foreach ( array( 'billing_email', 'order' ) as $field ) {
if ( ! empty( $input[ $field ] ) && is_scalar( $input[ $field ] ) ) {
$args[ $field ] = wc_clean( (string) $input[ $field ] );
}
}
+ $status = $input['status'] ?? array();
+ if ( is_string( $status ) && '' !== $status ) {
+ $args['status'] = array( wc_clean( $status ) );
+ } elseif ( is_array( $status ) && ! empty( $status ) ) {
+ $args['status'] = array_map( 'wc_clean', array_filter( array_map( 'strval', $status ), static fn ( string $slug ): bool => '' !== $slug ) );
+ }
+
if ( empty( $args['status'] ) ) {
- $args['status'] = self::get_allowed_order_status_slugs();
+ $args['status'] = 'shop_order' === $args['type'] ? self::get_allowed_order_status_slugs() : self::get_query_order_status_slugs();
}
+ $args['status'] = array_map( array( __CLASS__, 'prepare_status_arg' ), (array) $args['status'] );
+
if ( ! empty( $input['orderby'] ) && is_scalar( $input['orderby'] ) ) {
$orderby = sanitize_text_field( (string) $input['orderby'] );
$args['orderby'] = self::prepare_orderby_arg( $orderby );
@@ -173,7 +182,25 @@ class OrdersQuery extends AbstractDomainAbility implements AbilityDefinition {
public static function can_query_orders( $input = array() ): bool {
$order_id = self::get_id_from_input( $input );
- return wc_rest_check_post_permissions( 'shop_order', 'read', $order_id );
+ if ( $order_id > 0 ) {
+ return self::check_order_permission( $order_id, 'read' );
+ }
+
+ $type = is_array( $input ) ? self::get_order_type_from_input( $input ) : 'shop_order';
+
+ return wc_rest_check_post_permissions( $type, 'read' );
+ }
+
+ /**
+ * Order type from the input, or `shop_order` when the input has no known type.
+ *
+ * @param array $input Ability input.
+ * @return string
+ */
+ private static function get_order_type_from_input( array $input ): string {
+ $type = $input['type'] ?? 'shop_order';
+
+ return is_string( $type ) && in_array( $type, self::get_order_type_slugs(), true ) ? $type : 'shop_order';
}
/**
@@ -189,10 +216,27 @@ class OrdersQuery extends AbstractDomainAbility implements AbilityDefinition {
'type' => 'integer',
'minimum' => 1,
),
- 'status' => array(
+ 'type' => array(
'type' => 'string',
- 'description' => __( 'Filter by order status slug without the wc- prefix.', 'woocommerce' ),
- 'enum' => self::get_allowed_order_status_slugs(),
+ 'description' => __( 'Order type to list. Extensions can register more order types. Defaults to shop_order.', 'woocommerce' ),
+ 'enum' => self::get_order_type_slugs(),
+ 'default' => 'shop_order',
+ ),
+ 'status' => array(
+ 'description' => __( 'Filter by one order status slug, or a list of slugs, without the wc- prefix. Custom order types may add their own statuses.', 'woocommerce' ),
+ 'anyOf' => array(
+ array(
+ 'type' => 'string',
+ 'enum' => self::get_query_order_status_slugs(),
+ ),
+ array(
+ 'type' => 'array',
+ 'items' => array(
+ 'type' => 'string',
+ 'enum' => self::get_query_order_status_slugs(),
+ ),
+ ),
+ ),
),
'customer_id' => array(
'type' => 'integer',
@@ -333,6 +377,18 @@ class OrdersQuery extends AbstractDomainAbility implements AbilityDefinition {
return gmdate( 'Y-m-d H:i:s', $timestamp );
}
+ /**
+ * Add the `wc-` prefix to a status that is registered with it. The order
+ * query adds the prefix only to order statuses, not to the statuses of
+ * other order types.
+ *
+ * @param string $status Status slug without the `wc-` prefix.
+ * @return string
+ */
+ private static function prepare_status_arg( string $status ): string {
+ return in_array( 'wc-' . $status, get_post_stati(), true ) ? 'wc-' . $status : $status;
+ }
+
/**
* Prepare orderby values for wc_get_orders across HPOS and legacy storage.
*
diff --git a/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/OrderAbilityTrait.php b/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/OrderAbilityTrait.php
index 4cad9374209..63ea7ba88a7 100644
--- a/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/OrderAbilityTrait.php
+++ b/plugins/woocommerce/src/Internal/Abilities/Domain/Traits/OrderAbilityTrait.php
@@ -8,6 +8,7 @@ declare( strict_types=1 );
namespace Automattic\WooCommerce\Internal\Abilities\Domain\Traits;
use Automattic\WooCommerce\Enums\OrderStatus;
+use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Utilities\OrderUtil;
defined( 'ABSPATH' ) || exit;
@@ -34,6 +35,55 @@ trait OrderAbilityTrait {
);
}
+ /**
+ * Order types that the abilities can query: every registered order type
+ * except refunds and the HPOS placeholder.
+ *
+ * @return array<int, string>
+ */
+ protected static function get_order_type_slugs(): array {
+ return array_values(
+ array_diff(
+ wc_get_order_types(),
+ array( 'shop_order_refund', DataSynchronizer::PLACEHOLDER_ORDER_POST_TYPE )
+ )
+ );
+ }
+
+ /**
+ * Status slugs (without the `wc-` prefix) that are registered with the
+ * `wc-` prefix, including the statuses of other order types.
+ *
+ * @param array $args Arguments for get_post_stati().
+ * @return array<int, string>
+ */
+ protected static function get_registered_order_status_slugs( array $args = array() ): array {
+ $statuses = array_filter(
+ get_post_stati( $args ),
+ static function ( $status ): bool {
+ return 0 === strpos( $status, 'wc-' );
+ }
+ );
+
+ return array_map( array( OrderUtil::class, 'remove_status_prefix' ), array_values( $statuses ) );
+ }
+
+ /**
+ * Status slugs (without the `wc-` prefix) that the orders query accepts for
+ * any order type. Statuses that are excluded from search are not included.
+ *
+ * @return array<int, string>
+ */
+ protected static function get_query_order_status_slugs(): array {
+ return array_values(
+ array_diff(
+ array_unique( array_merge( self::get_allowed_order_status_slugs(), self::get_registered_order_status_slugs() ) ),
+ array( OrderStatus::CHECKOUT_DRAFT ),
+ self::get_registered_order_status_slugs( array( 'exclude_from_search' => true ) )
+ )
+ );
+ }
+
/**
* Possible order status slugs (without the `wc-` prefix) for ability output.
*
@@ -42,9 +92,12 @@ trait OrderAbilityTrait {
protected static function get_order_output_status_slugs(): array {
return array_values(
array_unique(
- array_map(
- array( OrderUtil::class, 'remove_status_prefix' ),
- array_merge( OrderStatus::get_all(), array_keys( wc_get_order_statuses() ) )
+ array_merge(
+ array_map(
+ array( OrderUtil::class, 'remove_status_prefix' ),
+ array_merge( OrderStatus::get_all(), array_keys( wc_get_order_statuses() ) )
+ ),
+ self::get_registered_order_status_slugs()
)
)
);
@@ -180,7 +233,24 @@ trait OrderAbilityTrait {
public static function can_edit_order( $input = array() ): bool {
$order_id = self::get_id_from_input( $input );
- return $order_id > 0 && wc_rest_check_post_permissions( 'shop_order', 'edit', $order_id );
+ return $order_id > 0 && self::check_order_permission( $order_id, 'edit' );
+ }
+
+ /**
+ * Check a permission against the actual type of an order. An order whose
+ * type the abilities do not support fails. An ID with no order is checked
+ * as `shop_order`.
+ *
+ * @param int $order_id Order ID.
+ * @param string $context Permission context, such as `read` or `edit`.
+ * @return bool
+ */
+ protected static function check_order_permission( int $order_id, string $context ): bool {
+ $type = OrderUtil::get_order_type( $order_id );
+ $type = $type ? $type : 'shop_order';
+
+ return in_array( $type, self::get_order_type_slugs(), true )
+ && wc_rest_check_post_permissions( $type, $context, $order_id );
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/Abilities/AbilitiesLoaderTest.php b/plugins/woocommerce/tests/php/src/Internal/Abilities/AbilitiesLoaderTest.php
index 56593e609a3..f4ad9ea0bf2 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Abilities/AbilitiesLoaderTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Abilities/AbilitiesLoaderTest.php
@@ -392,10 +392,257 @@ class AbilitiesLoaderTest extends \WC_Unit_Test_Case {
$query_schema = wp_get_ability( 'woocommerce/orders-query' )->get_input_schema();
$update_schema = wp_get_ability( 'woocommerce/order-update-status' )->get_input_schema();
- $this->assertNotContains( 'checkout-draft', $query_schema['properties']['status']['enum'] ?? array() );
+ $this->assertNotContains( 'checkout-draft', $query_schema['properties']['status']['anyOf'][0]['enum'] ?? array() );
$this->assertNotContains( 'checkout-draft', $update_schema['properties']['status']['enum'] ?? array() );
}
+ /**
+ * @testdox Should list shop_order by default, and not refunds or HPOS placeholders.
+ */
+ public function test_orders_query_type_defaults_to_shop_order(): void {
+ $schema = wp_get_ability( 'woocommerce/orders-query' )->get_input_schema();
+ $types = $schema['properties']['type']['enum'] ?? array();
+
+ $this->assertSame( 'shop_order', $schema['properties']['type']['default'] ?? null );
+ $this->assertContains( 'shop_order', $types );
+ $this->assertNotContains( 'shop_order_refund', $types );
+ $this->assertNotContains( 'shop_order_placehold', $types );
+
+ $query_args = $this->capture_order_query_args( array() );
+
+ $this->assertSame( 'shop_order', $query_args['type'] ?? null );
+ }
+
+ /**
+ * @testdox Should query an order type and status that an extension registers.
+ */
+ public function test_orders_query_accepts_registered_order_type_and_status(): void {
+ global $wc_order_types, $post_type_meta_caps;
+
+ $meta_caps = $post_type_meta_caps;
+ $this->unregister_domain_abilities();
+ wc_register_order_type(
+ 'shop_test_contract',
+ array(
+ 'capability_type' => 'shop_order',
+ 'exclude_from_order_views' => true,
+ 'add_order_meta_boxes' => false,
+ )
+ );
+ register_post_status(
+ 'wc-test-active',
+ array(
+ 'label' => 'Active',
+ 'exclude_from_search' => false,
+ )
+ );
+
+ try {
+ $this->register_domain_abilities();
+
+ $ability = wp_get_ability( 'woocommerce/orders-query' );
+ $input = $ability->get_input_schema()['properties'];
+ $output = $ability->get_output_schema()['properties']['orders']['items']['properties'];
+
+ $this->assertContains( 'shop_test_contract', $input['type']['enum'] );
+ $this->assertContains( 'test-active', $input['status']['anyOf'][0]['enum'] );
+ $this->assertContains( 'test-active', $output['status']['enum'] );
+
+ $query_args = $this->capture_order_query_args(
+ array(
+ 'type' => 'shop_test_contract',
+ 'status' => 'test-active',
+ )
+ );
+
+ $this->assertSame( 'shop_test_contract', $query_args['type'] ?? null );
+ $this->assertSame( array( 'wc-test-active' ), $query_args['status'] ?? null );
+ } finally {
+ $this->unregister_domain_abilities();
+ unset( $wc_order_types['shop_test_contract'], $GLOBALS['wp_post_statuses']['wc-test-active'] );
+ unregister_post_type( 'shop_test_contract' );
+ $post_type_meta_caps = $meta_caps; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- unregister_post_type() removes the shop_order meta caps too.
+ $this->register_domain_abilities();
+ }
+ }
+
+ /**
+ * @testdox Should use the same default statuses as before for shop_order.
+ */
+ public function test_orders_query_default_statuses_match_trunk_for_shop_order(): void {
+ $expected = array();
+ foreach ( array_keys( wc_get_order_statuses() ) as $status ) {
+ if ( 'wc-checkout-draft' !== $status ) {
+ $expected[] = $status;
+ }
+ }
+
+ $query_args = $this->capture_order_query_args( array() );
+
+ $this->assertSame( $expected, $query_args['status'] ?? null );
+ }
+
+ /**
+ * @testdox Should not accept a status that is excluded from search, but return it in the output.
+ */
+ public function test_a_status_excluded_from_search_is_not_queryable(): void {
+ global $wc_order_types, $post_type_meta_caps;
+
+ $meta_caps = $post_type_meta_caps;
+ $this->unregister_domain_abilities();
+ wc_register_order_type(
+ 'shop_test_contract',
+ array(
+ 'capability_type' => 'shop_order',
+ 'exclude_from_order_views' => true,
+ 'add_order_meta_boxes' => false,
+ )
+ );
+ register_post_status(
+ 'wc-test-hidden',
+ array(
+ 'label' => 'Hidden',
+ 'exclude_from_search' => true,
+ )
+ );
+
+ try {
+ $this->register_domain_abilities();
+
+ $ability = wp_get_ability( 'woocommerce/orders-query' );
+ $input = $ability->get_input_schema()['properties'];
+ $output = $ability->get_output_schema()['properties']['orders']['items']['properties'];
+
+ $this->assertNotContains( 'test-hidden', $input['status']['anyOf'][0]['enum'] );
+ $this->assertContains( 'test-hidden', $output['status']['enum'] );
+
+ $query_args = $this->capture_order_query_args( array( 'type' => 'shop_test_contract' ) );
+
+ $this->assertNotContains( 'test-hidden', $query_args['status'] ?? array() );
+ $this->assertNotContains( 'wc-test-hidden', $query_args['status'] ?? array() );
+ } finally {
+ $this->unregister_domain_abilities();
+ unset( $wc_order_types['shop_test_contract'], $GLOBALS['wp_post_statuses']['wc-test-hidden'] );
+ unregister_post_type( 'shop_test_contract' );
+ $post_type_meta_caps = $meta_caps; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- unregister_post_type() removes the shop_order meta caps too.
+ $this->register_domain_abilities();
+ }
+ }
+
+ /**
+ * @testdox Should accept a list of statuses and reject a list with an unknown status.
+ */
+ public function test_orders_query_accepts_a_list_of_statuses(): void {
+ $query_args = $this->capture_order_query_args( array( 'status' => array( 'on-hold', 'processing' ) ) );
+
+ $this->assertSame( array( 'wc-on-hold', 'wc-processing' ), $query_args['status'] ?? null );
+
+ $result = wp_get_ability( 'woocommerce/orders-query' )->execute( array( 'status' => array( 'processing', 'not-a-status' ) ) );
+
+ $this->assertWPError( $result );
+ $this->assertSame( 'ability_invalid_input', $result->get_error_code() );
+ }
+
+ /**
+ * @testdox Should refuse a status that another order type registers, and keep the order status.
+ */
+ public function test_order_update_status_refuses_status_of_another_order_type(): void {
+ $order = \WC_Helper_Order::create_order();
+ $this->created_order_ids[] = $order->get_id();
+ $status_before = $order->get_status();
+
+ $this->unregister_domain_abilities();
+ register_post_status(
+ 'wc-test-active',
+ array(
+ 'label' => 'Active',
+ 'exclude_from_search' => false,
+ )
+ );
+
+ try {
+ $this->register_domain_abilities();
+
+ $ability = wp_get_ability( 'woocommerce/order-update-status' );
+ $this->assertContains( 'test-active', $ability->get_input_schema()['properties']['status']['enum'] );
+
+ $result = $ability->execute(
+ array(
+ 'id' => $order->get_id(),
+ 'status' => 'test-active',
+ )
+ );
+ } finally {
+ $this->unregister_domain_abilities();
+ unset( $GLOBALS['wp_post_statuses']['wc-test-active'] );
+ $this->register_domain_abilities();
+ }
+
+ $this->assertWPError( $result );
+ $this->assertSame( 'woocommerce_order_status_invalid', $result->get_error_code() );
+ $this->assertSame( 400, $result->get_error_data()['status'] ?? null );
+ $this->assertSame( $status_before, wc_get_order( $order->get_id() )->get_status() );
+ }
+
+ /**
+ * @testdox Should reject an order type that is not registered.
+ */
+ public function test_orders_query_rejects_unknown_order_type(): void {
+ $result = wp_get_ability( 'woocommerce/orders-query' )->execute( array( 'type' => 'shop_order_refund' ) );
+
+ $this->assertWPError( $result );
+ $this->assertSame( 'ability_invalid_input', $result->get_error_code() );
+ }
+
+ /**
+ * @testdox Should check the permission against the actual type of the order ID.
+ */
+ public function test_orders_query_refuses_id_of_unsupported_order_type(): void {
+ $order = \WC_Helper_Order::create_order();
+ $this->created_order_ids[] = $order->get_id();
+ $refund = wc_create_refund(
+ array(
+ 'order_id' => $order->get_id(),
+ 'amount' => 1,
+ )
+ );
+
+ $result = wp_get_ability( 'woocommerce/orders-query' )->execute(
+ array(
+ 'id' => $refund->get_id(),
+ 'type' => 'shop_order',
+ )
+ );
+
+ $this->assertWPError( $result );
+ $this->assertSame( 'ability_invalid_permissions', $result->get_error_code() );
+ }
+
+ /**
+ * Run the orders query ability and return the arguments of its order query.
+ *
+ * @param array $input Ability input.
+ * @return array
+ */
+ private function capture_order_query_args( array $input ): array {
+ $captured = array();
+ $capture = static function ( $args ) use ( &$captured ) {
+ $captured = $args;
+ return $args;
+ };
+
+ add_filter( 'woocommerce_order_query_args', $capture );
+ try {
+ $result = wp_get_ability( 'woocommerce/orders-query' )->execute( $input );
+ } finally {
+ remove_filter( 'woocommerce_order_query_args', $capture );
+ }
+
+ $this->assertNotWPError( $result );
+
+ return $captured;
+ }
+
/**
* @testdox Should register extension ability classes appended via the loader filter.
*/