diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh new file mode 100755 index 0000000..73bb4c7 --- /dev/null +++ b/bin/install-wp-tests.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash + +if [ $# -lt 3 ]; then + echo "usage: $0 [db-host] [wp-version] [skip-database-creation]" + exit 1 +fi + +DB_NAME=$1 +DB_USER=$2 +DB_PASS=$3 +DB_HOST=${4-localhost} +WP_VERSION=${5-latest} +SKIP_DB_CREATE=${6-false} + +WP_TESTS_DIR=${WP_TESTS_DIR-/tmp/wordpress-tests-lib} +WP_CORE_DIR=${WP_CORE_DIR-/tmp/wordpress/} + +download() { + if [ `which curl` ]; then + curl -s "$1" > "$2"; + elif [ `which wget` ]; then + wget -nv -O "$2" "$1" + fi +} + +if [[ $WP_VERSION =~ [0-9]+\.[0-9]+(\.[0-9]+)? ]]; then + WP_TESTS_TAG="tags/$WP_VERSION" +elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + WP_TESTS_TAG="trunk" +else + # http serves a single offer, whereas https serves multiple. we only want one + download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json + grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json + LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//') + if [[ -z "$LATEST_VERSION" ]]; then + echo "Latest WordPress version could not be found" + exit 1 + fi + WP_TESTS_TAG="tags/$LATEST_VERSION" +fi + +set -ex + +install_wp() { + + if [ -d $WP_CORE_DIR ]; then + return; + fi + + mkdir -p $WP_CORE_DIR + + if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then + mkdir -p /tmp/wordpress-nightly + download https://wordpress.org/nightly-builds/wordpress-latest.zip /tmp/wordpress-nightly/wordpress-nightly.zip + unzip -q /tmp/wordpress-nightly/wordpress-nightly.zip -d /tmp/wordpress-nightly/ + mv /tmp/wordpress-nightly/wordpress/* $WP_CORE_DIR + else + if [ $WP_VERSION == 'latest' ]; then + local ARCHIVE_NAME='latest' + else + local ARCHIVE_NAME="wordpress-$WP_VERSION" + fi + download https://wordpress.org/${ARCHIVE_NAME}.tar.gz /tmp/wordpress.tar.gz + tar --strip-components=1 -zxmf /tmp/wordpress.tar.gz -C $WP_CORE_DIR + fi + + download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php +} + +install_test_suite() { + # portable in-place argument for both GNU sed and Mac OSX sed + if [[ $(uname -s) == 'Darwin' ]]; then + local ioption='-i .bak' + else + local ioption='-i' + fi + + # set up testing suite if it doesn't yet exist + if [ ! -d $WP_TESTS_DIR ]; then + # set up testing suite + mkdir -p $WP_TESTS_DIR + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes + svn co --quiet https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data + fi + + if [ ! -f wp-tests-config.php ]; then + download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php + # remove all forward slashes in the end + WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::") + sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php + sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php + fi + +} + +install_db() { + + if [ ${SKIP_DB_CREATE} = "true" ]; then + return 0 + fi + + # parse DB_HOST for port or socket references + local PARTS=(${DB_HOST//\:/ }) + local DB_HOSTNAME=${PARTS[0]}; + local DB_SOCK_OR_PORT=${PARTS[1]}; + local EXTRA="" + + if ! [ -z $DB_HOSTNAME ] ; then + if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then + EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp" + elif ! [ -z $DB_SOCK_OR_PORT ] ; then + EXTRA=" --socket=$DB_SOCK_OR_PORT" + elif ! [ -z $DB_HOSTNAME ] ; then + EXTRA=" --host=$DB_HOSTNAME --protocol=tcp" + fi + fi + + # create database + mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA +} + +install_wp +install_test_suite +install_db diff --git a/classes/class-exoskeleton.php b/classes/class-exoskeleton.php old mode 100644 new mode 100755 index 9631da5..68756a0 --- a/classes/class-exoskeleton.php +++ b/classes/class-exoskeleton.php @@ -292,7 +292,14 @@ private function back_off_please( $rule_id, $matched_rule, $lock ) { @header( "Cache-Control: public max-age=$retry_after" ); status_header( 429, 'Exoskeleton: too many requests for this endpoint. Please consult Retry-After and come back later. Meanwhile enjoy a well-deserved REST' ); @header( "Retry-After: $retry_after" ); + + // Do not die if testing. + if ( defined( 'PHPUNIT_EXOSKELETON_TESTING' ) ) { + throw new Exception( 'locked' ); + } + die(); + } /** diff --git a/phpcs.ruleset.xml b/phpcs.ruleset.xml new file mode 100644 index 0000000..210c25a --- /dev/null +++ b/phpcs.ruleset.xml @@ -0,0 +1,10 @@ + + + Generally-applicable sniffs for WordPress plugins + + + + + */node_modules/* + */vendor/* + diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100755 index 0000000..77950a8 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,23 @@ + + + + ./tests/ + + + + + exoskeleton.php + classes/class-exoskeleton.php + + + + + + diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..09b7457 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,25 @@ +rules = []; + } + + /** + * Get instance of Exoskeleton + */ + function test_get_instance() { + $this->assertInstanceOf( Exoskeleton::class, Exoskeleton::get_instance() ); + } + + /** + * Test adding a valid rule + * + * @dataProvider validRuleProvider + * @param array $rule Valid exoskeleton rule definition. + */ + function test_add_valid_rule( $rule ) { + $this->assertTrue( exoskeleton_add_rule( $rule ) ); + } + + /** + * Test adding a valid rule with multiple methods + */ + function test_add_valid_rule_with_multiple_methods() { + $rule = $this->getValidRuleHelper(); + $rule['method'] = 'POST,GET'; + $this->assertTrue( exoskeleton_add_rule( $rule ) ); + } + + /** + * Test adding rule fails if any required field is missing + * + * @dataProvider requiredFieldsProvider + * @param string $field required field names. + */ + public function test_adding_rule_fails_missing_required_fields( $field ) { + $rule = $this->getValidRuleHelper(); + unset( $rule[ $field ] ); + $this->assertFalse( exoskeleton_add_rule( $rule ) ); + } + + /** + * Test adding multiple valid rules + */ + function test_adding_multiple_valid_rules() { + $ruleset = $this->validRuleProvider(); + $this->assertNull( exoskeleton_add_rules( $ruleset[0] ) ); + $instance = Exoskeleton::get_instance(); + $this->assertEquals( 3, count( $instance->rules ) ); + } + + + /** + * Test adding multiple rules including invalid + */ + function test_adding_multiple_rules_including_invalid() { + $ruleset = $this->validRuleProvider(); + unset( $ruleset[0][1]['lockout'] ); + $this->assertNull( exoskeleton_add_rules( $ruleset[0] ) ); + $instance = Exoskeleton::get_instance(); + $this->assertEquals( 2, count( $instance->rules ) ); + } + + /** + * Test adding a valid rule for a custom route + * Exoskeleton makes no check for existence of custom route. + * + */ + function test_add_valid_custom_route_rule() { + $rule = $this->getValidRuleHelper(); + $rule['route'] = '/custom/route/that/does/not/exist'; + + $this->assertTrue( exoskeleton_add_rule( $rule ) ); + } + + /** + * Check that exoskeleton will not overwrite an already existing rule + */ + function test_internal_add_rule_method_fails_when_adding_existing_rule() { + $rule = $this->getValidRuleHelper(); + $exoskeleton = Exoskeleton::get_instance(); + $this->assertTrue( exoskeleton_add_rule( $rule ) ); + $this->assertFalse( $exoskeleton->add_rule( $rule ) ); + } + + /** + * Check process_custom_routes picks up pre-registered routes with exoskeleton rules. + */ + function test_process_custom_route_adds_rules() { + global $wp_rest_server; + + add_action( 'rest_api_init', function () { + register_rest_route( 'exoskeleton/v1', '/testing/(?P\d+)', array( + 'callback' => [ $this, 'custom_route_callback' ], + 'exoskeleton' => [ + 'window' => 10, + 'limit' => 5, + 'lockout' => 20, + ], + ) ); + } ); + + $exoskeleton = Exoskeleton::get_instance(); + $this->assertEquals( array(), $exoskeleton->rules ); + + // Remove action to prevent method being called automatically. + remove_action( 'rest_api_init', [ $exoskeleton, 'process_custom_routes' ], 999, 1 ); + $response = rest_do_request( new WP_REST_Request( 'GET', '/exoskeleton/v1/testing/1' ) ); + + // Explicitly call method. + $exoskeleton->process_custom_routes( $wp_rest_server ); + add_action( 'rest_api_init', [ $exoskeleton, 'process_custom_routes' ], 999, 1 ); + + $this->assertNotEquals( array(), $exoskeleton->rules ); + $this->assertEquals( 1, count( $exoskeleton->rules ) ); + $this->assertEquals( '9afa7edee56d7fe5ce41c557fc4f812d_GET', key( $exoskeleton->rules ) ); + + } + + /** + * Simple method to provide a callback for custom routes during testing + * + * @param mixed $data data passed into the request for this route. + * @return mixed returns the data that was input to provide a valid output. + */ + public function custom_route_callback( $data ) { + return new WP_REST_Response( $data, 200 ); + } + + /** + * Provides required fields for exoskeleton_add_rule + * Both the method and treat_head_like_get fields are defaulted + * when called via the add_rule method so are not required here + * + * @return array Valid exoskeleton rule methods + */ + public function requiredFieldsProvider() { + return [ + [ 'route' ], + [ 'window' ], + [ 'limit' ], + [ 'lockout' ], + ]; + } + + + /** + * Provides valid exoskeleton rule definitions + * + * @return array Valid exoskeleton rule methods + */ + public function validRuleProvider() { + return [ + [ + [ + 'route' => '/wp/v2/posts', + 'window' => 10, + 'limit' => 25, + 'lockout' => 200, + 'method' => 'any', + ],[ + 'route' => '/wp/v2/post/1', + 'window' => 100, + 'limit' => 5, + 'lockout' => 60, + 'method' => 'GET', + ],[ + 'route' => '/wp/v2/post/2', + 'window' => 90, + 'limit' => 2, + 'lockout' => 30, + 'method' => 'GET', + ], + ], + ]; + } + + /** + * Provides multiple valid exoskeleton rule definitions in single test + * + * @return array Valid exoskeleton rule methods + */ + public function multipleValidRuleProvider() { + return [ + [ + 'route' => '/wp/v2/posts', + 'window' => 10, + 'limit' => 25, + 'lockout' => 200, + 'method' => 'any', + ],[ + 'route' => '/wp/v2/post/1', + 'window' => 100, + 'limit' => 5, + 'lockout' => 60, + 'method' => 'GET', + ],[ + 'route' => '/wp/v2/post/2', + 'window' => 90, + 'limit' => 2, + 'lockout' => 30, + 'method' => 'GET', + ], + ]; + } + + /** + * Get a single valid rule definition from the validRuleProvider + * + * @return array Valid Exoskeleton rule definition + */ + public function getValidRuleHelper() { + $rules = $this->validRuleProvider(); + return $rules[0][0]; + + } + +} diff --git a/tests/test-exoskeleton-rest-api-calls.php b/tests/test-exoskeleton-rest-api-calls.php new file mode 100755 index 0000000..f5e5c9b --- /dev/null +++ b/tests/test-exoskeleton-rest-api-calls.php @@ -0,0 +1,290 @@ +\d+)', array( + 'methods' => 'GET', + 'callback' => [ $this, 'custom_route_callback' ], + ) ); + } ); + } + + + /** + * Post test cleanup + */ + function tearDown() { + parent::tearDown(); + // Clear up any added rules + $instance = Exoskeleton::get_instance(); + $instance->rules = []; + } + + /** + * Test key generation + * + * @dataProvider limitTestingProvider + * @param array $rule exoskeleton rule array. + * @param array $test extra test data. + */ + public function test_key_generation( $rule, $test ) { + exoskeleton_add_rule( $rule ); + $exoskeleton = Exoskeleton::get_instance(); + $this->assertEquals( $test['key'], key( $exoskeleton->rules ) ); + } + + /** + * Simple method to provide a callback for custom routes during testing + * + * @param mixed $data data passed into the request for this route. + * @return mixed returns the data that was input to provide a valid output. + */ + public function custom_route_callback( $data ) { + return new WP_REST_Response( $data, 200 ); + } + + + /** + * Check limits are honoured if a route is registered with exoskeleton limits. + */ + function test_pre_registered_custom_route_rules() { + global $wp_rest_server; + add_action( 'rest_api_init', function () { + register_rest_route( 'exoskeleton/v1', '/test_pre_registered_custom_route_rules/(?P\d+)', array( + 'methods' => 'GET', + 'callback' => [ $this, 'custom_route_callback' ], + 'exoskeleton' => [ + 'window' => 10, + 'method' => 'GET', + 'limit' => 5, + 'lockout' => 20, + ], + ), true ); + } ); + do_action( 'rest_api_init', $wp_rest_server ); + + for ( $request = 1; $request <= 6; ++$request ) { + if ( $request < 5 ) { + $response = rest_do_request( new WP_REST_Request( 'GET', '/exoskeleton/v1/test_pre_registered_custom_route_rules/1' ) ); + $this->assertEquals( 200, $response->status ); + } else { + $this->expectException( Exception::class ); + $this->expectExceptionMessage( 'locked' ); + $response = rest_do_request( new WP_REST_Request( 'GET', '/exoskeleton/v1/test_pre_registered_custom_route_rules/1' ) ); + } + } + } + + + /** + * Check that requests do get properly limited + * + * @dataProvider limitTestingProvider + * @param array $rule exoskeleton rule array. + * @param array $test extra test data. + */ + public function test_limits_are_applied( $rule, $test ) { + if ( empty( $test['method'] ) ) { + $test['method'] = $rule['method']; + } + $this->assertTrue( exoskeleton_add_rule( $rule ) ); + for ( $request = 1; $request <= $rule['limit'] + 1; ++$request ) { + if ( $request < $rule['limit'] ) { + $response = rest_do_request( new WP_REST_Request( $test['method'], $rule['route'] ) ); + $this->assertEquals( 200, $response->status ); + } else { + $this->expectException( Exception::class ); + $this->expectExceptionMessage( 'locked' ); + $response = rest_do_request( new WP_REST_Request( $test['method'], $rule['route'] ) ); + } + } + } + + + /** + * Make sure that requests falling outside the window do not get limited. + * + * @dataProvider limitTestingProvider + * @param array $rule exoskeleton rule array. + * @param array $test extra test data. + */ + public function test_limit_window_expiry( $rule, $test ) { + if ( empty( $test['method'] ) ) { + $test['method'] = $rule['method']; + } + $this->assertTrue( exoskeleton_add_rule( $rule ) ); + for ( $request = 1; $request <= $rule['limit'] + 1; ++$request ) { + if ( $request < $rule['limit'] ) { + $response = rest_do_request( new WP_REST_Request( $test['method'], $rule['route'] ) ); + $this->assertEquals( 200, $response->status ); + } else { + sleep( $rule['window'] + 1 ); + $response = rest_do_request( new WP_REST_Request( $test['method'], $rule['route'] ) ); + $this->assertEquals( 200, $response->status ); + } + } + } + + /** + * Limits should only be applied if the correct method is requested + * + * @dataProvider limitTestingProvider + * @param array $rule exoskeleton rule array. + * @param array $test extra test data. + */ + public function test_different_methods_not_limited( $rule, $test ) { + // Change the test or rule method for testing. + if ( empty( $test['method'] ) ) { + $test['method'] = 'HEAD'; + } else { + $rule['method'] = 'POST'; + } + $this->assertTrue( exoskeleton_add_rule( $rule ) ); + for ( $request = 1; $request <= $rule['limit'] + 1; ++$request ) { + $response = rest_do_request( new WP_REST_Request( $test['method'], $rule['route'] ) ); + $this->assertEquals( 200, $response->status ); + } + } + + /** + * Limits should apply across methods if 'any' is the rule method + * + * @dataProvider limitTestingProvider + * @param array $rule exoskeleton rule array. + * @param array $test extra test data. + */ + public function test_limits_apply_across_methods_for_any( $rule, $test ) { + // make sure the created rule applies to any method + $rule['method'] = 'any'; + if ( empty( $test['method'] ) ) { + $test['method'] = 'HEAD'; + } + $this->assertTrue( exoskeleton_add_rule( $rule ) ); + for ( $request = 1; $request <= $rule['limit'] + 1; ++$request ) { + if ( $request < $rule['limit'] ) { + // Initially test up to limit with one method + $response = rest_do_request( new WP_REST_Request( $test['method'], $rule['route'] ) ); + $this->assertEquals( 200, $response->status ); + } else { + // Now call a different method and ensure it is limited + $test['method'] = 'post'; + $this->expectException( Exception::class ); + $this->expectExceptionMessage( 'locked' ); + $response = rest_do_request( new WP_REST_Request( $test['method'], $rule['route'] ) ); + } + } + } + + + /** + * Limits should apply across methods if multiple methods are specified + * + * @dataProvider limitTestingProvider + * @param array $rule exoskeleton rule array. + * @param array $test extra test data. + */ + public function test_limits_apply_across_methods_for_multi_method_rules( $rule, $test ) { + // make sure the created rule applies to any method + $rule['method'] = 'HEAD,GET'; + $rule['treat_head_as_get'] = false; + $test['method'] = 'GET'; + $this->assertTrue( exoskeleton_add_rule( $rule ) ); + for ( $request = 1; $request <= $rule['limit'] + 1; ++$request ) { + if ( $request < ( $rule['limit'] - 1 ) ) { + // Initially test with one method + $response = rest_do_request( new WP_REST_Request( $test['method'], $rule['route'] ) ); + $this->assertEquals( 200, $response->status ); + } elseif ( ( $rule['limit'] - 1 ) === $request ) { + // Test second method allowed + $test['method'] = 'HEAD'; + $response = rest_do_request( new WP_REST_Request( $test['method'], $rule['route'] ) ); + $this->assertEquals( 200, $response->status ); + } else { + // Now call over the limit and ensure it is limited + $this->expectException( Exception::class ); + $this->expectExceptionMessage( 'locked' ); + $response = rest_do_request( new WP_REST_Request( $test['method'], $rule['route'] ) ); + } + } + } + + /** + * Data provider + * + * @return array exoskeleton rule information and test data. + */ + public function limitTestingProvider() { + return [ + [ + 'rule' => [ + 'route' => '/wp/v2/posts', + 'window' => 1, + 'limit' => 3, + 'lockout' => 5, + 'method' => 'GET', + 'treat_head_like_get' => false, + ], + 'test' => [ + 'key' => '01dd28291a6b5b95802281831ec3d6f5_GET', + ], + ], + [ + 'rule' => [ + 'route' => '/wp/v2/categories', + 'window' => 2, + 'limit' => 6, + 'lockout' => 5, + 'method' => 'GET', + 'treat_head_like_get' => false, + ], + 'test' => [ + 'key' => 'fd568c1eb104fbad04765b9f2f0100ed_GET', + ], + ], + [ + 'rule' => [ + 'route' => '/wp/v2/categories', + 'window' => 2, + 'limit' => 10, + 'lockout' => 5, + 'method' => 'GET', + 'treat_head_like_get' => true, + ], + 'test' => [ + 'method' => 'HEAD', + 'key' => '6ae5a5054b0a306061f10b9c1b193183_GET', + ], + ], + [ + 'rule' => [ + 'route' => '/exoskeleton/v1/exoskeleton/10', + 'window' => 1, + 'limit' => 3, + 'lockout' => 5, + 'method' => 'GET', + 'treat_head_like_get' => false, + ], + 'test' => [ + 'key' => 'b09a74d523deb0962e21cafaadc63679_GET', + ], + ], + ]; + } + +} diff --git a/tests/test-exoskeleton-rule_validation.php b/tests/test-exoskeleton-rule_validation.php new file mode 100755 index 0000000..524da10 --- /dev/null +++ b/tests/test-exoskeleton-rule_validation.php @@ -0,0 +1,193 @@ +rules = []; + } + + + /** + * Check that exoskeleton fails to validate an invalid rule + */ + function test_validate_rule_fails_when_adding_invalid_rule() { + $exoskeleton = Exoskeleton::get_instance(); + $rule = $this->getValidRuleHelper(); + unset( $rule['method'] ); + $this->assertFalse( $exoskeleton->validate_rule( $rule ) ); + } + + /** + * Check that exoskeleton validates rules with valid rule + */ + function test_validate_rule_passes_when_adding_a_valid_rule() { + $exoskeleton = Exoskeleton::get_instance(); + $rule = $this->getValidRuleHelper(); + $this->assertTrue( $exoskeleton->validate_rule( $rule ) ); + } + + + /** + * Test validation passes available methods + * + * @dataProvider validationMethodsProvider + * @param string $method Valid method name for validation. + */ + public function test_validate_rule_passes_all_available_methods( $method ) { + $rule = $this->getValidRuleHelper(); + $rule['method'] = $method; + $exoskeleton = Exoskeleton::get_instance(); + $this->assertTrue( $this->invokeMethod( $exoskeleton, 'validate_rule', array( $rule ) ) ); + } + + /** + * Test validation fails is any required field is missing + * + * @dataProvider requiredFieldsProvider + * @param string $field required field names. + */ + public function test_validate_rule_fails_missing_required_fields( $field ) { + $rule = $this->getValidRuleHelper(); + unset( $rule[ $field ] ); + $exoskeleton = Exoskeleton::get_instance(); + $this->assertFalse( $this->invokeMethod( $exoskeleton, 'validate_rule', array( $rule ) ) ); + } + + /** + * Test validation fails if fields contain invalid data + * + * @dataProvider invalidFieldValueProvider + * @param string $field array field_name => invalid_test_value. + */ + public function test_validate_rule_fails_invalid_field_values( $field ) { + $rule = $this->getValidRuleHelper(); + $rule[ key( $field ) ] = $field; + $exoskeleton = Exoskeleton::get_instance(); + $this->assertFalse( $this->invokeMethod( $exoskeleton, 'validate_rule', array( $rule ) ) ); + } + + + /** + * Data provider + * + * @return array Valid exoskeleton rule methods + */ + public function validationMethodsProvider() { + return [ + [ 'GET' ], + [ 'POST' ], + [ 'PUT' ], + [ 'PATCH' ], + [ 'DELETE' ], + [ 'HEAD' ], + [ 'any' ], + ]; + } + + /** + * Data provider + * + * @return array Valid exoskeleton rule methods + */ + public function requiredFieldsProvider() { + return [ + [ 'route' ], + [ 'method' ], + [ 'window' ], + [ 'limit' ], + [ 'lockout' ], + [ 'treat_head_like_get' ], + ]; + } + + /** + * Data provider + * + * @return array Valid exoskeleton rule methods + */ + public function invalidFieldValueProvider() { + return [ + [ + [ + 'window' => 'string', + ], + ], + [ + [ + 'window' => -1, + ], + ], + [ + [ + 'limit' => 'string', + ], + ], + [ + [ + 'limit' => -1, + ], + ], + [ + [ + 'lockout' => 'string', + ], + ], + [ + [ + 'treat_head_like_get' => 'string', + ], + ], + ]; + } + + + /** + * Get a single valid rule definition + * + * @return array Valid Exoskeleton rule definition + */ + public function getValidRuleHelper() { + return [ + 'route' => '/wp/v2/posts', + 'window' => 5, + 'limit' => 2, + 'lockout' => 30, + 'method' => 'any', + 'treat_head_like_get' => false, + ]; + + } + + + /** + * Call protected/private method of a class. + * + * @param object $object Instantiated object that we will run method on. + * @param string $method_name Method name to call. + * @param array $parameters Array of parameters to pass into method. + * + * @return mixed Method return. + */ + public function invokeMethod( &$object, $method_name, array $parameters = array() ) { + $reflection = new \ReflectionClass( get_class( $object ) ); + $method = $reflection->getMethod( $method_name ); + $method->setAccessible( true ); + + return $method->invokeArgs( $object, $parameters ); + } + +}