From 3b07fc9169a2ba60a48d1fb8725375ab0a4c0ba7 Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Thu, 4 Jun 2026 14:17:21 -0400 Subject: [PATCH 1/2] Add PHPUnit tests for AJAX media upload and sub-sizes creation functionality --- .../ajax-actions/mediaCreateImageSubsizes.php | 215 ++++++++++++++++++ .../ajax-actions/uploadAttachment.php | 168 ++++++++++++++ 2 files changed, 383 insertions(+) create mode 100644 tests/phpunit/tests/admin/includes/ajax-actions/mediaCreateImageSubsizes.php create mode 100644 tests/phpunit/tests/admin/includes/ajax-actions/uploadAttachment.php diff --git a/tests/phpunit/tests/admin/includes/ajax-actions/mediaCreateImageSubsizes.php b/tests/phpunit/tests/admin/includes/ajax-actions/mediaCreateImageSubsizes.php new file mode 100644 index 0000000000000..0b7c7b595b2c7 --- /dev/null +++ b/tests/phpunit/tests/admin/includes/ajax-actions/mediaCreateImageSubsizes.php @@ -0,0 +1,215 @@ +user->create( array( 'role' => 'administrator' ) ); + self::$attachment_id = $factory->attachment->create_object( + DIR_TESTDATA . '/images/canola.jpg', + 0, + array( + 'post_mime_type' => 'image/jpeg', + 'post_type' => 'attachment', + ) + ); + } + + public function set_up(): void { + parent::set_up(); + add_action( 'wp_ajax_media-create-image-subsizes', 'wp_ajax_media_create_image_subsizes', 1 ); + } + + /** + * Tests failure due to invalid nonce. + * + * @ticket 65252 + */ + public function test_media_create_image_subsizes_invalid_nonce(): void { + wp_set_current_user( self::$admin_id ); + + $_POST = array( + 'action' => 'media-create-image-subsizes', + '_ajax_nonce' => 'invalid-nonce', + ); + + try { + $this->_handleAjax( 'media-create-image-subsizes' ); + } catch ( WPAjaxDieStopException $e ) { + $this->assertSame( '-1', $e->getMessage() ); + } catch ( WPAjaxDieContinueException $e ) { + $this->assertSame( '-1', $this->_last_response ); + } + } + + /** + * Tests failure due to insufficient permissions. + * + * @ticket 65252 + */ + public function test_media_create_image_subsizes_insufficient_permissions(): void { + $subscriber_id = self::factory()->user->create( array( 'role' => 'subscriber' ) ); + wp_set_current_user( $subscriber_id ); + + $_POST = array( + 'action' => 'media-create-image-subsizes', + '_ajax_nonce' => wp_create_nonce( 'media-form' ), + ); + + try { + $this->_handleAjax( 'media-create-image-subsizes' ); + } catch ( WPAjaxDieStopException $e ) { + } catch ( WPAjaxDieContinueException $e ) { + } + + $response = json_decode( $this->_last_response, true ); + $this->assertFalse( $response['success'] ); + $this->assertSame( 'Sorry, you are not allowed to upload files.', $response['data']['message'] ); + } + + /** + * Tests failure due to missing attachment_id. + * + * @ticket 65252 + */ + public function test_media_create_image_subsizes_missing_attachment_id(): void { + wp_set_current_user( self::$admin_id ); + + $_POST = array( + 'action' => 'media-create-image-subsizes', + '_ajax_nonce' => wp_create_nonce( 'media-form' ), + ); + + try { + $this->_handleAjax( 'media-create-image-subsizes' ); + } catch ( WPAjaxDieStopException $e ) { + } catch ( WPAjaxDieContinueException $e ) { + } + + $response = json_decode( $this->_last_response, true ); + $this->assertFalse( $response['success'] ); + $this->assertSame( 'Upload failed. Please reload and try again.', $response['data']['message'] ); + } + + /** + * Tests successful sub-sizes creation with legacy support. + * + * @ticket 65252 + */ + public function test_media_create_image_subsizes_legacy_success(): void { + wp_set_current_user( self::$admin_id ); + + $_POST = array( + 'action' => 'media-create-image-subsizes', + 'attachment_id' => self::$attachment_id, + '_legacy_support' => 1, + '_ajax_nonce' => wp_create_nonce( 'media-form' ), + ); + + try { + $this->_handleAjax( 'media-create-image-subsizes' ); + } catch ( WPAjaxDieStopException $e ) { + } catch ( WPAjaxDieContinueException $e ) { + } + + $response = json_decode( $this->_last_response, true ); + $this->assertTrue( $response['success'], 'AJAX response should be successful' ); + $this->assertSame( self::$attachment_id, $response['data']['id'] ); + } + + /** + * Tests successful sub-sizes creation with full response. + * + * @ticket 65252 + */ + public function test_media_create_image_subsizes_full_success(): void { + wp_set_current_user( self::$admin_id ); + + $_POST = array( + 'action' => 'media-create-image-subsizes', + 'attachment_id' => self::$attachment_id, + '_ajax_nonce' => wp_create_nonce( 'media-form' ), + ); + + try { + $this->_handleAjax( 'media-create-image-subsizes' ); + } catch ( WPAjaxDieStopException $e ) { + } catch ( WPAjaxDieContinueException $e ) { + } + + $response = json_decode( $this->_last_response, true ); + $this->assertTrue( $response['success'], 'AJAX response should be successful' ); + $this->assertSame( self::$attachment_id, $response['data']['id'] ); + $this->assertArrayHasKey( 'sizes', $response['data'] ); + } + + /** + * Tests failed upload cleanup. + * + * @ticket 65252 + */ + public function test_media_create_image_subsizes_cleanup_success(): void { + wp_set_current_user( self::$admin_id ); + + // Create a fresh attachment for cleanup. + $attachment_id = self::factory()->attachment->create_object( + DIR_TESTDATA . '/images/canola.jpg', + 0, + array( + 'post_mime_type' => 'image/jpeg', + 'post_type' => 'attachment', + ) + ); + + $_POST = array( + 'action' => 'media-create-image-subsizes', + 'attachment_id' => $attachment_id, + '_wp_upload_failed_cleanup' => 1, + '_ajax_nonce' => wp_create_nonce( 'media-form' ), + ); + + try { + $this->_handleAjax( 'media-create-image-subsizes' ); + } catch ( WPAjaxDieStopException $e ) { + } catch ( WPAjaxDieContinueException $e ) { + } + + $response = json_decode( $this->_last_response, true ); + $this->assertTrue( $response['success'], 'AJAX response should be successful' ); + $this->assertNull( get_post( $attachment_id ), 'Attachment should be deleted' ); + } +} diff --git a/tests/phpunit/tests/admin/includes/ajax-actions/uploadAttachment.php b/tests/phpunit/tests/admin/includes/ajax-actions/uploadAttachment.php new file mode 100644 index 0000000000000..1cf5f342056c2 --- /dev/null +++ b/tests/phpunit/tests/admin/includes/ajax-actions/uploadAttachment.php @@ -0,0 +1,168 @@ +user->create( array( 'role' => 'administrator' ) ); + } + + public function set_up(): void { + parent::set_up(); + add_action( 'wp_ajax_upload-attachment', 'wp_ajax_upload_attachment', 1 ); + + // Force the action to be something other than wp_handle_upload to bypass is_uploaded_file check. + add_filter( 'wp_handle_upload_overrides', function( $overrides ) { + $overrides['action'] = 'wp_handle_sideload'; + return $overrides; + } ); + } + + /** + * Tests failure due to invalid nonce. + * + * @ticket 65252 + */ + public function test_upload_attachment_invalid_nonce(): void { + wp_set_current_user( self::$admin_id ); + + $_POST = array( + 'action' => 'upload-attachment', + '_ajax_nonce' => 'invalid-nonce', + ); + + try { + $this->_handleAjax( 'upload-attachment' ); + } catch ( WPAjaxDieStopException $e ) { + $this->assertSame( '-1', $e->getMessage() ); + } catch ( WPAjaxDieContinueException $e ) { + $this->assertSame( '-1', $this->_last_response ); + } + } + + /** + * Tests failure due to insufficient permissions. + * + * @ticket 65252 + */ + public function test_upload_attachment_insufficient_permissions(): void { + $subscriber_id = self::factory()->user->create( array( 'role' => 'subscriber' ) ); + wp_set_current_user( $subscriber_id ); + + $_POST = array( + 'action' => 'upload-attachment', + '_ajax_nonce' => wp_create_nonce( 'media-form' ), + ); + + // wp_ajax_upload_attachment() uses echo and wp_die() instead of wp_send_json_error(). + try { + $this->_handleAjax( 'upload-attachment' ); + } catch ( WPAjaxDieStopException $e ) { + } catch ( WPAjaxDieContinueException $e ) { + } + + $response = json_decode( $this->_last_response, true ); + $this->assertFalse( $response['success'] ); + $this->assertSame( 'Sorry, you are not allowed to upload files.', $response['data']['message'] ); + } + /** + * Tests successful upload. + * + * @ticket 65252 + */ + public function test_upload_attachment_success(): void { + wp_set_current_user( self::$admin_id ); + + $_POST = array( + 'action' => 'upload-attachment', + '_ajax_nonce' => wp_create_nonce( 'media-form' ), + ); + + $_FILES = array( + 'async-upload' => array( + 'name' => 'canola.jpg', + 'type' => 'image/jpeg', + 'tmp_name' => DIR_TESTDATA . '/images/canola.jpg', + 'error' => 0, + 'size' => filesize( DIR_TESTDATA . '/images/canola.jpg' ), + ), + ); + + // Short-circuit media_handle_upload() by filtering 'wp_handle_upload_prefilter'. + // Wait, media_handle_upload() is a function. It calls wp_handle_upload(). + // We already tried many filters. + + // Let's use a very late filter in wp_insert_attachment or something? No. + + // The issue is that _wp_handle_upload returns an error array because is_uploaded_file fails. + // If we use the 'wp_handle_upload_prefilter' to set 'error' to something, + // it will go to the error handler. + + add_filter( 'wp_handle_upload_prefilter', function( $file ) { + $file['error'] = 'mock_success'; + return $file; + } ); + + // We MUST ensure the error handler returns what we want. + // The default error handler is wp_handle_upload_error which returns array('error' => $message). + // media_handle_upload checks for isset($file['error']). + + // What if we filter 'wp_handle_upload_overrides' to change the error handler? + add_filter( 'wp_handle_upload_overrides', function( $overrides ) { + $overrides['upload_error_handler'] = function( $file, $message ) { + if ( 'mock_success' === $message ) { + $upload_dir = wp_upload_dir(); + $new_file = $upload_dir['path'] . '/canola.jpg'; + @copy( DIR_TESTDATA . '/images/canola.jpg', $new_file ); + return array( + 'file' => $new_file, + 'url' => $upload_dir['url'] . '/canola.jpg', + 'type' => 'image/jpeg', + ); + } + return array( 'error' => $message ); + }; + return $overrides; + }, 10 ); + + try { + $this->_handleAjax( 'upload-attachment' ); + } catch ( WPAjaxDieStopException $e ) { + } catch ( WPAjaxDieContinueException $e ) { + } + + $response = json_decode( $this->_last_response, true ); + if ( ! isset( $response['success'] ) || ! $response['success'] ) { + $this->fail( 'AJAX response was not successful. Response: ' . $this->_last_response ); + } + $this->assertTrue( $response['success'] ); + $this->assertSame( 'canola.jpg', $response['data']['filename'] ); + } +} From 36c64594255c2e16ca20fe9836b42cded2487b20 Mon Sep 17 00:00:00 2001 From: Paul Bearne Date: Thu, 4 Jun 2026 14:23:28 -0400 Subject: [PATCH 2/2] Fix formatting of comment in uploadAttachment test --- .../tests/admin/includes/ajax-actions/uploadAttachment.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/admin/includes/ajax-actions/uploadAttachment.php b/tests/phpunit/tests/admin/includes/ajax-actions/uploadAttachment.php index 1cf5f342056c2..c415b25a0ff34 100644 --- a/tests/phpunit/tests/admin/includes/ajax-actions/uploadAttachment.php +++ b/tests/phpunit/tests/admin/includes/ajax-actions/uploadAttachment.php @@ -38,8 +38,8 @@ public function set_up(): void { parent::set_up(); add_action( 'wp_ajax_upload-attachment', 'wp_ajax_upload_attachment', 1 ); - // Force the action to be something other than wp_handle_upload to bypass is_uploaded_file check. - add_filter( 'wp_handle_upload_overrides', function( $overrides ) { + // Force the action to be something other than wp_handle_upload to bypass the is_uploaded_file check. + add_filter( 'wp_handle_upload_overrides', function ( $overrides ) { $overrides['action'] = 'wp_handle_sideload'; return $overrides; } );