From 39adebf415678dd5fb734bebdae6a0ba2c358066 Mon Sep 17 00:00:00 2001 From: Geoff Taylor Date: Mon, 23 Mar 2026 18:04:51 -0400 Subject: [PATCH] fix: customer order query strips pagination and where args (#981) The array_intersect_key call in the orders connection resolver was filtering all $args (including first, last, after, before) against get_connection_args('public') keys. Since get_connection_args returns where arg definitions (not top-level connection args), the intersection matched nothing and stripped everything. Fix: only filter $args['where'] instead of all $args, so pagination and other connection args are preserved for non-admin customers. --- includes/connection/class-orders.php | 8 +- tests/wpunit/OrderConnectionFiltersTest.php | 160 ++++++++++++++++++++ 2 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 tests/wpunit/OrderConnectionFiltersTest.php diff --git a/includes/connection/class-orders.php b/includes/connection/class-orders.php index 39b3ade1..83692f4a 100644 --- a/includes/connection/class-orders.php +++ b/includes/connection/class-orders.php @@ -122,10 +122,10 @@ class Orders { // Check if user shop manager. $not_manager = ! current_user_can( $post_object->cap->edit_posts ); - // Remove any arguments that require querying user to have "shop manager" role. - $args = $not_manager && 'shop_order' === $post_object->name - ? \array_intersect_key( $args, array_keys( self::get_connection_args( 'public' ) ) ) - : $args; + // Remove any where arguments that require querying user to have "shop manager" role. + if ( $not_manager && 'shop_order' === $post_object->name && isset( $args['where'] ) ) { + $args['where'] = \array_intersect_key( $args['where'], self::get_connection_args( 'public' ) ); + } // Initialize connection resolver. $resolver = new Order_Connection_Resolver( $source, $args, $context, $info, $post_object->name ); diff --git a/tests/wpunit/OrderConnectionFiltersTest.php b/tests/wpunit/OrderConnectionFiltersTest.php new file mode 100644 index 00000000..ec450234 --- /dev/null +++ b/tests/wpunit/OrderConnectionFiltersTest.php @@ -0,0 +1,160 @@ +factory->customer->create(); + + // Create 3 orders for this customer. + $order_ids = []; + for ( $i = 0; $i < 3; $i++ ) { + $order_ids[] = $this->factory->order->createNew( + [ 'customer_id' => $customer_id ] + ); + } + + $this->loginAs( $customer_id ); + + // Query first 2 orders. + $query = ' + query ($first: Int, $after: String) { + orders(first: $first, after: $after) { + nodes { + databaseId + } + pageInfo { + hasNextPage + endCursor + } + } + } + '; + + $variables = [ 'first' => 2 ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + + $this->assertQuerySuccessful( $response, [] ); + + $nodes = $this->lodashGet( $response, 'data.orders.nodes', [] ); + $this->assertCount( 2, $nodes, 'Should return exactly 2 orders with first: 2.' ); + + $has_next_page = $this->lodashGet( $response, 'data.orders.pageInfo.hasNextPage' ); + $this->assertTrue( $has_next_page, 'Should have a next page.' ); + + // Query the next page. + $end_cursor = $this->lodashGet( $response, 'data.orders.pageInfo.endCursor' ); + $variables = [ + 'first' => 2, + 'after' => $end_cursor, + ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + + $this->assertQuerySuccessful( $response, [] ); + + $nodes = $this->lodashGet( $response, 'data.orders.nodes', [] ); + $this->assertCount( 1, $nodes, 'Should return 1 order on the second page.' ); + + $has_next_page = $this->lodashGet( $response, 'data.orders.pageInfo.hasNextPage' ); + $this->assertFalse( $has_next_page, 'Should not have a next page.' ); + } + + /** + * Test that a non-admin customer can filter their own orders with where args. + */ + public function testCustomerCanFilterOrdersByStatus() { + $customer_id = $this->factory->customer->create(); + + // Create orders with different statuses. + $completed_id = $this->factory->order->createNew( + [ + 'customer_id' => $customer_id, + 'status' => 'completed', + ] + ); + $pending_id = $this->factory->order->createNew( + [ + 'customer_id' => $customer_id, + 'status' => 'pending', + ] + ); + $processing_id = $this->factory->order->createNew( + [ + 'customer_id' => $customer_id, + 'status' => 'processing', + ] + ); + + $this->loginAs( $customer_id ); + + $query = ' + query ($statuses: [OrderStatusEnum]) { + orders(where: { statuses: $statuses }) { + nodes { + databaseId + status + } + } + } + '; + + // Filter by completed only. + $variables = [ 'statuses' => [ 'COMPLETED' ] ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedField( 'orders.nodes.#.databaseId', $completed_id ), + $this->not()->expectedField( 'orders.nodes.#.databaseId', $pending_id ), + $this->not()->expectedField( 'orders.nodes.#.databaseId', $processing_id ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + + // Filter by processing only. + $variables = [ 'statuses' => [ 'PROCESSING' ] ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + $expected = [ + $this->expectedField( 'orders.nodes.#.databaseId', $processing_id ), + $this->not()->expectedField( 'orders.nodes.#.databaseId', $completed_id ), + $this->not()->expectedField( 'orders.nodes.#.databaseId', $pending_id ), + ]; + + $this->assertQuerySuccessful( $response, $expected ); + } + + /** + * Test that non-admin customer where args are properly restricted + * (customerId, customersIn, billingEmail should be stripped). + */ + public function testCustomerCannotUsePrivateWhereArgs() { + $customer_id = $this->factory->customer->create(); + $other_customer = $this->factory->customer->create(); + + $own_order_id = $this->factory->order->createNew( [ 'customer_id' => $customer_id ] ); + $other_order_id = $this->factory->order->createNew( [ 'customer_id' => $other_customer ] ); + + $this->loginAs( $customer_id ); + + // Try to query another customer's orders — the customerId arg should be stripped. + $query = ' + query ($customerId: Int) { + orders(where: { customerId: $customerId }) { + nodes { + databaseId + } + } + } + '; + + $variables = [ 'customerId' => $other_customer ]; + $response = $this->graphql( compact( 'query', 'variables' ) ); + + $this->assertQuerySuccessful( $response, [] ); + + // Should only see own orders, not the other customer's. + $nodes = $this->lodashGet( $response, 'data.orders.nodes', [] ); + $returned_ids = array_column( $nodes, 'databaseId' ); + $this->assertContains( $own_order_id, $returned_ids ); + $this->assertNotContains( $other_order_id, $returned_ids ); + } +}