From f50eb68983345d8e97e4cc3037884acaedb79980 Mon Sep 17 00:00:00 2001 From: John Parris Date: Thu, 24 Sep 2026 11:05:00 -0400 Subject: [PATCH] Tests: Expand test coverage for `wp_list_filter()`. Every case in the data provider passed a list of objects, and every case spelled the operator in uppercase. Two parts of `WP_List_Util::filter()` therefore never ran: * A list can hold arrays as well as objects, and the two are matched differently: arrays with `array_key_exists()`, objects with `isset()`. Only the object path was tested. * The operator is uppercased before it is checked against `AND`, `OR`, and `NOT`, so `and` works the same as `AND`. Since the tests only ever passed uppercase, dropping the `strtoupper()` would not have failed anything, even though `wp_filter_object_list()` itself defaults to lowercase `and`. Also adds cases for the default operator, an empty list, a `null` list, list values that are neither arrays nor objects, empty arguments with an invalid operator, string keys in the output, `OR` matching nothing, `NOT` matching everything, and a loose `true` to `'yes'` comparison alongside the existing numeric one. See #53987. --- .../phpunit/tests/functions/wpListFilter.php | 288 +++++++++++++++++- 1 file changed, 279 insertions(+), 9 deletions(-) diff --git a/tests/phpunit/tests/functions/wpListFilter.php b/tests/phpunit/tests/functions/wpListFilter.php index d9fca8cf5c494..81007b776e7dc 100644 --- a/tests/phpunit/tests/functions/wpListFilter.php +++ b/tests/phpunit/tests/functions/wpListFilter.php @@ -10,6 +10,8 @@ class Tests_Functions_wpListFilter extends WP_UnitTestCase { /** + * @ticket 53987 + * * @dataProvider data_wp_list_filter * * @param array $input_list An array of objects to filter. @@ -22,6 +24,32 @@ public function test_wp_list_filter( $input_list, $args, $operator, $expected ) $this->assertEqualSetsWithIndex( $expected, wp_list_filter( $input_list, $args, $operator ) ); } + /** + * Tests that the 'AND' operator is used when no operator is passed. + * + * @ticket 53987 + */ + public function test_wp_list_filter_should_default_to_and_operator() { + $input_list = array( + (object) array( + 'foo' => 'bar', + 'bar' => 'baz', + ), + (object) array( 'foo' => 'bar' ), + ); + + $this->assertEqualSetsWithIndex( + array( 0 => $input_list[0] ), + wp_list_filter( + $input_list, + array( + 'foo' => 'bar', + 'bar' => 'baz', + ) + ) + ); + } + /** * Data provider. * @@ -29,25 +57,25 @@ public function test_wp_list_filter( $input_list, $args, $operator, $expected ) */ public function data_wp_list_filter() { return array( - 'string instead of array' => array( + 'string instead of array' => array( 'foo', array(), 'AND', array(), ), - 'object instead of array' => array( + 'object instead of array' => array( (object) array( 'foo' ), array(), 'AND', array(), ), - 'empty args' => array( + 'empty args' => array( array( 'foo', 'bar' ), array(), 'AND', array( 'foo', 'bar' ), ), - 'invalid operator' => array( + 'invalid operator' => array( array( (object) array( 'foo' => 'bar' ), (object) array( 'foo' => 'baz' ), @@ -56,7 +84,7 @@ public function data_wp_list_filter() { 'XOR', array(), ), - 'single argument to match' => array( + 'single argument to match' => array( array( (object) array( 'foo' => 'bar', @@ -94,7 +122,7 @@ public function data_wp_list_filter() { ), ), ), - 'all must match' => array( + 'all must match' => array( array( (object) array( 'foo' => 'bar', @@ -132,7 +160,7 @@ public function data_wp_list_filter() { ), ), ), - 'any must match' => array( + 'any must match' => array( array( (object) array( 'foo' => 'bar', @@ -179,7 +207,7 @@ public function data_wp_list_filter() { ), ), ), - 'none must match' => array( + 'none must match' => array( array( (object) array( 'foo' => 'bar', @@ -216,7 +244,7 @@ public function data_wp_list_filter() { ), ), ), - 'string to int comparison' => array( + 'string to int comparison' => array( array( (object) array( 'foo' => '1', @@ -230,6 +258,248 @@ public function data_wp_list_filter() { ), ), ), + 'bool to string comparison' => array( + array( + (object) array( + 'foo' => true, + ), + ), + array( 'foo' => 'yes' ), + 'AND', + array( + 0 => (object) array( + 'foo' => true, + ), + ), + ), + 'null instead of array' => array( + null, + array( 'foo' => 'bar' ), + 'AND', + array(), + ), + 'empty list' => array( + array(), + array( 'foo' => 'bar' ), + 'AND', + array(), + ), + + /* + * Empty arguments are checked before the operator is validated, + * so the original list is returned even for an invalid operator. + */ + 'empty args, invalid operator' => array( + array( 'foo', 'bar' ), + array(), + 'XOR', + array( 'foo', 'bar' ), + ), + + // The operator is uppercased before it is validated. + 'lowercase AND operator' => array( + array( + (object) array( 'foo' => 'bar' ), + (object) array( 'foo' => 'baz' ), + ), + array( 'foo' => 'bar' ), + 'and', + array( + 0 => (object) array( 'foo' => 'bar' ), + ), + ), + 'mixed case OR operator' => array( + array( + (object) array( 'foo' => 'bar' ), + (object) array( 'foo' => 'baz' ), + ), + array( 'foo' => 'bar' ), + 'oR', + array( + 0 => (object) array( 'foo' => 'bar' ), + ), + ), + 'lowercase NOT operator' => array( + array( + (object) array( 'foo' => 'bar' ), + (object) array( 'foo' => 'baz' ), + ), + array( 'foo' => 'bar' ), + 'not', + array( + 1 => (object) array( 'foo' => 'baz' ), + ), + ), + + // Lists of arrays are supported alongside lists of objects. + 'arrays: single argument to match' => array( + array( + array( + 'foo' => 'bar', + 'key' => 'foo', + ), + array( + 'foo' => 'baz', + 'key' => 'value', + ), + array( + 'foo' => 'bar', + 'key' => 'value', + ), + ), + array( 'foo' => 'bar' ), + 'AND', + array( + 0 => array( + 'foo' => 'bar', + 'key' => 'foo', + ), + 2 => array( + 'foo' => 'bar', + 'key' => 'value', + ), + ), + ), + 'arrays: all must match' => array( + array( + array( + 'foo' => 'bar', + 'bar' => 'baz', + ), + array( 'foo' => 'bar' ), + ), + array( + 'foo' => 'bar', + 'bar' => 'baz', + ), + 'AND', + array( + 0 => array( + 'foo' => 'bar', + 'bar' => 'baz', + ), + ), + ), + 'arrays: any must match' => array( + array( + array( 'foo' => 'bar' ), + array( 'bar' => 'baz' ), + array( 'foo' => 'baz' ), + ), + array( + 'foo' => 'bar', + 'bar' => 'baz', + ), + 'OR', + array( + 0 => array( 'foo' => 'bar' ), + 1 => array( 'bar' => 'baz' ), + ), + ), + 'arrays: none must match' => array( + array( + array( 'foo' => 'bar' ), + array( 'bar' => 'baz' ), + array( 'foo' => 'baz' ), + ), + array( + 'foo' => 'bar', + 'bar' => 'baz', + ), + 'NOT', + array( + 2 => array( 'foo' => 'baz' ), + ), + ), + 'mixed arrays and objects' => array( + array( + array( 'foo' => 'bar' ), + (object) array( 'foo' => 'bar' ), + array( 'foo' => 'baz' ), + (object) array( 'foo' => 'baz' ), + ), + array( 'foo' => 'bar' ), + 'AND', + array( + 0 => array( 'foo' => 'bar' ), + 1 => (object) array( 'foo' => 'bar' ), + ), + ), + + /* + * Values that are neither arrays nor objects can never match, + * so they are only returned by the 'NOT' operator. + */ + 'scalar values with AND' => array( + array( 'a string', 42, null, array( 'foo' => 'bar' ) ), + array( 'foo' => 'bar' ), + 'AND', + array( + 3 => array( 'foo' => 'bar' ), + ), + ), + 'scalar values with NOT' => array( + array( 'a string', 42, null, array( 'foo' => 'bar' ) ), + array( 'foo' => 'bar' ), + 'NOT', + array( + 0 => 'a string', + 1 => 42, + 2 => null, + ), + ), + + /* + * Array values are checked with array_key_exists(), so a null value + * matches, while object properties are checked with isset(), + * so a null property does not. + */ + 'array with a null value matching null' => array( + array( array( 'foo' => null ) ), + array( 'foo' => null ), + 'AND', + array( + 0 => array( 'foo' => null ), + ), + ), + 'object with a null property matching null' => array( + array( (object) array( 'foo' => null ) ), + array( 'foo' => null ), + 'AND', + array(), + ), + + 'string keys are preserved' => array( + array( + 'first' => (object) array( 'foo' => 'bar' ), + 'second' => (object) array( 'foo' => 'baz' ), + 'third' => (object) array( 'foo' => 'bar' ), + ), + array( 'foo' => 'bar' ), + 'AND', + array( + 'first' => (object) array( 'foo' => 'bar' ), + 'third' => (object) array( 'foo' => 'bar' ), + ), + ), + 'no matches with OR' => array( + array( + (object) array( 'foo' => 'baz' ), + (object) array( 'foo' => 'qux' ), + ), + array( 'foo' => 'bar' ), + 'OR', + array(), + ), + 'all matches with NOT' => array( + array( + (object) array( 'foo' => 'bar' ), + (object) array( 'foo' => 'bar' ), + ), + array( 'foo' => 'bar' ), + 'NOT', + array(), + ), ); } }