diff options
author | Jonathan Desrosiers <desrosj@git.wordpress.org> | 2019-03-15 18:07:09 +0000 |
---|---|---|
committer | Jonathan Desrosiers <desrosj@git.wordpress.org> | 2019-03-15 18:07:09 +0000 |
commit | 152e9d2b8fd52d86f3f61b9c3d60162a993b079b (patch) | |
tree | 4e68b7774e4688bcbaf77dc5a8f4c3111dff49b9 | |
parent | 40ff06c08335f3b0972ed4422493e1f268086dc9 (diff) | |
download | wordpress-152e9d2b8fd52d86f3f61b9c3d60162a993b079b.tar.gz wordpress-152e9d2b8fd52d86f3f61b9c3d60162a993b079b.zip |
Privacy: Add unit tests for exporting and erasing personal data.
Props birgire, garrett-eclipse, desrosj.
Fixes #43438.
git-svn-id: https://develop.svn.wordpress.org/trunk@44909 602fd350-edb4-49c9-b593-d223f7449a82
-rw-r--r-- | tests/phpunit/includes/testcase-ajax.php | 2 | ||||
-rwxr-xr-x | tests/phpunit/tests/ajax/PrivacyErasePersonalData.php | 814 | ||||
-rwxr-xr-x | tests/phpunit/tests/ajax/PrivacyExportPersonalData.php | 818 |
3 files changed, 1634 insertions, 0 deletions
diff --git a/tests/phpunit/includes/testcase-ajax.php b/tests/phpunit/includes/testcase-ajax.php index bf5a5bce91..736d362f76 100644 --- a/tests/phpunit/includes/testcase-ajax.php +++ b/tests/phpunit/includes/testcase-ajax.php @@ -119,6 +119,8 @@ abstract class WP_Ajax_UnitTestCase extends WP_UnitTestCase { 'delete-theme', 'install-theme', 'get-post-thumbnail-html', + 'wp-privacy-export-personal-data', + 'wp-privacy-erase-personal-data', ); public static function setUpBeforeClass() { diff --git a/tests/phpunit/tests/ajax/PrivacyErasePersonalData.php b/tests/phpunit/tests/ajax/PrivacyErasePersonalData.php new file mode 100755 index 0000000000..4d4126a677 --- /dev/null +++ b/tests/phpunit/tests/ajax/PrivacyErasePersonalData.php @@ -0,0 +1,814 @@ +<?php +/** + * Testing Ajax handler for erasing personal data. + * + * @package WordPress\UnitTests + * @since 5.2.0 + */ + +/** + * Tests_Ajax_PrivacyExportPersonalData class. + * + * @since 5.2.0 + * + * @group ajax + * @group privacy + * + * @covers ::wp_ajax_wp_privacy_erase_personal_data + */ +class Tests_Ajax_PrivacyErasePersonalData extends WP_Ajax_UnitTestCase { + + /** + * User Request ID. + * + * @since 5.2.0 + * + * @var int $request_id + */ + protected static $request_id; + + /** + * User Request Email. + * + * @since 5.2.0 + * + * @var string $request_email + */ + protected static $request_email; + + /** + * Ajax Action. + * + * @since 5.2.0 + * + * @var string $action + */ + protected static $action; + + /** + * Eraser Index. + * + * @since 5.2.0 + * + * @var int $eraser + */ + protected static $eraser; + + /** + * Eraser Key. + * + * @since 5.2.0 + * + * @var string $eraser_key + */ + protected static $eraser_key; + + /** + * Eraser Friendly Name. + * + * @since 5.2.0 + * + * @var string $eraser_friendly_name + */ + protected static $eraser_friendly_name; + + /** + * Page Index. + * + * @since 5.2.0 + * + * @var int $page + */ + protected static $page; + + /** + * Last response parsed. + * + * @since 5.2.0 + * + * @var array $_last_response_parsed + */ + protected $_last_response_parsed; + + /** + * An array key in the test eraser to unset. + * + * @since 5.2.0 + * + * @var string $key_to_unset + */ + protected $key_to_unset; + + /** + * A value to change the test eraser callback to. + * + * @since 5.2.0 + * + * @var string $new_callback_value + */ + protected $new_callback_value; + + /** + * Create user erase request fixtures. + * + * @param WP_UnitTest_Factory $factory Factory. + */ + public static function wpSetUpBeforeClass( $factory ) { + self::$request_email = 'requester@example.com'; + self::$request_id = wp_create_user_request( self::$request_email, 'remove_personal_data' ); + self::$action = 'wp-privacy-erase-personal-data'; + self::$eraser = 1; + self::$eraser_key = 'custom-eraser'; + self::$eraser_friendly_name = 'Custom Eraser'; + self::$page = 1; + } + + /** + * Register a custom personal data eraser. + */ + public function setUp() { + parent::setUp(); + + $this->key_to_unset = ''; + + // Make sure the erasers response is not modified and avoid sending emails. + remove_all_filters( 'wp_privacy_personal_data_erasure_page' ); + remove_all_actions( 'wp_privacy_personal_data_erased' ); + + // Only use our custom privacy personal data eraser. + remove_all_filters( 'wp_privacy_personal_data_erasers' ); + add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_custom_personal_data_eraser' ) ); + + $this->_setRole( 'administrator' ); + } + + /** + * Clean up after each test method. + */ + public function tearDown() { + remove_filter( 'wp_privacy_personal_data_erasers', array( $this, 'register_custom_personal_data_eraser' ) ); + $this->new_callback_value = ''; + + parent::tearDown(); + } + + /** + * Helper method for changing the test eraser's callback function. + * + * @param string|array $callback New test eraser callback index value. + */ + protected function _set_eraser_callback( $callback ) { + $this->new_callback_value = $callback; + add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'filter_eraser_callback_value' ), 20 ); + } + + /** + * Change the test eraser callback to a specified value. + * + * @since 5.2.0 + * + * @param array $erasers List of data erasers. + * + * @return array $erasersList of data erasers. + */ + public function filter_eraser_callback_value( $erasers ) { + $erasers[ self::$eraser_key ]['callback'] = $this->new_callback_value; + + return $erasers; + } + + /** + * Helper method for unsetting an array index in the test eraser. + * + * @param string|bool $key Test eraser key to unset. + */ + protected function _unset_eraser_key( $key ) { + $this->key_to_unset = $key; + add_filter( 'wp_privacy_personal_data_erasers', array( $this, 'filter_unset_eraser_index' ), 20 ); + } + + /** + * Unsets an array key in the test eraser. + * + * If the key is false, the eraser is set to false. + * + * @since 5.2.0 + * + * @param array $erasers Erasers. + * + * @return array $erasers Erasers. + */ + public function filter_unset_eraser_index( $erasers ) { + if ( false === $this->key_to_unset ) { + $erasers[ self::$eraser_key ] = false; + } elseif ( ! empty( $this->key_to_unset ) ) { + unset( $erasers[ self::$eraser_key ][ $this->key_to_unset ] ); + } + + return $erasers; + } + + /** + * Helper method for erasing a key from the eraser response. + * + * @since 5.2.0 + * + * @param array $key Response key to unset. + */ + protected function _unset_response_key( $key ) { + $this->key_to_unset = $key; + $this->_set_eraser_callback( array( $this, 'filter_unset_response_index' ) ); + } + + /** + * Unsets an array index in a response. + * + * @since 5.2.0 + * + * @param string $email_address The requester's email address. + * @param int $page Page number. + * + * @return array $return Export data. + */ + public function filter_unset_response_index( $email_address, $page = 1 ) { + $response = $this->callback_personal_data_eraser( $email_address, $page ); + + if ( ! empty( $this->key_to_unset ) ) { + unset( $response[ $this->key_to_unset ] ); + } + + return $response; + } + + /** + * The function should send an error when the request ID is missing. + * + * @since 5.2.0 + * + * @ticket 43438 + */ + public function test_error_when_missing_request_id() { + $this->assertNotWPError( self::$request_id ); + + // Set up a request. + $this->_make_ajax_call( + array( + 'id' => null, // Missing request ID. + ) + ); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Missing request ID.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the request ID is less than 1. + * + * @since 5.2.0 + * + * @ticket 43438 + */ + public function test_error_when_request_id_invalid() { + $this->assertNotWPError( self::$request_id ); + + // Set up a request. + $this->_make_ajax_call( + array( + 'id' => -1, // Invalid request ID. + ) + ); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Invalid request ID.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the current user is missing required capabilities. + * + * @since 5.2.0 + * + * @ticket 43438 + */ + public function test_error_when_current_user_missing_required_capabilities() { + $this->_setRole( 'author' ); + + $this->assertFalse( current_user_can( 'erase_others_personal_data' ) ); + $this->assertFalse( current_user_can( 'delete_users' ) ); + + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Sorry, you are not allowed to perform this action.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the nonce does not validate. + * + * @since 5.2.0 + */ + public function test_failure_with_invalid_nonce() { + $this->setExpectedException( 'WPAjaxDieStopException', '-1' ); + + $this->_make_ajax_call( + array( + 'security' => 'invalid-nonce', + ) + ); + } + + /** + * The function should send an error when the request type is incorrect. + * + * @since 5.2.0 + */ + public function test_error_when_incorrect_request_type() { + $request_id = wp_create_user_request( + 'export-request@example.com', + 'export_personal_data' // Incorrect request type, expects 'remove_personal_data'. + ); + + $this->_make_ajax_call( + array( + 'security' => wp_create_nonce( 'wp-privacy-erase-personal-data-' . $request_id ), + 'id' => $request_id, + ) + ); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Invalid request type.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the request email is invalid. + * + * @since 5.2.0 + */ + public function test_error_when_invalid_email() { + wp_update_post( + array( + 'ID' => self::$request_id, + 'post_title' => '', // Invalid requester's email address. + ) + ); + + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Invalid email address in request.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the eraser index is missing. + * + * @since 5.2.0 + */ + public function test_error_when_missing_eraser_index() { + $this->_make_ajax_call( + array( + 'eraser' => null, // Missing eraser index. + ) + ); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Missing eraser index.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the page index is missing. + * + * @since 5.2.0 + */ + public function test_error_when_missing_page_index() { + $this->_make_ajax_call( + array( + 'page' => null, // Missing page index. + ) + ); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Missing page index.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the eraser index is negative. + * + * @since 5.2.0 + */ + public function test_error_when_negative_eraser_index() { + $this->_make_ajax_call( + array( + 'eraser' => -1, // Negative eraser index. + ) + ); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Eraser index cannot be less than one.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the eraser index is out of range. + * + * @since 5.2.0 + */ + public function test_error_when_eraser_index_out_of_range() { + $this->_make_ajax_call( + array( + 'eraser' => PHP_INT_MAX, // Out of range eraser index. + ) + ); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Eraser index is out of range.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the page index is less than one. + * + * @since 5.2.0 + */ + public function test_error_when_page_index_less_than_one() { + $this->_make_ajax_call( + array( + 'page' => 0, // Page index less than one. + ) + ); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Page index cannot be less than one.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when an eraser is not an array. + * + * @since 5.2.0 + */ + public function test_error_when_eraser_not_array() { + $this->_unset_eraser_key( false ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Expected an array describing the eraser at index %s.', + self::$eraser + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * The function should send an error when an eraser is missing a friendly name. + * + * @since 5.2.0 + */ + public function test_error_when_eraser_missing_friendly_name() { + $this->_unset_eraser_key( 'eraser_friendly_name' ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Eraser array at index %s does not include a friendly name.', + self::$eraser + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * The function should send an error when an eraser is missing a callback. + * + * @since 5.2.0 + */ + public function test_error_when_eraser_missing_callback() { + $this->_unset_eraser_key( 'callback' ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Eraser does not include a callback: %s.', + self::$eraser_friendly_name + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * The function should send an error when an eraser, at a given index, has an invalid callback. + * + * @since 5.2.0 + */ + public function test_error_when_eraser_index_invalid_callback() { + $this->_set_eraser_callback( false ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Eraser callback is not valid: %s.', + self::$eraser_friendly_name + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * The function should send an error when an eraser, at a given index, is missing an array response. + * + * @since 5.2.0 + */ + public function test_error_when_eraser_index_invalid_response() { + $this->_set_eraser_callback( '__return_null' ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Did not receive array from %1$s eraser (index %2$d).', + self::$eraser_friendly_name, + self::$eraser + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * The function should send an error when missing an items_removed index. + * + * @since 5.2.0 + */ + public function test_error_when_eraser_items_removed_missing() { + $this->_unset_response_key( 'items_removed' ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Expected items_removed key in response array from %1$s eraser (index %2$d).', + self::$eraser_friendly_name, + self::$eraser + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * The function should send an error when missing an items_retained index. + * + * @since 5.2.0 + */ + public function test_error_when_eraser_items_retained_missing() { + $this->_unset_response_key( 'items_retained' ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Expected items_retained key in response array from %1$s eraser (index %2$d).', + self::$eraser_friendly_name, + self::$eraser + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * The function should send an error when missing a messages index. + * + * @since 5.2.0 + */ + public function test_error_when_eraser_messages_missing() { + $this->_unset_response_key( 'messages' ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Expected messages key in response array from %1$s eraser (index %2$d).', + self::$eraser_friendly_name, + self::$eraser + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * The function should send an error when the messages index is not an array. + * + * @since 5.2.0 + */ + public function test_error_when_eraser_messages_not_array() { + $this->_set_eraser_callback( array( $this, 'filter_response_messages_invalid' ) ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Expected messages key to reference an array in response array from %1$s eraser (index %2$d).', + self::$eraser_friendly_name, + self::$eraser + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * Change the messages index to an invalid value (not an array). + * + * @since 5.2.0 + * + * @param string $email_address The requester's email address. + * @param int $page Page number. + * + * @return array $return Export data. + */ + public function filter_response_messages_invalid( $email_address, $page = 1 ) { + $response = $this->callback_personal_data_eraser( $email_address, $page ); + $response['messages'] = true; + + return $response; + } + + /** + * The function should send an error when an eraser is missing 'done' in array response. + * + * @since 5.2.0 + */ + public function test_error_when_eraser_missing_done_response() { + $this->_unset_response_key( 'done' ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Expected done flag in response array from %1$s eraser (index %2$d).', + self::$eraser_friendly_name, + self::$eraser + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * The function should successfully send erasers response data when the current user has the required + * capabilities. + * + * @since 5.2.0 + * + * @ticket 43438 + */ + public function test_success_when_current_user_has_required_capabilities() { + $this->assertTrue( current_user_can( 'erase_others_personal_data' ) ); + $this->assertTrue( current_user_can( 'delete_users' ) ); + + $this->_make_ajax_call(); + + $this->assertSame( + sprintf( 'A message regarding retained data for %s.', self::$request_email ), + $this->_last_response_parsed['data']['messages'][0] + ); + $this->assertTrue( $this->_last_response_parsed['success'] ); + $this->assertTrue( $this->_last_response_parsed['data']['items_removed'] ); + $this->assertTrue( $this->_last_response_parsed['data']['items_retained'] ); + $this->assertTrue( $this->_last_response_parsed['data']['done'] ); + } + + /** + * The function should successfully send erasers response data when no items to erase. + * + * @since 5.2.0 + * + * @ticket 43438 + */ + public function test_success_when_no_items_to_erase() { + + $this->_make_ajax_call( array( 'page' => 2 ) ); + + $this->assertTrue( $this->_last_response_parsed['success'] ); + $this->assertFalse( $this->_last_response_parsed['data']['items_removed'] ); + $this->assertFalse( $this->_last_response_parsed['data']['items_retained'] ); + $this->assertEmpty( $this->_last_response_parsed['data']['messages'] ); + $this->assertTrue( $this->_last_response_parsed['data']['done'] ); + } + + /** + * Test that the function's output should be filterable with the `wp_privacy_personal_data_erasure_page` filter. + * + * @since 5.2.0 + */ + public function test_output_should_be_filterable() { + add_filter( 'wp_privacy_personal_data_erasure_page', array( $this, 'filter_eraser_data_response' ), 20, 6 ); + $this->_make_ajax_call(); + + $expected_new_index = self::$request_email . '-' . self::$request_id . '-' . self::$eraser_key; + + $this->assertTrue( $this->_last_response_parsed['success'] ); + $this->assertSame( 'filtered removed', $this->_last_response_parsed['data']['items_removed'] ); + $this->assertSame( 'filtered retained', $this->_last_response_parsed['data']['items_retained'] ); + $this->assertSame( array( 'filtered messages' ), $this->_last_response_parsed['data']['messages'] ); + $this->assertSame( 'filtered done', $this->_last_response_parsed['data']['done'] ); + $this->assertSame( $expected_new_index, $this->_last_response_parsed['data']['new_index'] ); + } + + /** + * Filters the eraser response. + * + * @since 5.2.0 + * + * @param array $response The personal data for the given eraser and page. + * @param int $eraser_index The index of the eraser that provided this data. + * @param string $email_address The email address associated with this personal data. + * @param int $page The page for this response. + * @param int $request_id The privacy request post ID associated with this request. + * @param string $eraser_key The key (slug) of the eraser that provided this data. + * + * @return array Filtered erase response. + */ + public function filter_eraser_data_response( $response, $eraser_index, $email_address, $page, $request_id, $eraser_key ) { + $response['items_removed'] = 'filtered removed'; + $response['items_retained'] = 'filtered retained'; + $response['messages'] = array( 'filtered messages' ); + $response['done'] = 'filtered done'; + $response['new_index'] = $email_address . '-' . $request_id . '-' . $eraser_key; + + return $response; + } + + /** + * Register handler for a custom personal data eraser. + * + * @since 5.2.0 + * + * @param array $erasers An array of personal data erasers. + * + * @return array $erasers An array of personal data erasers. + */ + public function register_custom_personal_data_eraser( $erasers ) { + $erasers[ self::$eraser_key ] = array( + 'eraser_friendly_name' => self::$eraser_friendly_name, + 'callback' => array( $this, 'callback_personal_data_eraser' ), + ); + return $erasers; + } + + /** + * Custom Personal Data Eraser. + * + * @since 5.2.0 + * + * @param string $email_address The comment author email address. + * @param int $page Page number. + * + * @return array $return Erase data. + */ + public function callback_personal_data_eraser( $email_address, $page = 1 ) { + if ( 1 === $page ) { + return array( + 'items_removed' => true, + 'items_retained' => true, + 'messages' => array( sprintf( 'A message regarding retained data for %s.', $email_address ) ), + 'done' => true, + ); + } + + return array( + 'items_removed' => false, + 'items_retained' => false, + 'messages' => array(), + 'done' => true, + ); + } + + /** + * Helper function for ajax handler. + * + * @since 5.2.0 + * + * @param array $args Ajax request arguments. + */ + protected function _make_ajax_call( $args = array() ) { + $this->_last_response_parsed = null; + $this->_last_response = ''; + + $defaults = array( + 'action' => self::$action, + 'security' => wp_create_nonce( self::$action . '-' . self::$request_id ), + 'page' => self::$page, + 'id' => self::$request_id, + 'eraser' => self::$eraser, + ); + + $_POST = wp_parse_args( $args, $defaults ); + + try { + $this->_handleAjax( self::$action ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + if ( $this->_last_response ) { + $this->_last_response_parsed = json_decode( $this->_last_response, true ); + } + } +} diff --git a/tests/phpunit/tests/ajax/PrivacyExportPersonalData.php b/tests/phpunit/tests/ajax/PrivacyExportPersonalData.php new file mode 100755 index 0000000000..7473a4957c --- /dev/null +++ b/tests/phpunit/tests/ajax/PrivacyExportPersonalData.php @@ -0,0 +1,818 @@ +<?php +/** + * Testing Ajax handler for exporting personal data. + * + * @package WordPress\UnitTests + * @since 5.2.0 + */ + +/** + * Tests_Ajax_PrivacyExportPersonalData class. + * + * @since 5.2.0 + * + * @group ajax + * @group privacy + * + * @covers ::wp_ajax_wp_privacy_export_personal_data + */ +class Tests_Ajax_PrivacyExportPersonalData extends WP_Ajax_UnitTestCase { + + /** + * User Request ID. + * + * @since 5.2.0 + * + * @var int $request_id + */ + protected static $request_id; + + /** + * User Request Email. + * + * @since 5.2.0 + * + * @var string $request_email + */ + protected static $request_email; + + /** + * Ajax Action. + * + * @since 5.2.0 + * + * @var string $action + */ + protected static $action; + + /** + * Exporter Index. + * + * @since 5.2.0 + * + * @var int $exporter + */ + protected static $exporter; + + /** + * Exporter Key. + * + * @since 5.2.0 + * + * @var string $exporter_key + */ + protected static $exporter_key; + + /** + * Exporter Friendly Name. + * + * @since 5.2.0 + * + * @var string $exporter_friendly_name + */ + protected static $exporter_friendly_name; + + /** + * Page Index. + * + * @since 5.2.0 + * + * @var int $page + */ + protected static $page; + + /** + * Send As Email. + * + * @since 5.2.0 + * + * @var bool $send_as_email + */ + protected static $send_as_email; + + /** + * Last response parsed. + * + * @since 5.2.0 + * + * @var array $_last_response_parsed + */ + protected $_last_response_parsed; + + /** + * An array key in the test exporter to unset. + * + * @since 5.2.0 + * + * @var string $key_to_unset + */ + protected $key_to_unset; + + /** + * A value to change the test exporter callback to. + * + * @since 5.2.0 + * + * @var string $new_callback_value + */ + protected $new_callback_value; + + /** + * Create user export request fixtures. + * + * @since 5.2.0 + * + * @param WP_UnitTest_Factory $factory Factory. + */ + public static function wpSetUpBeforeClass( $factory ) { + self::$request_email = 'requester@example.com'; + self::$request_id = wp_create_user_request( self::$request_email, 'export_personal_data' ); + self::$action = 'wp-privacy-export-personal-data'; + self::$exporter = 1; + self::$exporter_key = 'custom-exporter'; + self::$exporter_friendly_name = 'Custom Exporter'; + self::$page = 1; + self::$send_as_email = false; + } + + /** + * Setup before each test method. + * + * @since 5.2.0 + */ + public function setUp() { + parent::setUp(); + + $this->key_to_unset = ''; + $this->new_callback_value = ''; + + // Make sure the exporter response is not modified and avoid e.g. writing export file to disk. + remove_all_filters( 'wp_privacy_personal_data_export_page' ); + + // Only use our custom privacy personal data exporter. + remove_all_filters( 'wp_privacy_personal_data_exporters' ); + add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'filter_register_custom_personal_data_exporter' ) ); + + $this->_setRole( 'administrator' ); + } + + /** + * Clean up after each test method. + */ + public function tearDown() { + remove_filter( 'wp_privacy_personal_data_exporters', array( $this, 'filter_register_custom_personal_data_exporter' ) ); + + parent::tearDown(); + } + + /** + * Helper method for changing the test exporter's callback function. + * + * @param string|array $callback New test exporter callback function. + */ + protected function _set_exporter_callback( $callback ) { + $this->new_callback_value = $callback; + add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'filter_exporter_callback_value' ), 20 ); + } + + /** + * Change the test exporter callback to a specified value. + * + * @since 5.2.0 + * + * @param array $exporters List of data exporters. + * @return array $exporters List of data exporters. + */ + public function filter_exporter_callback_value( $exporters ) { + $exporters[ self::$exporter_key ]['callback'] = $this->new_callback_value; + + return $exporters; + } + + /** + * Helper method for unsetting an array index in the test exporter. + * + * @param string $key Test exporter key to unset. + */ + protected function _unset_exporter_key( $key ) { + $this->key_to_unset = $key; + add_filter( 'wp_privacy_personal_data_exporters', array( $this, 'filter_unset_exporter_key' ), 20 ); + } + + /** + * Unset a specified key in the test exporter array. + * + * @param array $exporters List of data exporters. + * + * @return array $exporters List of data exporters. + */ + public function filter_unset_exporter_key( $exporters ) { + if ( false === $this->key_to_unset ) { + $exporters[ self::$exporter_key ] = false; + } elseif ( ! empty( $this->key_to_unset ) ) { + unset( $exporters[ self::$exporter_key ][ $this->key_to_unset ] ); + } + + return $exporters; + } + + /** + * The function should send an error when the request ID is missing. + * + * @since 5.2.0 + */ + public function test_error_when_missing_request_id() { + $this->_make_ajax_call( + array( + 'id' => null, // Missing request ID. + ) + ); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Missing request ID.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the request ID is less than 1. + * + * @since 5.2.0 + */ + public function test_error_when_invalid_id() { + $this->_make_ajax_call( + array( + 'id' => -1, // Invalid request ID, less than 1. + ) + ); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Invalid request ID.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the current user is missing the required capability. + * + * @since 5.2.0 + */ + public function test_error_when_current_user_missing_required_capability() { + $this->_setRole( 'author' ); + + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertFalse( current_user_can( 'export_others_personal_data' ) ); + $this->assertSame( 'Sorry, you are not allowed to perform this action.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the nonce does not validate. + * + * @since 5.2.0 + */ + public function test_failure_with_invalid_nonce() { + $this->setExpectedException( 'WPAjaxDieStopException', '-1' ); + + $this->_make_ajax_call( + array( + 'security' => 'invalid-nonce', + ) + ); + } + + /** + * The function should send an error when the request type is incorrect. + * + * @since 5.2.0 + */ + public function test_error_when_incorrect_request_type() { + $request_id = wp_create_user_request( + 'erase-request@example.com', + 'remove_personal_data' // Incorrect request type, expects 'export_personal_data'. + ); + + $this->_make_ajax_call( + array( + 'security' => wp_create_nonce( 'wp-privacy-export-personal-data-' . $request_id ), + 'id' => $request_id, + ) + ); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Invalid request type.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the requester's email address is invalid. + * + * @since 5.2.0 + */ + public function test_error_when_invalid_email_address() { + wp_update_post( + array( + 'ID' => self::$request_id, + 'post_title' => '', // Invalid requester's email address. + ) + ); + + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'A valid email address must be given.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the exporter index is missing. + * + * @since 5.2.0 + */ + public function test_error_when_missing_exporter_index() { + $this->_make_ajax_call( + array( + 'exporter' => null, // Missing exporter index. + ) + ); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Missing exporter index.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the page index is missing. + * + * @since 5.2.0 + */ + public function test_error_when_missing_page_index() { + $this->_make_ajax_call( + array( + 'page' => null, // Missing page index. + ) + ); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Missing page index.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when an exporter has improperly used the `wp_privacy_personal_data_exporters` filter. + * + * @since 5.2.0 + */ + public function test_error_when_exporter_has_improperly_used_exporters_filter() { + // Improper filter usage: returns false instead of an expected array. + add_filter( 'wp_privacy_personal_data_exporters', '__return_false', 999 ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'An exporter has improperly used the registration filter.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the exporter index is negative. + * + * @since 5.2.0 + */ + public function test_error_when_negative_exporter_index() { + $this->_make_ajax_call( + array( + 'exporter' => -1, // Negative exporter index. + ) + ); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Exporter index cannot be negative.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the exporter index is out of range. + * + * @since 5.2.0 + */ + public function test_error_when_exporter_index_out_of_range() { + $this->_make_ajax_call( + array( + 'exporter' => PHP_INT_MAX, // Out of range exporter index. + ) + ); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Exporter index is out of range.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when the page index is less than one. + * + * @since 5.2.0 + */ + public function test_error_when_page_index_less_than_one() { + $this->_make_ajax_call( + array( + 'page' => 0, // Page index less than one. + ) + ); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'Page index cannot be less than one.', $this->_last_response_parsed['data'] ); + } + + /** + * The function should send an error when an exporter is not an array. + * + * @since 5.2.0 + */ + public function test_error_when_exporter_not_array() { + $this->_unset_exporter_key( false ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Expected an array describing the exporter at index %s.', + self::$exporter_key + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * The function should send an error when an exporter is missing a friendly name. + * + * @since 5.2.0 + */ + public function test_error_when_exporter_missing_friendly_name() { + $this->_unset_exporter_key( 'exporter_friendly_name' ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Exporter array at index %s does not include a friendly name.', + self::$exporter_key + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * The function should send an error when an exporter is missing a callback. + * + * @since 5.2.0 + */ + public function test_error_when_exporter_missing_callback() { + $this->_unset_exporter_key( 'callback' ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Exporter does not include a callback: %s.', + self::$exporter_friendly_name + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * The function should send an error when an exporter, at a given index, has an invalid callback. + * + * @since 5.2.0 + */ + public function test_error_when_exporter_index_invalid_callback() { + $this->_set_exporter_callback( false ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Exporter callback is not a valid callback: %s.', + self::$exporter_friendly_name + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * When an exporter callback returns a WP_Error, it should be passed as the error. + * + * @since 5.2.0 + */ + public function test_error_when_exporter_callback_returns_wp_error() { + $this->_set_exporter_callback( array( $this, 'callback_return_wp_error' ) ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( 'passed_message', $this->_last_response_parsed['data'][0]['code'] ); + $this->assertSame( 'This is a WP_Error message.', $this->_last_response_parsed['data'][0]['message'] ); + } + + /** + * Callback for exporter's response. + * + * @since 5.2.0 + * + * @param string $email_address The requester's email address. + * @param int $page Page number. + * @return WP_Error WP_Error instance. + */ + public function callback_return_wp_error( $email_address, $page = 1 ) { + return new WP_Error( 'passed_message', 'This is a WP_Error message.' ); + } + + /** + * The function should send an error when an exporter, at a given index, is missing an array response. + * + * @since 5.2.0 + */ + public function test_error_when_exporter_index_invalid_response() { + $this->_set_exporter_callback( '__return_null' ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Expected response as an array from exporter: %s.', + self::$exporter_friendly_name + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * The function should send an error when an exporter is missing data in array response. + * + * @since 5.2.0 + */ + public function test_error_when_exporter_missing_data_response() { + $this->_set_exporter_callback( array( $this, 'callback_missing_data_response' ) ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Expected data in response array from exporter: %s.', + self::$exporter_friendly_name + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * Callback for exporter's response. + * + * @since 5.2.0 + * + * @param string $email_address The requester's email address. + * @param int $page Page number. + * + * @return array $return Export data. + */ + public function callback_missing_data_response( $email_address, $page = 1 ) { + $response = $this->callback_custom_personal_data_exporter( $email_address, $page ); + unset( $response['data'] ); // Missing data part of response. + + return $response; + } + + /** + * The function should send an error when an exporter is missing 'data' array in array response. + * + * @since 5.2.0 + */ + public function test_function_should_error_when_exporter_missing_data_array_response() { + $this->_set_exporter_callback( array( $this, 'callback_missing_data_array_response' ) ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Expected data array in response array from exporter: %s.', + self::$exporter_friendly_name + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * Callback for exporter's response. + * + * @since 5.2.0 + * + * @param string $email_address The requester's email address. + * @param int $page Page number. + * + * @return array $return Export data. + */ + public function callback_missing_data_array_response( $email_address, $page = 1 ) { + $response = $this->callback_custom_personal_data_exporter( $email_address, $page ); + $response['data'] = false; // Not an array. + return $response; + } + + /** + * The function should send an error when an exporter is missing 'done' in array response. + * + * @since 5.2.0 + */ + public function test_error_when_exporter_missing_done_response() { + $this->_set_exporter_callback( array( $this, 'callback_missing_done_response' ) ); + $this->_make_ajax_call(); + + $this->assertFalse( $this->_last_response_parsed['success'] ); + $this->assertSame( + sprintf( + 'Expected done (boolean) in response array from exporter: %s.', + self::$exporter_friendly_name + ), + $this->_last_response_parsed['data'] + ); + } + + /** + * Remove the response's done flag. + * + * @since 5.2.0 + * + * @param string $email_address The requester's email address. + * @param int $page Page number. + * + * @return array $return Export data. + */ + public function callback_missing_done_response( $email_address, $page = 1 ) { + $response = $this->callback_custom_personal_data_exporter( $email_address, $page ); + unset( $response['done'] ); + + return $response; + } + + /** + * The function should successfully send exporter data response when the current user has the required capability. + * + * @since 5.2.0 + */ + public function test_succeeds_when_current_user_has_required_capability() { + $this->assertTrue( current_user_can( 'export_others_personal_data' ) ); + + $this->_make_ajax_call(); + + $this->assertTrue( $this->_last_response_parsed['success'] ); + $this->assertSame( 'custom-exporter-item-id', $this->_last_response_parsed['data']['data']['item_id'] ); + $this->assertSame( 'Email', $this->_last_response_parsed['data']['data']['data'][0]['name'] ); + $this->assertSame( self::$request_email, $this->_last_response_parsed['data']['data']['data'][0]['value'] ); + } + + /** + * The function should successfully send exporter data response when no items to export. + * + * @since 5.2.0 + */ + public function test_success_when_no_items_to_export() { + + $this->_make_ajax_call( array( 'page' => 2 ) ); + + $this->assertTrue( $this->_last_response_parsed['success'] ); + $this->assertEmpty( $this->_last_response_parsed['data']['data'] ); + $this->assertTrue( $this->_last_response_parsed['data']['done'] ); + } + + /** + * The function's output should be filterable with the `wp_privacy_personal_data_export_page` filter. + * + * @since 5.2.0 + */ + public function test_output_should_be_filterable() { + add_filter( 'wp_privacy_personal_data_export_page', array( $this, 'filter_exporter_data_response' ), 20, 7 ); + $this->_make_ajax_call(); + + $expected_group_label = sprintf( + '%s-%s-%s-%s-%s-%s', + self::$exporter, + self::$page, + self::$request_email, + self::$request_id, + self::$send_as_email, + self::$exporter_key + ); + + $this->assertTrue( $this->_last_response_parsed['success'] ); + $this->assertSame( $expected_group_label, $this->_last_response_parsed['data']['group_label'] ); + $this->assertSame( 'filtered_group_id', $this->_last_response_parsed['data']['group_id'] ); + $this->assertSame( 'filtered_item_id', $this->_last_response_parsed['data']['item_id'] ); + $this->assertSame( 'filtered_name', $this->_last_response_parsed['data']['data'][0]['name'] ); + $this->assertSame( 'filtered_value', $this->_last_response_parsed['data']['data'][0]['value'] ); + } + + /** + * Filter exporter's data response. + * + * @since 5.2.0 + * + * @param array $response The personal data for the given exporter and page. + * @param int $exporter_index The index of the exporter that provided this data. + * @param string $email_address The email address associated with this personal data. + * @param int $page The page for this response. + * @param int $request_id The privacy request post ID associated with this request. + * @param bool $send_as_email Whether the final results of the export should be emailed to the user. + * @param string $exporter_key The key (slug) of the exporter that provided this data. + * + * @return array $response The personal data for the given exporter and page. + */ + public function filter_exporter_data_response( $response, $exporter_index, $email_address, $page, $request_id, $send_as_email, $exporter_key ) { + $group_label = sprintf( + '%s-%s-%s-%s-%s-%s', + $exporter_index, + $page, + $email_address, + $request_id, + $send_as_email, + $exporter_key + ); + $response['group_label'] = $group_label; + $response['group_id'] = 'filtered_group_id'; + $response['item_id'] = 'filtered_item_id'; + $response['data'][0]['name'] = 'filtered_name'; + $response['data'][0]['value'] = 'filtered_value'; + + return $response; + } + + /** + * Filter to register a custom personal data exporter. + * + * @since 5.2.0 + * + * @param array $exporters An array of personal data exporters. + * + * @return array $exporters An array of personal data exporters. + */ + public function filter_register_custom_personal_data_exporter( $exporters ) { + $exporters[ self::$exporter_key ] = array( + 'exporter_friendly_name' => self::$exporter_friendly_name, + 'callback' => array( $this, 'callback_custom_personal_data_exporter' ), + ); + return $exporters; + } + + /** + * Callback for a custom personal data exporter. + * + * @since 5.2.0 + * + * @param string $email_address The requester's email address. + * @param int $page Page number. + * + * @return array $response Export data response. + */ + public function callback_custom_personal_data_exporter( $email_address, $page = 1 ) { + $data_to_export = array(); + + if ( 1 === $page ) { + $data_to_export = array( + 'group_id' => self::$exporter_key . '-group-id', + 'group_label' => self::$exporter_key . '-group-label', + 'item_id' => self::$exporter_key . '-item-id', + 'data' => array( + array( + 'name' => 'Email', + 'value' => $email_address, + ), + ), + ); + } + + return array( + 'data' => $data_to_export, + 'done' => true, + ); + } + + /** + * Helper function for ajax handler. + * + * @since 5.2.0 + * + * @param array $args Ajax request arguments. + */ + protected function _make_ajax_call( $args = array() ) { + $this->_last_response_parsed = null; + $this->_last_response = ''; + + $defaults = array( + 'action' => self::$action, + 'security' => wp_create_nonce( self::$action . '-' . self::$request_id ), + 'exporter' => self::$exporter, + 'page' => self::$page, + 'sendAsEmail' => self::$send_as_email, + 'id' => self::$request_id, + ); + + $_POST = wp_parse_args( $args, $defaults ); + + try { + $this->_handleAjax( self::$action ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + if ( $this->_last_response ) { + $this->_last_response_parsed = json_decode( $this->_last_response, true ); + } + } +} |