From 9b3242d32bb76134e13d5ff9df7067b62554c42e Mon Sep 17 00:00:00 2001 From: Rodrigo Primo Date: Wed, 5 Nov 2025 09:46:21 -0300 Subject: [PATCH] ValidationHelper::is_validated(): add basic tests --- ...structNotFollowedByParenthesisUnitTest.inc | 4 + ...edConstructUnclosedParenthesisUnitTest.inc | 4 + ...onValidationNotSeenAtFileScopeUnitTest.inc | 8 + ...edInConditionOnlyNoParenthesisUnitTest.inc | 7 + ...terValidationNotSeenInFunctionUnitTest.inc | 7 + .../ValidationHelper/IsValidatedUnitTest.inc | 211 +++++++++ .../ValidationHelper/IsValidatedUnitTest.php | 439 ++++++++++++++++++ ...sValidatedValidatedAtFileScopeUnitTest.inc | 6 + .../ValidatedSanitizedInputUnitTest.php | 1 - 9 files changed, 686 insertions(+), 1 deletion(-) create mode 100644 WordPress/Tests/Helpers/ValidationHelper/IsValidatedConstructNotFollowedByParenthesisUnitTest.inc create mode 100644 WordPress/Tests/Helpers/ValidationHelper/IsValidatedConstructUnclosedParenthesisUnitTest.inc create mode 100644 WordPress/Tests/Helpers/ValidationHelper/IsValidatedFunctionValidationNotSeenAtFileScopeUnitTest.inc create mode 100644 WordPress/Tests/Helpers/ValidationHelper/IsValidatedInConditionOnlyNoParenthesisUnitTest.inc create mode 100644 WordPress/Tests/Helpers/ValidationHelper/IsValidatedOuterValidationNotSeenInFunctionUnitTest.inc create mode 100644 WordPress/Tests/Helpers/ValidationHelper/IsValidatedUnitTest.inc create mode 100644 WordPress/Tests/Helpers/ValidationHelper/IsValidatedUnitTest.php create mode 100644 WordPress/Tests/Helpers/ValidationHelper/IsValidatedValidatedAtFileScopeUnitTest.inc diff --git a/WordPress/Tests/Helpers/ValidationHelper/IsValidatedConstructNotFollowedByParenthesisUnitTest.inc b/WordPress/Tests/Helpers/ValidationHelper/IsValidatedConstructNotFollowedByParenthesisUnitTest.inc new file mode 100644 index 0000000000..6d2d4f195d --- /dev/null +++ b/WordPress/Tests/Helpers/ValidationHelper/IsValidatedConstructNotFollowedByParenthesisUnitTest.inc @@ -0,0 +1,4 @@ + isset( $_POST['key'] ); + echo /* testArrowFunctionValidationNotCounted */ $_POST['key']; +} + +function test_construct_wrong_variable_or_key() { + if ( isset( $_GET['key'] ) || isset( $_POST['other'] ) ) { + echo /* testConstructWrongVariableOrKey */ $_POST['key']; + } +} + +function test_construct_param_key_mismatch() { + if ( isset( $_POST['key'] ) ) { + echo /* testConstructParamKeyMismatch */ $_POST['key']; + } +} + +function test_function_name_used_as_constant() { + echo ARRAY_KEY_EXISTS; + echo /* testFunctionNameUsedAsConstant */ $_POST['key']; +} + +function test_function_call_in_attribute() { + #[array_key_exists('key', $_POST)] + echo /* testFunctionCallInAttribute */ $_POST['key']; +} + +function test_function_call_non_global() { + if ( + $obj->array_key_exists( 'key', $_POST ) + || $obj?->key_exists( 'key', $_POST ) + || MyClass::array_key_exists( 'key', $_POST ) + ) { + echo /* testFunctionCallNonGlobal */ $_POST['key']; + } +} + +function test_function_call_namespaced() { + if ( + MyNamespace\array_key_exists( 'key', $_POST ) + || \MyNamespace\key_exists( 'key', $_POST ) + || namespace\array_key_exists( 'key', $_POST ) // This should be considered validated in the future once the method is able to resolve relative namespaces. + || namespace\Sub\key_exists( 'key', $_POST ) + ) { + echo /* testFunctionCallNamespaced */ $_POST['key']; + } +} + +function test_function_call_missing_parameters() { + if ( key_exists() ) { + echo /* testFunctionCallMissingParameters */ $_POST['key']; + } +} + +function test_function_call_wrong_array_param() { + if ( + key_exists( 'key', SOME_CONSTANT ) + || array_key_exists( 'key', $_GET ) + ) { + echo /* testFunctionCallWrongArrayParam */ $_POST['key']; + } +} + +function test_function_call_mismatched_key() { + if ( array_key_exists( 'other', $_POST ) ) { + echo /* testFunctionCallMismatchedKey */ $_POST['key']; + } +} + +function test_coalesce_no_match() { + $a = 'something' ?? 'default'; + $_GET['key'] ??= 'default'; + $_POST['other'] ??= 'default'; + echo /* testCoalesceNoMatch */ $_POST['key']; +} + +/* + * The below should be considered validated. + */ + +function test_validated_with_isset() { + if ( isset( $_POST["key"] ) ) { + echo /* testValidatedWithIsset */ $_POST['key']; + } +} + +function test_validated_with_empty() { + if ( empty( $_POST['key'] ) ) { + echo /* testValidatedWithEmpty */ $_POST['key']; + } +} + +$closure = function() { + if ( empty( $_POST['key'] ) ) { + echo /* testValidatedInClosure */ $_POST['key']; + } +}; + +function test_function_call() { + if ( array_key_exists( 'key', $_POST ) ) { + echo /* testFunctionCall */ $_POST['key']; + } +} + +function test_function_call_mixed_case() { + if ( Key_Exists( 'key', $_POST ) ) { + echo /* testFunctionCallMixedCase */ $_POST['key']; + } +} + +function test_function_call_fully_qualified() { + if ( \array_key_exists( 'key', $_POST ) ) { + echo /* testFunctionCallFullyQualified */ $_POST['key']; + } +} + +function test_function_call_fully_qualified_uppercase() { + if ( \KEY_EXISTS( 'key', $_POST ) ) { + echo /* testFunctionCallFullyQualifiedUppercase */ $_POST['key']; + } +} + +function test_validated_with_null_coalesce() { + $_POST['key'] = $_POST['key'] ?? 'default'; + echo /* testValidatedWithNullCoalesce */ $_POST['key']; +} + +function test_validated_with_coalesce_equal() { + $_POST['key'] ??= 'default'; + echo /* testValidatedWithCoalesceEqual */ $_POST['key']; +} + +/* + * Test cases for `$in_condition_only` set to true. + */ + +function test_in_condition_only_use_inside_condition() { + if ( isset( $_POST['key'] ) ) { + echo /* testInConditionOnlyUseInsideCondition */ $_POST['key']; + } +} + +function test_in_condition_only_use_outside_condition() { + if ( empty( $_POST['key'] ) ) { + return; + } + echo /* testInConditionOnlyUseOutsideCondition */ $_POST['key']; +} + +/* + * Test cases for multi-level array key matching across validation paths. + */ + +function test_array_keys_construct() { + if ( isset( $_POST['key']['sub'] ) ) { + echo /* testArrayKeysConstruct */ $_POST['key']['sub']; + } +} + +function test_array_keys_function_call() { + if ( array_key_exists( 'sub', $_POST['key'] ) ) { + echo /* testArrayKeysFunctionCall */ $_POST['key']['sub']; + } +} + +function test_array_keys_function_call_key_param_mismatch() { + if ( array_key_exists( 'wrong_key', $_POST['key'] ) ) { + echo /* testArrayKeysFunctionCallKeyParamMismatch */ $_POST['key']['sub']; + } +} + +function test_array_keys_coalesce() { + $_POST['key']['sub'] = $_POST['key']['sub'] ?? 'default'; + echo /* testArrayKeysCoalesce */ $_POST['key']['sub']; +} diff --git a/WordPress/Tests/Helpers/ValidationHelper/IsValidatedUnitTest.php b/WordPress/Tests/Helpers/ValidationHelper/IsValidatedUnitTest.php new file mode 100644 index 0000000000..1169847f31 --- /dev/null +++ b/WordPress/Tests/Helpers/ValidationHelper/IsValidatedUnitTest.php @@ -0,0 +1,439 @@ +assertFalse( ValidationHelper::is_validated( self::$phpcsFile, -1 ) ); + } + + /** + * Test is_validated() handles live coding / parse error situations. + * + * @dataProvider dataIsValidatedLiveCoding + * + * @param string $testCaseFile The test case file to parse. + * + * @return void + */ + public function testIsValidatedLiveCoding( $testCaseFile ) { + $this->assertIsValidatedInFile( $testCaseFile, false ); + } + + /** + * Data provider. + * + * @see testIsValidatedLiveCoding() + * + * @return array> + */ + public static function dataIsValidatedLiveCoding() { + return array( + 'construct_not_followed_by_parenthesis' => array( + 'testCaseFile' => 'IsValidatedConstructNotFollowedByParenthesisUnitTest.inc', + ), + 'construct_unclosed_parenthesis' => array( + 'testCaseFile' => 'IsValidatedConstructUnclosedParenthesisUnitTest.inc', + ), + ); + } + + /** + * Test is_validated() correctly respects scope boundaries between file scope and function scope. + * + * @dataProvider dataIsValidatedScopeBoundaries + * + * @param string $testCaseFile The test case file to parse. + * @param bool $expectedResult The expected return value. + * + * @return void + */ + public function testIsValidatedScopeBoundaries( $testCaseFile, $expectedResult ) { + $this->assertIsValidatedInFile( $testCaseFile, $expectedResult ); + } + + /** + * Data provider. + * + * @see testIsValidatedScopeBoundaries() + * + * @return array> + */ + public static function dataIsValidatedScopeBoundaries() { + return array( + 'function_validation_not_seen_at_file_scope' => array( + 'testCaseFile' => 'IsValidatedFunctionValidationNotSeenAtFileScopeUnitTest.inc', + 'expectedResult' => false, + ), + 'outer_validation_not_seen_in_function' => array( + 'testCaseFile' => 'IsValidatedOuterValidationNotSeenInFunctionUnitTest.inc', + 'expectedResult' => false, + ), + 'validated_at_file_scope' => array( + 'testCaseFile' => 'IsValidatedValidatedAtFileScopeUnitTest.inc', + 'expectedResult' => true, + ), + ); + } + + /** + * Test is_validated() with default parameter for $in_condition_only and a single $array_keys entry. + * + * @dataProvider dataIsValidated + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param bool $expectedResult The expected return value. + * + * @return void + */ + public function testIsValidated( $testMarker, $expectedResult ) { + $stackPtr = $this->getTargetToken( $testMarker, \T_VARIABLE ); + $result = ValidationHelper::is_validated( self::$phpcsFile, $stackPtr, array( 'key' ) ); + + $this->assertSame( $expectedResult, $result ); + } + + /** + * Data provider. + * + * @see testIsValidated() + * + * @return array> + */ + public static function dataIsValidated() { + return array( + // Cases that should return false. + 'not_validated' => array( + 'testMarker' => '/* testNotValidated */', + 'expectedResult' => false, + ), + 'outer_function_validation_not_counted' => array( + 'testMarker' => '/* testOuterFunctionValidationNotCounted */', + 'expectedResult' => false, + ), + 'closed_scope_validation_not_counted' => array( + 'testMarker' => '/* testClosedScopeValidationNotCounted */', + 'expectedResult' => false, + ), + 'arrow_function_validation_not_counted' => array( + 'testMarker' => '/* testArrowFunctionValidationNotCounted */', + 'expectedResult' => false, + ), + 'construct_wrong_variable_or_key' => array( + 'testMarker' => '/* testConstructWrongVariableOrKey */', + 'expectedResult' => false, + ), + 'function_name_used_as_constant' => array( + 'testMarker' => '/* testFunctionNameUsedAsConstant */', + 'expectedResult' => false, + ), + 'function_call_in_attribute' => array( + 'testMarker' => '/* testFunctionCallInAttribute */', + 'expectedResult' => false, + ), + 'function_call_non_global' => array( + 'testMarker' => '/* testFunctionCallNonGlobal */', + 'expectedResult' => false, + ), + 'function_call_namespaced' => array( + 'testMarker' => '/* testFunctionCallNamespaced */', + 'expectedResult' => false, + ), + 'function_call_missing_parameters' => array( + 'testMarker' => '/* testFunctionCallMissingParameters */', + 'expectedResult' => false, + ), + 'function_call_wrong_array_param' => array( + 'testMarker' => '/* testFunctionCallWrongArrayParam */', + 'expectedResult' => false, + ), + 'function_call_mismatched_key' => array( + 'testMarker' => '/* testFunctionCallMismatchedKey */', + 'expectedResult' => false, + ), + 'coalesce_no_match' => array( + 'testMarker' => '/* testCoalesceNoMatch */', + 'expectedResult' => false, + ), + + // Cases that should return true. + 'validated_with_isset' => array( + 'testMarker' => '/* testValidatedWithIsset */', + 'expectedResult' => true, + ), + 'validated_with_empty' => array( + 'testMarker' => '/* testValidatedWithEmpty */', + 'expectedResult' => true, + ), + 'validated_in_closure' => array( + 'testMarker' => '/* testValidatedInClosure */', + 'expectedResult' => true, + ), + 'function_call' => array( + 'testMarker' => '/* testFunctionCall */', + 'expectedResult' => true, + ), + 'function_call_mixed_case' => array( + 'testMarker' => '/* testFunctionCallMixedCase */', + 'expectedResult' => true, + ), + 'function_call_fully_qualified' => array( + 'testMarker' => '/* testFunctionCallFullyQualified */', + 'expectedResult' => true, + ), + 'function_call_fully_qualified_uppercase' => array( + 'testMarker' => '/* testFunctionCallFullyQualifiedUppercase */', + 'expectedResult' => true, + ), + 'validated_with_null_coalesce' => array( + 'testMarker' => '/* testValidatedWithNullCoalesce */', + 'expectedResult' => true, + ), + 'validated_with_coalesce_equal' => array( + 'testMarker' => '/* testValidatedWithCoalesceEqual */', + 'expectedResult' => true, + ), + ); + } + + /** + * Test is_validated() with $in_condition_only set to true returns false when the variable + * is not inside a condition or the condition has no parentheses. + * + * @dataProvider dataIsValidatedInConditionOnlyReturnsFalse + * + * @param string $testCaseFile The test case file to parse. + * + * @return void + */ + public function testIsValidatedInConditionOnlyReturnsFalse( $testCaseFile ) { + $this->assertIsValidatedInFile( $testCaseFile, false, true ); + } + + /** + * Data provider. + * + * @see testIsValidatedInConditionOnlyReturnsFalse() + * + * @return array> + */ + public static function dataIsValidatedInConditionOnlyReturnsFalse() { + return array( + 'no_condition' => array( + 'testCaseFile' => 'IsValidatedValidatedAtFileScopeUnitTest.inc', + ), + 'condition_without_parentheses' => array( + 'testCaseFile' => 'IsValidatedInConditionOnlyNoParenthesisUnitTest.inc', + ), + ); + } + + /** + * Test is_validated() with $in_condition_only set to true. + * + * @dataProvider dataIsValidatedInConditionOnly + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param bool $expectedResult The expected return value. + * + * @return void + */ + public function testIsValidatedInConditionOnly( $testMarker, $expectedResult ) { + $stackPtr = $this->getTargetToken( $testMarker, \T_VARIABLE ); + $result = ValidationHelper::is_validated( self::$phpcsFile, $stackPtr, array( 'key' ), true ); + + $this->assertSame( $expectedResult, $result ); + } + + /** + * Data provider. + * + * @see testIsValidatedInConditionOnly() + * + * @return array> + */ + public static function dataIsValidatedInConditionOnly() { + return array( + 'use_outside_condition' => array( + 'testMarker' => '/* testInConditionOnlyUseOutsideCondition */', + 'expectedResult' => false, + ), + 'use_inside_condition' => array( + 'testMarker' => '/* testInConditionOnlyUseInsideCondition */', + 'expectedResult' => true, + ), + ); + } + + /** + * Test is_validated() multi-level array key matching across validation paths. + * + * @dataProvider dataIsValidatedArrayKeys + * + * @param string $testMarker The comment which prefaces the target token in the test file. + * @param bool $expectedResult The expected return value. + * @param array|string $array_keys The array keys to check for. + * + * @return void + */ + public function testIsValidatedArrayKeys( $testMarker, $expectedResult, $array_keys ) { + $stackPtr = $this->getTargetToken( $testMarker, \T_VARIABLE ); + $result = ValidationHelper::is_validated( self::$phpcsFile, $stackPtr, $array_keys ); + + $this->assertSame( $expectedResult, $result ); + } + + /** + * Data provider. + * + * @see testIsValidatedArrayKeys() + * + * @return array|bool|string>> + */ + public static function dataIsValidatedArrayKeys() { + return array( + 'construct_subset' => array( + 'testMarker' => '/* testArrayKeysConstruct */', + 'expectedResult' => true, + 'array_keys' => array( 'key' ), + ), + 'construct_exact' => array( + 'testMarker' => '/* testArrayKeysConstruct */', + 'expectedResult' => true, + 'array_keys' => array( 'key', 'sub' ), + ), + 'construct_superset' => array( + 'testMarker' => '/* testArrayKeysConstruct */', + 'expectedResult' => false, + 'array_keys' => array( 'key', 'sub', 'deeper' ), + ), + 'construct_wrong_order' => array( + 'testMarker' => '/* testArrayKeysConstruct */', + 'expectedResult' => false, + 'array_keys' => array( 'sub', 'key' ), + ), + 'construct_string_key_valid' => array( + 'testMarker' => '/* testArrayKeysConstruct */', + 'expectedResult' => true, + 'array_keys' => 'key', + ), + 'construct_string_key_invalid' => array( + 'testMarker' => '/* testArrayKeysConstruct */', + 'expectedResult' => false, + 'array_keys' => 'other', + ), + 'construct_key_mismatch' => array( + 'testMarker' => '/* testConstructParamKeyMismatch */', + 'expectedResult' => false, + 'array_keys' => array( 'other' ), + ), + 'construct_empty_array_keys' => array( + 'testMarker' => '/* testValidatedWithIsset */', + 'expectedResult' => true, + 'array_keys' => array(), + ), + 'function_call_all_in_array_param' => array( + 'testMarker' => '/* testArrayKeysFunctionCall */', + 'expectedResult' => true, + 'array_keys' => array( 'key' ), + ), + 'function_call_split_keys' => array( + 'testMarker' => '/* testArrayKeysFunctionCall */', + 'expectedResult' => true, + 'array_keys' => array( 'key', 'sub' ), + ), + 'function_call_superset' => array( + 'testMarker' => '/* testArrayKeysFunctionCall */', + 'expectedResult' => false, + 'array_keys' => array( 'key', 'sub', 'deeper' ), + ), + 'function_call_wrong_order' => array( + 'testMarker' => '/* testArrayKeysFunctionCall */', + 'expectedResult' => false, + 'array_keys' => array( 'sub', 'key' ), + ), + 'function_call_key_param_mismatch' => array( + 'testMarker' => '/* testArrayKeysFunctionCallKeyParamMismatch */', + 'expectedResult' => false, + 'array_keys' => array( 'key', 'sub' ), + ), + 'function_call_empty_array_keys' => array( + 'testMarker' => '/* testFunctionCallMixedCase */', + 'expectedResult' => true, + 'array_keys' => array(), + ), + 'coalesce_subset' => array( + 'testMarker' => '/* testArrayKeysCoalesce */', + 'expectedResult' => true, + 'array_keys' => array( 'key' ), + ), + 'coalesce_exact' => array( + 'testMarker' => '/* testArrayKeysCoalesce */', + 'expectedResult' => true, + 'array_keys' => array( 'key', 'sub' ), + ), + 'coalesce_superset' => array( + 'testMarker' => '/* testArrayKeysCoalesce */', + 'expectedResult' => false, + 'array_keys' => array( 'key', 'sub', 'deeper' ), + ), + 'coalesce_wrong_order' => array( + 'testMarker' => '/* testArrayKeysCoalesce */', + 'expectedResult' => false, + 'array_keys' => array( 'sub', 'key' ), + ), + ); + } + + /** + * Parse a separate .inc file and assert the result of is_validated(). + * + * Temporarily swaps self::$phpcsFile so that getTargetToken() works on the parsed file. + * + * @param string $testCaseFile The test case file to parse. + * @param bool $expectedResult The expected return value. + * @param bool $inConditionOnly What to pass as the $in_condition_only parameter. + * + * @return void + */ + private function assertIsValidatedInFile( $testCaseFile, $expectedResult, $inConditionOnly = false ) { + $originalFile = self::$phpcsFile; + self::$phpcsFile = self::parseFile( + __DIR__ . '/' . $testCaseFile, + $originalFile->ruleset, + $originalFile->config + ); + + $stackPtr = $this->getTargetToken( '/* testValidationTarget */', \T_VARIABLE ); + + $this->assertSame( + $expectedResult, + ValidationHelper::is_validated( self::$phpcsFile, $stackPtr, array( 'key' ), $inConditionOnly ) + ); + + self::$phpcsFile = $originalFile; + } +} diff --git a/WordPress/Tests/Helpers/ValidationHelper/IsValidatedValidatedAtFileScopeUnitTest.inc b/WordPress/Tests/Helpers/ValidationHelper/IsValidatedValidatedAtFileScopeUnitTest.inc new file mode 100644 index 0000000000..79b2ada2f1 --- /dev/null +++ b/WordPress/Tests/Helpers/ValidationHelper/IsValidatedValidatedAtFileScopeUnitTest.inc @@ -0,0 +1,6 @@ +