<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[44909] trunk/tests/phpunit: Privacy: Add unit tests for exporting and erasing personal data.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/44909">44909</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/44909","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>desrosj</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2019-03-15 18:07:09 +0000 (Fri, 15 Mar 2019)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Privacy: Add unit tests for exporting and erasing personal data.

Props birgire, garrett-eclipse, desrosj.
Fixes <a href="https://core.trac.wordpress.org/ticket/43438">#43438</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunktestsphpunitincludestestcaseajaxphp">trunk/tests/phpunit/includes/testcase-ajax.php</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li><a href="#trunktestsphpunittestsajaxPrivacyErasePersonalDataphp">trunk/tests/phpunit/tests/ajax/PrivacyErasePersonalData.php</a></li>
<li><a href="#trunktestsphpunittestsajaxPrivacyExportPersonalDataphp">trunk/tests/phpunit/tests/ajax/PrivacyExportPersonalData.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunktestsphpunitincludestestcaseajaxphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/includes/testcase-ajax.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/includes/testcase-ajax.php    2019-03-15 18:06:08 UTC (rev 44908)
+++ trunk/tests/phpunit/includes/testcase-ajax.php      2019-03-15 18:07:09 UTC (rev 44909)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -119,6 +119,8 @@
</span><span class="cx" style="display: block; padding: 0 10px">                'delete-theme',
</span><span class="cx" style="display: block; padding: 0 10px">                'install-theme',
</span><span class="cx" style="display: block; padding: 0 10px">                'get-post-thumbnail-html',
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                'wp-privacy-export-personal-data',
+               'wp-privacy-erase-personal-data',
</ins><span class="cx" style="display: block; padding: 0 10px">         );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        public static function setUpBeforeClass() {
</span></span></pre></div>
<a id="trunktestsphpunittestsajaxPrivacyErasePersonalDataphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/ajax/PrivacyErasePersonalData.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/ajax/PrivacyErasePersonalData.php                               (rev 0)
+++ trunk/tests/phpunit/tests/ajax/PrivacyErasePersonalData.php 2019-03-15 18:07:09 UTC (rev 44909)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,814 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?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 );
+               }
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/ajax/PrivacyErasePersonalData.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svnexecutable"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:executable</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+*
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsajaxPrivacyExportPersonalDataphp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/ajax/PrivacyExportPersonalData.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/ajax/PrivacyExportPersonalData.php                              (rev 0)
+++ trunk/tests/phpunit/tests/ajax/PrivacyExportPersonalData.php        2019-03-15 18:07:09 UTC (rev 44909)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,818 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?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 );
+               }
+       }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/ajax/PrivacyExportPersonalData.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svnexecutable"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:executable</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+*
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span></div>

</body>
</html>