diff --git a/WP_Auth0.php b/WP_Auth0.php index e4350926..c8e48665 100644 --- a/WP_Auth0.php +++ b/WP_Auth0.php @@ -508,6 +508,7 @@ private function autoloader( $class ) { $paths = array( $source_dir, $source_dir . 'admin/', + $source_dir . 'api/', $source_dir . 'exceptions/', $source_dir . 'wizard/', $source_dir . 'initial-setup/', diff --git a/assets/js/die-with-verify-email.js b/assets/js/die-with-verify-email.js index 783f1f65..9f27be20 100644 --- a/assets/js/die-with-verify-email.js +++ b/assets/js/die-with-verify-email.js @@ -1,6 +1,7 @@ -/* globals jQuery, console, WPAuth0EmailVerification */ +/* globals jQuery, alert, WPAuth0EmailVerification */ jQuery( document ).ready( function ($) { + 'use strict'; var $resendLink = $( '#js-a0-resend-verification' ); @@ -8,23 +9,25 @@ jQuery( document ).ready( function ($) { var postData = { action: 'resend_verification_email', - nonce: WPAuth0EmailVerification.nonce, + _ajax_nonce: WPAuth0EmailVerification.nonce, sub: WPAuth0EmailVerification.sub }; + var errorMsg = WPAuth0EmailVerification.e_msg; $.post( WPAuth0EmailVerification.ajaxUrl, postData ) - .done( function( data ) { - - if ( 'success' === data ) { + .done( function( response ) { + if ( response.success ) { $resendLink.after( WPAuth0EmailVerification.s_msg ); $resendLink.remove(); } else { - alert( WPAuth0EmailVerification.e_msg ); + if ( response.data && response.data.error ) { + errorMsg = response.data.error; + } + alert( errorMsg ); } - } ) .fail( function() { - alert( WPAuth0EmailVerification.e_msg ); + alert( errorMsg ); } ); } ); } ); \ No newline at end of file diff --git a/lib/WP_Auth0_Api_Client.php b/lib/WP_Auth0_Api_Client.php index 2d7b48cc..a61d4d3c 100755 --- a/lib/WP_Auth0_Api_Client.php +++ b/lib/WP_Auth0_Api_Client.php @@ -120,6 +120,14 @@ public static function ro( $domain, $client_id, $username, $password, $connectio } + /** + * Validate the scopes of the API token. + * TODO: Deprecate, not used. + * + * @param string $app_token - API token. + * + * @return bool + */ public static function validate_user_token( $app_token ) { if ( empty( $app_token ) ) { @@ -149,20 +157,13 @@ public static function validate_user_token( $app_token ) { } /** - * Get required telemetry header + * Get required telemetry header. + * TODO: Refactor to use WP_Auth0_Api_Abstract::get_info_headers and deprecate. * * @return array */ public static function get_info_headers() { - $header_value = array( - 'name' => 'wp-auth0', - 'version' => WPA0_VERSION, - 'environment' => array( - 'PHP' => phpversion(), - 'WordPress' => get_bloginfo( 'version' ), - ), - ); - return array( 'Auth0-Client' => base64_encode( wp_json_encode( $header_value ) ) ); + return WP_Auth0_Api_Abstract::get_info_headers(); } /** @@ -190,6 +191,7 @@ private static function get_headers( $token = '', $content_type = 'application/j return $headers; } + public static function get_token( $domain, $client_id, $client_secret, $grantType = 'client_credentials', $extraBody = null ) { if ( ! is_array( $extraBody ) ) { $body = array(); @@ -224,6 +226,7 @@ public static function get_token( $domain, $client_id, $client_secret, $grantTyp /** * Get a client_credentials token using default stored connection info + * TODO: Change implementations to use WP_Auth0_Api_Abstract and deprecate. * * @since 3.4.1 * @@ -276,6 +279,9 @@ public static function get_user_info( $domain, $access_token ) { ); } + /** + * TODO: Deprecate, not used. + */ public static function search_users( $domain, $jwt, $q = '', $page = 0, $per_page = 100, $include_totals = false, $sort = 'user_id:1' ) { $include_totals = $include_totals ? 'true' : 'false'; @@ -296,7 +302,8 @@ public static function search_users( $domain, $jwt, $q = '', $page = 0, $per_pag } /** - * Trigger a verification email re-send + * Trigger a verification email re-send. + * TODO: Deprecate, not used. * * @since 3.5.0 * @@ -349,6 +356,9 @@ public static function get_user( $domain, $jwt, $user_id ) { } + /** + * TODO: Deprecate, not used. + */ public static function create_user( $domain, $jwt, $data ) { $endpoint = "https://$domain/api/v2/users"; @@ -524,6 +534,9 @@ public static function create_client( $domain, $app_token, $name ) { return json_decode( $response['body'] ); } + /** + * TODO: Deprecate, not used. + */ public static function search_clients( $domain, $app_token ) { $endpoint = "https://$domain/api/v2/clients"; @@ -810,6 +823,9 @@ public static function get_connection( $domain, $app_token, $id ) { return json_decode( $response['body'] ); } + /** + * TODO: Deprecate, not used. + */ public static function get_current_user( $domain, $app_token ) { list( $head, $payload, $signature ) = explode( '.', $app_token ); $decoded = json_decode( JWT::urlsafeB64Decode( $payload ) ); @@ -852,6 +868,9 @@ public static function update_connection( $domain, $app_token, $id, $payload ) { return json_decode( $response['body'] ); } + /** + * TODO: Deprecate, not used. + */ public static function delete_connection( $domain, $app_token, $id ) { $endpoint = "https://$domain/api/v2/connections/$id"; @@ -982,6 +1001,9 @@ public static function change_password( $domain, $payload ) { return json_decode( $response['body'] ); } + /** + * TODO: Deprecate, not used. + */ public static function link_users( $domain, $app_token, $main_user_id, $user_id, $provider, $connection_id = null ) { $endpoint = "https://$domain/api/v2/users/$main_user_id/identities"; diff --git a/lib/WP_Auth0_Email_Verification.php b/lib/WP_Auth0_Email_Verification.php index 7f224985..83bdd1d5 100644 --- a/lib/WP_Auth0_Email_Verification.php +++ b/lib/WP_Auth0_Email_Verification.php @@ -1,37 +1,62 @@ api_jobs_resend = $api_jobs_resend; + } + + /** + * Set up hooks tied to functions that can be dequeued. + * + * @codeCoverageIgnore - Called at startup, tested in TestEmailVerification::testHooks() */ public static function init() { add_action( 'wp_ajax_nopriv_resend_verification_email', 'wp_auth0_ajax_resend_verification_email' ); } /** - * Stop the login process and show email verification prompt + * Stop the login process and show email verification prompt. * - * @param object $userinfo + * @param object $userinfo - User profile object returned from Auth0. */ public static function render_die( $userinfo ) { $user_id = isset( $userinfo->user_id ) ? $userinfo->user_id : $userinfo->sub; - $html = sprintf( '
%s
', __( 'This site requires a verified email address. ', 'wp-auth0' ) ); + $html = sprintf( '%s
', __( 'This site requires a verified email address.', 'wp-auth0' ) ); - // Only provide resend verification link for DB connection users + // Only provide resend verification link for DB connection users. if ( 0 === strpos( $user_id, 'auth0|' ) ) { $html .= sprintf( ' - - - - ', + + + + ', __( 'Resend verification email.', 'wp-auth0' ), wp_login_url(), time(), @@ -40,12 +65,7 @@ public static function render_die( $userinfo ) { esc_js( $user_id ), esc_js( wp_create_nonce( self::RESEND_NONCE_ACTION ) ), esc_js( __( 'Something went wrong; please login and try again.', 'wp-auth0' ) ), - esc_js( - sprintf( - __( 'Email successfully re-sent to %s!', 'wp-auth0' ), - $userinfo->email - ) - ), + esc_js( __( 'Email successfully re-sent to ', 'wp-auth0' ) . $userinfo->email ), '//code.jquery.com/jquery-1.12.4.js', WPA0_PLUGIN_URL . 'assets/js/die-with-verify-email.js?ver=' . WPA0_VERSION ); @@ -56,18 +76,53 @@ public static function render_die( $userinfo ) { } /** - * AJAX handler to request that the verification email be resent - * Triggered in $this->render_die + * AJAX handler to request that the verification email be resent. + * TODO: Deprecate, use $this->resend_verification_email() + * + * @codeCoverageIgnore - Not adding tests for soon-to-be-deprecated methods. */ public static function ajax_resend_email() { - check_ajax_referer( self::RESEND_NONCE_ACTION, 'nonce' ); + check_ajax_referer( self::RESEND_NONCE_ACTION ); if ( ! empty( $_POST['sub'] ) ) { - echo WP_Auth0_Api_Client::resend_verification_email( sanitize_text_field( $_POST['sub'] ) ) ? 'success' : 'fail'; + $user_id = sanitize_text_field( $_POST['sub'] ); + $result = WP_Auth0_Api_Client::resend_verification_email( $user_id ); + echo $result ? 'success' : 'fail'; } - exit; + } + + /** + * AJAX handler to request that the verification email be resent. + * Triggered in $this->render_die + * + * @codeCoverageIgnore - Tested in TestEmailVerification::testResendVerificationEmail() + */ + public function resend_verification_email() { + check_ajax_referer( self::RESEND_NONCE_ACTION ); + + if ( empty( $_POST['sub'] ) ) { + wp_send_json_error( array( 'error' => __( 'No Auth0 user ID provided.', 'wp-auth0' ) ) ); + } + + if ( ! $this->api_jobs_resend->call() ) { + wp_send_json_error( array( 'error' => __( 'API call failed.', 'wp-auth0' ) ) ); + } + + wp_send_json_success(); } } +/** + * AJAX handler to re-send verification email. + * Hooked to: wp_ajax_nopriv_resend_verification_email + * + * @codeCoverageIgnore - Tested in TestEmailVerification::testResendVerificationEmail() + */ function wp_auth0_ajax_resend_verification_email() { - WP_Auth0_Email_Verification::ajax_resend_email(); + $options = WP_Auth0_Options::Instance(); + $api_client_creds = new WP_Auth0_Api_Client_Credentials( $options ); + $auth0_user_id = isset( $_POST['sub'] ) ? $_POST['sub'] : null; + $api_jobs_verification = new WP_Auth0_Api_Jobs_Verification( $options, $api_client_creds, $auth0_user_id ); + $email_verification = new WP_Auth0_Email_Verification( $api_jobs_verification ); + + $email_verification->resend_verification_email(); } diff --git a/lib/api/WP_Auth0_Api_Abstract.php b/lib/api/WP_Auth0_Api_Abstract.php new file mode 100644 index 00000000..d48b8c3e --- /dev/null +++ b/lib/api/WP_Auth0_Api_Abstract.php @@ -0,0 +1,459 @@ +options = $options; + + // Required settings in the plugin. + $this->domain = $this->options->get( 'domain' ); + $this->client_id = $this->options->get( 'client_id' ); + $this->client_secret = $this->options->get( 'client_secret' ); + + // Headers sent with every request. + $this->headers = static::get_info_headers(); + } + + /** + * Get required telemetry header + * + * @return array + */ + public static function get_info_headers() { + $header_value = array( + 'name' => 'wp-auth0', + 'version' => WPA0_VERSION, + 'environment' => array( + 'PHP' => phpversion(), + 'WordPress' => get_bloginfo( 'version' ), + ), + ); + return array( 'Auth0-Client' => base64_encode( wp_json_encode( $header_value ) ) ); + } + + /** + * Call the API. + * + * @return mixed + */ + abstract function call(); + + /** + * Handle the response. + * + * @param string $method - Calling method name. + * + * @return mixed + */ + abstract protected function handle_response( $method ); + + /** + * Set the remote path to call. + * + * @param string $path - Path to use. + * + * @return $this + */ + protected function set_path( $path ) { + $this->remote_path = $this->clean_path( $path ); + return $this; + } + + /** + * Set the stored API Token or perform a Client Credentials grant to get a new access token. + * + * @param string $scope - Scope to check. + * + * @return bool + */ + protected function set_bearer( $scope ) { + + $this->api_token = wp_cache_get( self::CACHE_KEY, WPA0_CACHE_GROUP ); + if ( ! $this->api_token ) { + $this->api_token = $this->options->get( 'auth0_app_token' ); + } + + if ( $this->api_token ) { + try { + $this->api_token_decoded = $this->decode_jwt( $this->api_token ); + } catch ( Exception $e ) { + // If we can't decode the token, try a client credentials grant below. + $this->api_token = null; + } + } + + // Could not decode the stored API token or none was found so try to get one via API. + if ( ! $this->api_token_decoded && $this->api_client_creds instanceof WP_Auth0_Api_Client_Credentials ) { + $this->api_token = $this->api_client_creds->call(); + $this->api_token_decoded = $this->api_client_creds->get_token_decoded(); + } + + // No token to use, error recorded in previous steps. + if ( ! $this->api_token_decoded ) { + wp_cache_delete( self::CACHE_KEY, WPA0_CACHE_GROUP ); + return false; + } + + // API token is missing the required scope. + if ( ! $this->api_token_has_scope( $scope ) ) { + WP_Auth0_ErrorManager::insert_auth0_error( + __METHOD__, + // translators: The $scope var here is a machine term and should not be translated. + sprintf( __( 'API token does not include the scope %s.', 'wp-auth0' ), $scope ) + ); + wp_cache_delete( self::CACHE_KEY, WPA0_CACHE_GROUP ); + return false; + } + + // Scope exists, add to the header and cache. + $this->add_header( 'Authorization', 'Bearer ' . $this->api_token ); + wp_cache_set( self::CACHE_KEY, $this->api_token, WPA0_CACHE_GROUP ); + return true; + } + + /** + * Include the Management API audience in the body array. + * + * @return $this + */ + protected function send_audience() { + $this->body['audience'] = 'https://' . $this->domain . '/api/v2/'; + return $this; + } + + /** + * Include the Client ID in the body array. + * + * @return $this + */ + protected function send_client_id() { + $this->body['client_id'] = $this->client_id; + return $this; + } + + /** + * Include the Client Secret in the body array. + * + * @return $this + */ + protected function send_client_secret() { + $this->body['client_secret'] = $this->client_secret; + return $this; + } + + /** + * Set a header array key to a specific value. + * + * @param string $header - Header name to set. + * @param string $value - Value to set to the key above. + * + * @return $this + */ + protected function add_header( $header, $value ) { + $this->headers[ $header ] = $value; + return $this; + } + + /** + * Set a body array key to a specific value. + * + * @param string $key - Body key to set. + * @param string $value - Value to set to the key above. + * + * @return $this + */ + protected function add_body( $key, $value ) { + $this->body[ $key ] = $value; + return $this; + } + + /** + * Return the remote URL from the domain and path. + * + * @return string + */ + protected function build_url() { + return 'https://' . $this->domain . '/' . $this->remote_path; + } + + /** + * Send a GET request. + * + * @return $this + */ + protected function get() { + return $this->request( 'GET' ); + } + + /** + * Send a POST request. + * + * @return $this + */ + protected function post() { + return $this->add_header( 'Content-Type', 'application/json' )->request( 'POST' ); + } + + /** + * Send a DELETE request. + * + * @return $this + */ + protected function delete() { + return $this->request( 'DELETE' ); + } + + /** + * Send a PATCH request. + * + * @return $this + */ + protected function patch() { + return $this->add_header( 'Content-Type', 'application/json' )->request( 'PATCH' ); + } + + /** + * Handle a WP_Error stemming from a failed HTTP call. + * Can be called in child class handle_response method to generically handle WP_Error responses. + * + * @param string $method - Method name that called the API. + * + * @return bool - True if there was a WP_Error, false if not. + */ + protected function handle_wp_error( $method ) { + if ( $this->response instanceof WP_Error ) { + WP_Auth0_ErrorManager::insert_auth0_error( $method, $this->response ); + return true; + } + return false; + } + + /** + * Handle common failure responses returned from a remote server. + * Can be called in child class handle_response method to generically handle common failure responses. + * + * @param string $method - Method name that called the API. + * @param int $success_code - Code integer representing success. + * + * @return bool - True if there was an error, false if not. + */ + protected function handle_failed_response( $method, $success_code = 200 ) { + + if ( $this->response_code === $success_code ) { + return false; + } + + $response_body = json_decode( $this->response_body, true ); + $message = __( 'Error returned', 'wp-auth0' ); + + if ( isset( $response_body['statusCode'] ) ) { + + if ( isset( $response_body['message'] ) ) { + $message .= ' - ' . sanitize_text_field( $response_body['message'] ); + } + if ( isset( $response_body['errorCode'] ) ) { + $message .= ' [' . sanitize_text_field( $response_body['errorCode'] ) . ']'; + } + WP_Auth0_ErrorManager::insert_auth0_error( $method, new WP_Error( $response_body['statusCode'], $message ) ); + return true; + } + + if ( isset( $response_body['error'] ) ) { + if ( isset( $response_body['error_description'] ) ) { + $message .= ' - ' . sanitize_text_field( $response_body['error_description'] ); + } + WP_Auth0_ErrorManager::insert_auth0_error( $method, new WP_Error( $response_body['error'], $message ) ); + return true; + } + + WP_Auth0_ErrorManager::insert_auth0_error( $method, $this->response_body ); + return true; + } + + /** + * Decode an Auth0 Management API token. + * + * @param string $token - API JWT to decode. + * + * @return object + * + * @throws DomainException Algorithm was not provided. + * @throws UnexpectedValueException Provided JWT was invalid. + * @throws SignatureInvalidException Provided JWT was invalid because the signature verification failed. + * @throws BeforeValidException Provided JWT used before it's eligible as defined by 'nbf'. + * @throws BeforeValidException Provided JWT used before it's been created as defined by 'iat'. + * @throws ExpiredException Provided JWT has since expired, as defined by the 'exp' claim. + */ + protected function decode_jwt( $token ) { + return JWT::decode( + $token, + $this->options->get_client_secret_as_key(), + // Management API tokens are always RS256. + array( 'RS256' ) + ); + } + + /** + * Send the HTTP request. + * + * @param string $method - HTTP method to use. + * + * @return $this + * + * @codeCoverageIgnore - Tested by individual HTTP methods in TestApiAbstract::testHttpRequests() + */ + private function request( $method ) { + $remote_url = $this->build_url(); + $http_args = array( + 'headers' => $this->headers, + 'method' => $method, + 'body' => ! empty( $this->body ) ? json_encode( $this->body ) : null, + ); + + $this->response = wp_remote_request( $remote_url, $http_args ); + $this->response_code = (int) wp_remote_retrieve_response_code( $this->response ); + $this->response_body = wp_remote_retrieve_body( $this->response ); + + return $this; + } + + /** + * Check the stored API token for a specific scope. + * + * @param string $scope - API token scope to check for. + * + * @return bool + */ + private function api_token_has_scope( $scope ) { + $scopes = explode( ' ', $this->api_token_decoded->scope ); + return ! empty( $scopes ) && in_array( $scope, $scopes ); + } + + /** + * Remove slash at the first character, if there is one. + * + * @param string $path - Path to clean. + * + * @return string + * + * @codeCoverageIgnore + */ + private function clean_path( $path ) { + if ( ! empty( $path[0] ) && '/' === $path[0] ) { + $path = substr( $path, 1 ); + } + return $path; + } +} diff --git a/lib/api/WP_Auth0_Api_Client_Credentials.php b/lib/api/WP_Auth0_Api_Client_Credentials.php new file mode 100644 index 00000000..417ca868 --- /dev/null +++ b/lib/api/WP_Auth0_Api_Client_Credentials.php @@ -0,0 +1,94 @@ +set_path( 'oauth/token' ) + ->send_client_id() + ->send_client_secret() + ->send_audience() + ->add_body( 'grant_type', 'client_credentials' ); + } + + /** + * Make the API call and handle the response. + * + * @return mixed|null + */ + public function call() { + return $this->post()->handle_response( __METHOD__ ); + } + + /** + * Return the decoded API token received. + * + * @return null|object + */ + public function get_token_decoded() { + return $this->token_decoded; + } + + /** + * Handle API response. + * + * @param string $method - Method that called the API. + * + * @return string|null + */ + protected function handle_response( $method ) { + + if ( $this->handle_wp_error( $method ) ) { + return self::RETURN_ON_FAILURE; + } + + if ( $this->handle_failed_response( $method ) ) { + return self::RETURN_ON_FAILURE; + } + + $response_body = json_decode( $this->response_body ); + + if ( empty( $response_body->access_token ) ) { + WP_Auth0_ErrorManager::insert_auth0_error( $method, __( 'No access_token returned.', 'wp-auth0' ) ); + return self::RETURN_ON_FAILURE; + } + + try { + $this->token_decoded = $this->decode_jwt( $response_body->access_token ); + return $response_body->access_token; + } catch ( Exception $e ) { + WP_Auth0_ErrorManager::insert_auth0_error( $method, $e ); + return self::RETURN_ON_FAILURE; + } + } +} diff --git a/lib/api/WP_Auth0_Api_Jobs_Verification.php b/lib/api/WP_Auth0_Api_Jobs_Verification.php new file mode 100644 index 00000000..a4d72aea --- /dev/null +++ b/lib/api/WP_Auth0_Api_Jobs_Verification.php @@ -0,0 +1,84 @@ +api_client_creds = $api_client_creds; + $this->set_path( 'api/v2/jobs/verification-email' ) + ->add_body( 'user_id', $user_id ) + ->send_client_id(); + } + + /** + * Set body data, make the API call, and handle the response. + * + * @return boolean + */ + public function call() { + + if ( ! $this->set_bearer( self::API_SCOPE ) ) { + return self::RETURN_ON_FAILURE; + } + + return $this->post()->handle_response( __METHOD__ ); + } + + /** + * Handle API response. + * + * @param string $method - Method that called the API. + * + * @return mixed|null + */ + protected function handle_response( $method ) { + + if ( $this->handle_wp_error( $method ) ) { + return self::RETURN_ON_FAILURE; + } + + if ( $this->handle_failed_response( $method, 201 ) ) { + return self::RETURN_ON_FAILURE; + } + + return true; + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 1d3d72f5..a6ffeab3 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -39,6 +39,11 @@ function _manually_load_plugin() { require dirname( __FILE__ ) . '/../vendor/autoload.php'; +require dirname( __FILE__ ) . '/classes/Test_WP_Auth0_Api_Abstract.php'; + +require dirname( __FILE__ ) . '/traits/ajaxHelpers.php'; require dirname( __FILE__ ) . '/traits/domDocumentHelpers.php'; +require dirname( __FILE__ ) . '/traits/hookHelpers.php'; +require dirname( __FILE__ ) . '/traits/httpHelpers.php'; require dirname( __FILE__ ) . '/traits/setUpTestDb.php'; -require dirname( __FILE__ ) . '/traits/users.php'; +require dirname( __FILE__ ) . '/traits/usersHelper.php'; diff --git a/tests/classes/Test_WP_Auth0_Api_Abstract.php b/tests/classes/Test_WP_Auth0_Api_Abstract.php new file mode 100644 index 00000000..2dd6fd5c --- /dev/null +++ b/tests/classes/Test_WP_Auth0_Api_Abstract.php @@ -0,0 +1,89 @@ +set_http_method(). + * + * @return mixed + * + * @throws Exception - When HTTP method is not set. + */ + public function call() { + if ( empty( $this->http_method ) ) { + throw new Exception( 'No HTTP method set. Call $this->set_http_method() first.' ); + } + return $this->{$this->http_method}()->handle_response( __METHOD__ ); + } + + /** + * Stub method required to extend WP_Auth0_Api_Abstract. + * + * @param string $method - Calling method name, __METHOD__. + * + * @return boolean + */ + public function handle_response( $method ) { + if ( $this->handle_wp_error( $method ) ) { + return 'caught_wp_error'; + } + + if ( $this->handle_failed_response( $method ) ) { + return 'caught_failed_response'; + } + + return 'completed_successfully'; + } + + /** + * Set the HTTP method used by $this->call. + * Always call this before $this->call. + * + * @param string $method - HTTP method to use. + * + * @return $this + * + * @throws Exception - If the method does not exist. + */ + public function set_http_method( $method ) { + if ( ! method_exists( $this, $method ) ) { + throw new Exception( 'Method ' . $method . ' does not exist.' ); + } + $this->http_method = $method; + return $this; + } + + /** + * Return request in its current state. + * + * @param null $key - Request array key to return. + * + * @return array|mixed + */ + public function get_request( $key = null ) { + $request = array( + 'body' => $this->body, + 'headers' => $this->headers, + 'url' => $this->build_url(), + ); + return $key && array_key_exists( $key, $request ) ? $request[ $key ] : $request; + } +} diff --git a/tests/testApiAbstract.php b/tests/testApiAbstract.php new file mode 100644 index 00000000..cd87596e --- /dev/null +++ b/tests/testApiAbstract.php @@ -0,0 +1,319 @@ +set( 'domain', self::TEST_DOMAIN ); + + // 1. Test that the default URL was set correctly. + $api_abstract = new Test_WP_Auth0_Api_Abstract( self::$options ); + $this->assertEquals( 'https://' . self::TEST_DOMAIN . '/', $api_abstract->get_request( 'url' ) ); + + // 2. Test that we have an analytics header being sent with the correct data. + $headers = $api_abstract->get_request( 'headers' ); + $this->assertNotEmpty( $headers ); + $this->assertNotEmpty( $headers['Auth0-Client'] ); + + $client_header = base64_decode( $headers['Auth0-Client'] ); + $client_header = json_decode( $client_header, true ); + + $this->assertEquals( 'wp-auth0', $client_header['name'] ); + $this->assertEquals( WPA0_VERSION, $client_header['version'] ); + } + + /** + * Test that headers are set properly. + */ + public function testHeaders() { + $mock_abstract = new ReflectionClass( Test_WP_Auth0_Api_Abstract::class ); + $method = $mock_abstract->getMethod( 'add_header' ); + $method->setAccessible( true ); + + // 1. Test a basic value. + $class = $method->invoke( new Test_WP_Auth0_Api_Abstract( self::$options ), '__test_key_1__', '__test_val_1__' ); + $this->assertEquals( '__test_val_1__', $class->get_request( 'headers' )['__test_key_1__'] ); + + // 2. Test another basic value. + $class = $method->invoke( new Test_WP_Auth0_Api_Abstract( self::$options ), '__test_key_2__', '__test_val_2__' ); + $this->assertEquals( '__test_val_2__', $class->get_request( 'headers' )['__test_key_2__'] ); + + // 3. Test that existing values are overwritten. + $class = $method->invoke( new Test_WP_Auth0_Api_Abstract( self::$options ), '__test_key_1__', '__test_val_3__' ); + $this->assertEquals( '__test_val_3__', $class->get_request( 'headers' )['__test_key_1__'] ); + } + + /** + * Test that the path is set properly. + */ + public function testSetPath() { + self::$options->set( 'domain', self::TEST_DOMAIN ); + + // Reflect the Test_WP_Auth0_Api_Abstract class to set 2 methods as public. + $mock_abstract = new ReflectionClass( Test_WP_Auth0_Api_Abstract::class ); + $set_path = $mock_abstract->getMethod( 'set_path' ); + $set_path->setAccessible( true ); + $build_url = $mock_abstract->getMethod( 'build_url' ); + $build_url->setAccessible( true ); + + // 1. Make sure a basic path is added successfully. + $class = $set_path->invoke( new Test_WP_Auth0_Api_Abstract( self::$options ), 'path' ); + $this->assertEquals( 'https://' . self::TEST_DOMAIN . '/path', $class->get_request( 'url' ) ); + $this->assertEquals( $class->get_request( 'url' ), $build_url->invoke( $class ) ); + + // 2. Make sure a leading slash is cleared before adding. + $class = $set_path->invoke( new Test_WP_Auth0_Api_Abstract( self::$options ), '/path' ); + $this->assertEquals( 'https://' . self::TEST_DOMAIN . '/path', $class->get_request( 'url' ) ); + $this->assertEquals( $class->get_request( 'url' ), $build_url->invoke( $class ) ); + + // 3. Make sure a trailing slash is included. + $class = $set_path->invoke( new Test_WP_Auth0_Api_Abstract( self::$options ), 'path/' ); + $this->assertEquals( 'https://' . self::TEST_DOMAIN . '/path/', $class->get_request( 'url' ) ); + $this->assertEquals( $class->get_request( 'url' ), $build_url->invoke( $class ) ); + + // 4. Make sure a more complex path can be added. + $class = $set_path->invoke( new Test_WP_Auth0_Api_Abstract( self::$options ), 'multi/path' ); + $this->assertEquals( 'https://' . self::TEST_DOMAIN . '/multi/path', $class->get_request( 'url' ) ); + $this->assertEquals( $class->get_request( 'url' ), $build_url->invoke( $class ) ); + + // 5. Make sure the path is overwritten, not appended. + $api_abstract = new Test_WP_Auth0_Api_Abstract( self::$options ); + $set_path->invoke( $api_abstract, 'path1' ); + $set_path->invoke( $api_abstract, 'path2' ); + $this->assertEquals( 'https://' . self::TEST_DOMAIN . '/path2', $api_abstract->get_request( 'url' ) ); + $this->assertEquals( $api_abstract->get_request( 'url' ), $build_url->invoke( $api_abstract ) ); + } + + /** + * Test that the body is modified properly. + */ + public function testSendBodyMethods() { + $this->startHttpHalting(); + + self::$options->set( 'domain', self::TEST_DOMAIN ); + self::$options->set( 'client_id', '__test_client_id__' ); + self::$options->set( 'client_secret', '__test_client_secret__' ); + + $api_abstract = new Test_WP_Auth0_Api_Abstract( self::$options ); + + // Reflect the class to set 1 method as public. + $mock_abstract = new ReflectionClass( Test_WP_Auth0_Api_Abstract::class ); + $send_audience = $mock_abstract->getMethod( 'send_audience' ); + $send_audience->setAccessible( true ); + + // 1. Test that the audience is set correctly when using a path. + $api_abstract = $send_audience->invoke( $api_abstract ); + $this->assertEquals( + 'https://' . self::TEST_DOMAIN . '/api/v2/', + $api_abstract->get_request( 'body' )['audience'] + ); + + // 2. Test that the client_id is set. + $send_client_id = $mock_abstract->getMethod( 'send_client_id' ); + $send_client_id->setAccessible( true ); + $api_abstract = $send_client_id->invoke( $api_abstract ); + $this->assertEquals( '__test_client_id__', $api_abstract->get_request( 'body' )['client_id'] ); + + // 3. Test that the client_secret is set. + $send_client_secret = $mock_abstract->getMethod( 'send_client_secret' ); + $send_client_secret->setAccessible( true ); + $api_abstract = $send_client_secret->invoke( $api_abstract ); + $this->assertEquals( '__test_client_secret__', $api_abstract->get_request( 'body' )['client_secret'] ); + + // 4. Test an arbitrary body value. + $add_body = $mock_abstract->getMethod( 'add_body' ); + $add_body->setAccessible( true ); + $api_abstract = $add_body->invoke( $api_abstract, '__test_key__', '__test_val__' ); + $this->assertEquals( '__test_val__', $api_abstract->get_request( 'body' )['__test_key__'] ); + + // 5. Make sure all keys set previously are sent with the request. + $decoded_res = []; + try { + $api_abstract->set_http_method( 'get' )->call(); + } catch ( Exception $e ) { + $decoded_res = unserialize( $e->getMessage() ); + } + $this->assertEquals( 'https://' . self::TEST_DOMAIN . '/api/v2/', $decoded_res['body']['audience'] ); + $this->assertEquals( '__test_client_id__', $decoded_res['body']['client_id'] ); + $this->assertEquals( '__test_client_secret__', $decoded_res['body']['client_secret'] ); + $this->assertEquals( '__test_val__', $decoded_res['body']['__test_key__'] ); + } + + /** + * Test basic HTTP request methods. + * + * @throws Exception - If the method passed to set_http_method does not exist. + */ + public function testHttpRequests() { + $this->startHttpHalting(); + self::$options->set( 'domain', self::TEST_DOMAIN ); + + $api_abstract = new Test_WP_Auth0_Api_Abstract( self::$options ); + + // 1. Test a basic GET request. + $decoded_res = []; + try { + $api_abstract->set_http_method( 'get' )->call(); + } catch ( Exception $e ) { + $decoded_res = unserialize( $e->getMessage() ); + } + $this->assertEquals( 'https://' . self::TEST_DOMAIN . '/', $decoded_res['url'] ); + $this->assertEquals( 'GET', $decoded_res['method'] ); + $this->assertNotEmpty( $decoded_res['headers']['Auth0-Client'] ); + + // 2. Test a basic DELETE request. + $decoded_res = []; + try { + $api_abstract->set_http_method( 'delete' )->call(); + } catch ( Exception $e ) { + $decoded_res = unserialize( $e->getMessage() ); + } + $this->assertEquals( 'https://' . self::TEST_DOMAIN . '/', $decoded_res['url'] ); + $this->assertEquals( 'DELETE', $decoded_res['method'] ); + $this->assertNotEmpty( $decoded_res['headers']['Auth0-Client'] ); + + // 4. Test a basic POST request. + $decoded_res = []; + try { + $api_abstract->set_http_method( 'post' )->call(); + } catch ( Exception $e ) { + $decoded_res = unserialize( $e->getMessage() ); + } + $this->assertEquals( 'https://' . self::TEST_DOMAIN . '/', $decoded_res['url'] ); + $this->assertEquals( 'POST', $decoded_res['method'] ); + $this->assertEquals( 'application/json', $decoded_res['headers']['Content-Type'] ); + $this->assertNotEmpty( $decoded_res['headers']['Auth0-Client'] ); + + // 5. Test a basic PATCH request. + $decoded_res = []; + try { + $api_abstract->set_http_method( 'patch' )->call(); + } catch ( Exception $e ) { + $decoded_res = unserialize( $e->getMessage() ); + } + $this->assertEquals( 'https://' . self::TEST_DOMAIN . '/', $decoded_res['url'] ); + $this->assertEquals( 'PATCH', $decoded_res['method'] ); + $this->assertEquals( 'application/json', $decoded_res['headers']['Content-Type'] ); + $this->assertNotEmpty( $decoded_res['headers']['Auth0-Client'] ); + } + + /** + * Test that a WP_Error as a response is handled properly. + * + * @throws Exception - If the set_http_method is not called with a valid HTTP method. + */ + public function testHandleWpError() { + $this->startHttpMocking(); + + $api_abstract = new Test_WP_Auth0_Api_Abstract( self::$options ); + + $this->http_request_type = 'wp_error'; + $this->assertEquals( 'caught_wp_error', $api_abstract->set_http_method( 'get' )->call() ); + $this->assertCount( 1, self::$error_log->get() ); + } + + /** + * Test that an Auth0 server error as a response is handled properly. + * + * @throws Exception - If the set_http_method is not called with a valid HTTP method. + */ + public function testHandleAuth0FailedResponse() { + $this->startHttpMocking(); + + $api_abstract = new Test_WP_Auth0_Api_Abstract( self::$options ); + + // 1. Test that a successful call does not log an error. + $this->http_request_type = 'success_empty_body'; + $this->assertEquals( 'completed_successfully', $api_abstract->call() ); + $this->assertEmpty( self::$error_log->get() ); + + // 2. Test that a typical Auth0 API error is logged properly. + $this->http_request_type = 'auth0_api_error'; + $this->assertEquals( 'caught_failed_response', $api_abstract->call() ); + $log = self::$error_log->get(); + $this->assertCount( 1, $log ); + $this->assertEquals( 'caught_api_error', $log[0]['code'] ); + $this->assertEquals( 'Error returned - Error [error_code]', $log[0]['message'] ); + + // 3. Test that a typical Auth0 callback error is logged properly. + $this->http_request_type = 'auth0_callback_error'; + $this->assertEquals( 'caught_failed_response', $api_abstract->call() ); + $log = self::$error_log->get(); + $this->assertCount( 2, $log ); + $this->assertEquals( 'caught_callback_error', $log[0]['code'] ); + $this->assertEquals( 'Error returned - Error', $log[0]['message'] ); + } + + /** + * Test that an unspecified server error response is logged properly. + */ + public function testHandleOtherFailedResponse() { + $this->startHttpMocking(); + + $api_abstract = new Test_WP_Auth0_Api_Abstract( self::$options ); + + $this->http_request_type = 'other_error'; + $this->assertEquals( 'caught_failed_response', $api_abstract->call() ); + $log = self::$error_log->get(); + $this->assertCount( 1, $log ); + $this->assertEquals( '{"other_error":"Other error"}', $log[0]['message'] ); + } + + /** + * Runs after each test method. + */ + public function tearDown() { + parent::tearDown(); + $this->stopHttpHalting(); + $this->stopHttpMocking(); + } +} diff --git a/tests/testApiClientCredentials.php b/tests/testApiClientCredentials.php new file mode 100644 index 00000000..290b00a8 --- /dev/null +++ b/tests/testApiClientCredentials.php @@ -0,0 +1,178 @@ +startHttpHalting(); + + $client_id = uniqid(); + $client_secret = uniqid(); + + self::$options->set( 'domain', self::TEST_DOMAIN ); + self::$options->set( 'client_id', $client_id ); + self::$options->set( 'client_secret', $client_secret ); + $api_client_creds = new WP_Auth0_Api_Client_Credentials( self::$options ); + + $decoded_res = []; + try { + $api_client_creds->call(); + } catch ( Exception $e ) { + $decoded_res = unserialize( $e->getMessage() ); + } + + $this->assertNotEmpty( $decoded_res ); + $this->assertEquals( 'https://' . self::TEST_DOMAIN . '/oauth/token', $decoded_res['url'] ); + $this->assertEquals( 'POST', $decoded_res['method'] ); + $this->assertArrayHasKey( 'Content-Type', $decoded_res['headers'] ); + $this->assertEquals( 'application/json', $decoded_res['headers']['Content-Type'] ); + $this->assertArrayHasKey( 'client_id', $decoded_res['body'] ); + $this->assertEquals( $client_id, $decoded_res['body']['client_id'] ); + $this->assertArrayHasKey( 'client_secret', $decoded_res['body'] ); + $this->assertEquals( $client_secret, $decoded_res['body']['client_secret'] ); + $this->assertArrayHasKey( 'audience', $decoded_res['body'] ); + $this->assertEquals( 'https://' . self::TEST_DOMAIN . '/api/v2/', $decoded_res['body']['audience'] ); + $this->assertArrayHasKey( 'grant_type', $decoded_res['body'] ); + $this->assertEquals( 'client_credentials', $decoded_res['body']['grant_type'] ); + } + + /** + * Test a basic Client Credentials call against a mock API server. + */ + public function testCall() { + $this->startHttpMocking(); + set_transient( WPA0_JWKS_CACHE_TRANSIENT_NAME, uniqid() ); + + $api_client_creds = new WP_Auth0_Api_Client_Credentials( self::$options ); + + // 1. Set the response to be a WP_Error, make sure we get null back, and check for a log entry. + $this->http_request_type = 'wp_error'; + $this->assertNull( $api_client_creds->call() ); + $log = self::$error_log->get(); + $this->assertCount( 1, $log ); + $this->assertEquals( 'Caught WP_Error.', $log[0]['message'] ); + + // 2. Set the response to be an Auth0 server error, check for null, and check for another log entry. + $this->http_request_type = 'auth0_api_error'; + $this->assertNull( $api_client_creds->call() ); + $log = self::$error_log->get(); + $this->assertCount( 2, $log ); + $this->assertEquals( 'caught_api_error', $log[0]['code'] ); + + // 3. Set the response to be successful but empty, check for null, and check for another log entry. + $this->http_request_type = 'success_empty_body'; + $this->assertNull( $api_client_creds->call() ); + $log = self::$error_log->get(); + $this->assertCount( 3, $log ); + $this->assertEquals( 'No access_token returned.', $log[0]['message'] ); + + // 4. Set the response to be successful but an invalid JWT, check for null, and check for another error entry. + $this->http_request_type = 'access_token'; + $this->assertNull( $api_client_creds->call() ); + $log = self::$error_log->get(); + $this->assertCount( 4, $log ); + $this->assertEquals( 'Wrong number of segments', $log[0]['message'] ); + + // Create a dummy decoded token. + $dummy_decoded_token = (object) array( 'scope' => 'dummy:scope' ); + + // Mock the parent decode_jwt method to return the dummy decoded token. + $api_client_creds_mock = $this->getMockBuilder( WP_Auth0_Api_Client_Credentials::class ) + ->setMethods( [ 'decode_jwt' ] ) + ->setConstructorArgs( [ self::$options ] ) + ->getMock(); + $api_client_creds_mock->method( 'decode_jwt' ) + ->willReturn( $dummy_decoded_token ); + + // Reflect the mocked class to make the get_token_decoded method public. + $reflect_mock = new ReflectionClass( WP_Auth0_Api_Client_Credentials::class ); + $method = $reflect_mock->getMethod( 'get_token_decoded' ); + $method->setAccessible( true ); + + // 5. Make sure we get an access token back from the API call. + $this->http_request_type = 'access_token'; + $this->assertEquals( '__test_access_token__', $api_client_creds_mock->call() ); + + // 6. Make sure the dummy decoded token stored during handle_response is correct. + $this->assertEquals( $dummy_decoded_token, $method->invoke( $api_client_creds_mock ) ); + } + + /** + * Specific mock API responses for this suite. + * + * @return array|null|WP_Error + */ + public function httpMock() { + $parent_mock = $this->httpMockDefault(); + if ( ! is_null( $parent_mock ) ) { + return $parent_mock; + } + + switch ( $this->getResponseType() ) { + case 'access_token': + return [ + 'body' => '{"access_token":"__test_access_token__"}', + 'response' => [ 'code' => 200 ], + ]; + } + } + + /** + * Stop HTTP halting and mocking, reset JWKS transient. + */ + public function tearDown() { + parent::tearDown(); + $this->stopHttpHalting(); + $this->stopHttpMocking(); + delete_transient( WPA0_JWKS_CACHE_TRANSIENT_NAME ); + } +} diff --git a/tests/testEmailVerification.php b/tests/testEmailVerification.php new file mode 100644 index 00000000..780c9007 --- /dev/null +++ b/tests/testEmailVerification.php @@ -0,0 +1,208 @@ +getHooked( 'wp_ajax_nopriv_resend_verification_email' ); + + $this->assertNotEmpty( $hooked[0] ); + $this->assertEquals( 'wp_auth0_ajax_resend_verification_email', $hooked[0]['function'] ); + $this->assertEquals( 10, $hooked[0]['priority'] ); + $this->assertEquals( 1, $hooked[0]['accepted_args'] ); + } + + /** + * Test wp_die output when email needs to be verified. + */ + public function testWpRenderDie() { + add_filter( + 'wp_die_handler', function() { + return [ $this, 'wp_die_handler' ]; + }, 10 + ); + + $userinfo = $this->getUserinfo( 'not-auth0' ); + + // 1. Check that only the default message appears if this is not an Auth0 strategy. + ob_start(); + WP_Auth0_Email_Verification::render_die( $userinfo ); + $this->assertEquals( 'This site requires a verified email address.
', ob_get_clean() ); + + // Set the userinfo as an Auth0 strategy. + $userinfo = $this->getUserinfo( 'auth0' ); + + ob_start(); + WP_Auth0_Email_Verification::render_die( $userinfo ); + $html = ob_get_clean(); + + // 2. Check that required HTML and JS elements exist + $this->assertContains( 'This site requires a verified email address', $html ); + $this->assertContains( 'id="js-a0-resend-verification"', $html ); + $this->assertContains( 'Resend verification email', $html ); + $this->assertContains( 'var WPAuth0EmailVerification', $html ); + $this->assertContains( 'nonce:"' . wp_create_nonce( WP_Auth0_Email_Verification::RESEND_NONCE_ACTION ) . '"', $html ); + $this->assertContains( 'sub:"' . $userinfo->sub . '"', $html ); + $this->assertContains( '//code.jquery.com/jquery-', $html ); + $this->assertContains( 'assets/js/die-with-verify-email.js?ver=' . WPA0_VERSION, $html ); + + add_filter( + 'auth0_verify_email_page', function() { + return '__test_auth0_verify_email_page__'; + }, 10 + ); + + // 3. Test that the auth0_verify_email_page returns passed-in content. + ob_start(); + WP_Auth0_Email_Verification::render_die( $userinfo ); + $this->assertEquals( '__test_auth0_verify_email_page__', ob_get_clean() ); + } + + /** + * Test AJAX email verification send. + */ + public function testResendVerificationEmail() { + $this->start_ajax(); + + // 1. Should fail with a bad nonce. + $caught_exception = false; + $error_msg = 'No exception'; + try { + // Use the hooked function to perform default DI. + wp_auth0_ajax_resend_verification_email(); + } catch ( Exception $e ) { + $error_msg = $e->getMessage(); + $caught_exception = ( 'bad_nonce' === $error_msg ); + } + $this->assertTrue( $caught_exception, $error_msg ); + + // Set the nonce value that check_ajax_referrer looks for. + $_REQUEST['_ajax_nonce'] = wp_create_nonce( WP_Auth0_Email_Verification::RESEND_NONCE_ACTION ); + + // 2. Should fail without a user sub value. + ob_start(); + $caught_exception = false; + $error_msg = 'No exception'; + try { + wp_auth0_ajax_resend_verification_email(); + } catch ( Exception $e ) { + $error_msg = $e->getMessage(); + $caught_exception = ( 'die_ajax' === $error_msg ); + } + $return_json = ob_get_clean(); + $this->assertTrue( $caught_exception, $error_msg ); + $this->assertEquals( '{"success":false,"data":{"error":"No Auth0 user ID provided."}}', $return_json ); + + // Set the sub value that the method looks for. + $_POST['sub'] = $this->getUserinfo()->sub; + + // Mock the API call. + $api_jobs_resend_mock = $this->getMockBuilder( WP_Auth0_Api_Jobs_Verification::class ) + ->setMethods( [ 'call' ] ) + ->setConstructorArgs( + [ + self::$options, + new WP_Auth0_Api_Client_Credentials( self::$options ), + $_POST['sub'], + ] + ) + ->getMock(); + + // Fail on first call (#3) and succeed on the second (#4). + $api_jobs_resend_mock->method( 'call' )->will( $this->onConsecutiveCalls( false, true ) ); + $email_verification = new WP_Auth0_Email_Verification( $api_jobs_resend_mock ); + + // 3. Should fail when mocked API call fails. + ob_start(); + $caught_exception = false; + try { + $email_verification->resend_verification_email(); + } catch ( Exception $e ) { + $caught_exception = ( 'die_ajax' === $e->getMessage() ); + } + $return_json = ob_get_clean(); + $this->assertTrue( $caught_exception ); + $this->assertEquals( '{"success":false,"data":{"error":"API call failed."}}', $return_json ); + + // 4. Should succeed when mocked API call returns true. + ob_start(); + $caught_exception = false; + try { + $email_verification->resend_verification_email(); + } catch ( Exception $e ) { + $caught_exception = ( 'die_ajax' === $e->getMessage() ); + } + $return_json = ob_get_clean(); + $this->assertTrue( $caught_exception ); + $this->assertEquals( '{"success":true}', $return_json ); + } + + /** + * Prevent the wp_die page from dying and echo the message passed. + * Hooked to: wp_die_handler + * + * @param string $message - HTML to show on the wp_die page. + */ + public function wp_die_handler( $message ) { + echo $message; + } +} diff --git a/tests/testUserMeta.php b/tests/testUserMeta.php index 613d48d1..50b5bb21 100644 --- a/tests/testUserMeta.php +++ b/tests/testUserMeta.php @@ -16,7 +16,7 @@ class TestUserMeta extends TestCase { use setUpTestDb; - use users; + use UsersHelper; /** * Instance of WP_Auth0_Options. diff --git a/tests/testUserRepoCreate.php b/tests/testUserRepoCreate.php index 052829c9..a54afd80 100644 --- a/tests/testUserRepoCreate.php +++ b/tests/testUserRepoCreate.php @@ -16,7 +16,7 @@ class TestUserRepoCreate extends TestCase { use setUpTestDb; - use users; + use UsersHelper; /** * Instance of WP_Auth0_Options. diff --git a/tests/traits/ajaxHelpers.php b/tests/traits/ajaxHelpers.php new file mode 100644 index 00000000..03106094 --- /dev/null +++ b/tests/traits/ajaxHelpers.php @@ -0,0 +1,47 @@ +callbacks ) ) { + array_walk( + $wp_filter[ $hook ]->callbacks, function( $callbacks, $priority ) use ( &$hooks ) { + foreach ( $callbacks as $id => $callback ) { + $hooks[] = array_merge( + [ + 'id' => $id, + 'priority' => $priority, + ], $callback + ); + } + } + ); + } else { + return []; + } + + foreach ( $hooks as &$item ) { + + if ( ! is_callable( $item['function'] ) ) { + continue; + } + + if ( is_array( $item['function'] ) ) { + $item['function'] = array( + is_object( $item['function'][0] ) ? get_class( $item['function'][0] ) : $item['function'][0], + $item['function'][1], + ); + } elseif ( ! is_string( $item['function'] ) && is_callable( $item['function'] ) ) { + $item['function'] = get_class( $item['function'] ); + } + } + + return $hooks; + } + + /** + * Assert that hooked functions exists with the correct priority and arg numbers. + * + * @param string $hook_name - Hook name in WP. + * @param string $function - Function name, typically the class name. + * @param array $hooked - Array of functions to check. + * + * @return void + */ + public function assertHooked( $hook_name, $function, array $hooked ) { + $hooks = $this->getHooked( $hook_name ); + $found = 0; + + foreach ( $hooks as $hook ) { + $method_name = $hook['function'][1]; + + if ( ! is_array( $hook['function'] ) ) { + continue; + } + + if ( $function !== $hook['function'][0] ) { + continue; + } + + if ( ! empty( $hooked[ $method_name ] ) ) { + $this->assertEquals( $hooked[ $method_name ]['priority'], $hook['priority'] ); + $this->assertEquals( $hooked[ $method_name ]['accepted_args'], $hook['accepted_args'] ); + $found++; + } + } + $this->assertEquals( count( $hooked ), $found ); + } +} diff --git a/tests/traits/httpHelpers.php b/tests/traits/httpHelpers.php new file mode 100644 index 00000000..63aed373 --- /dev/null +++ b/tests/traits/httpHelpers.php @@ -0,0 +1,124 @@ + $url, + 'method' => $args['method'], + 'headers' => $args['headers'], + 'body' => json_decode( $args['body'], true ), + 'preempt' => $preempt, + ] + ); + throw new Exception( $error_msg ); + } + + /** + * Stop halting HTTP requests. + * Use this in a tearDown() method in the test suite. + */ + public function stopHttpHalting() { + remove_filter( 'pre_http_request', [ $this, 'httpHalt' ], 1 ); + } + + /** + * Start mocking all HTTP requests. + * Use this at the top of tests that should test behavior for different HTTP responses. + */ + public function startHttpMocking() { + add_filter( 'pre_http_request', [ $this, 'httpMock' ], 1 ); + } + + /** + * Get the current http_request_type. + * + * @return string|null + */ + public function getResponseType() { + return $this->http_request_type; + } + + /** + * Return a mocked API call based on type. + * + * @return array|null|WP_Error + */ + public function httpMock() { + switch ( $this->getResponseType() ) { + + case 'wp_error': + return new WP_Error( 1, 'Caught WP_Error.' ); + + case 'auth0_api_error': + return [ + 'body' => '{"statusCode":"caught_api_error","message":"Error","errorCode":"error_code"}', + 'response' => [ 'code' => 400 ], + ]; + + case 'auth0_callback_error': + return [ + 'body' => '{"error":"caught_callback_error","error_description":"Error"}', + 'response' => [ 'code' => 400 ], + ]; + + case 'other_error': + return [ + 'body' => '{"other_error":"Other error"}', + 'response' => [ 'code' => 500 ], + ]; + + case 'success_empty_body': + return [ + 'body' => '', + 'response' => [ 'code' => 200 ], + ]; + + default: + return null; + } + } + + /** + * Stop mocking API calls. + * Use this in a tearDown() method in the test suite. + */ + public function stopHttpMocking() { + remove_filter( 'pre_http_request', [ $this, 'httpMock' ], 1 ); + } +} diff --git a/tests/traits/users.php b/tests/traits/usersHelper.php similarity index 98% rename from tests/traits/users.php rename to tests/traits/usersHelper.php index b9519d2a..4138daac 100644 --- a/tests/traits/users.php +++ b/tests/traits/usersHelper.php @@ -9,7 +9,7 @@ /** * Trait Users. */ -trait Users { +trait UsersHelper { /** * Create a new User.