- -
-| - | syncPairs() ) ); ?> | -
|---|---|
| - | loggingLevel() ); ?> | -
| - |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
-
| - | conflictStrategy() ); ?> | -
| + | + |
|---|---|
| + | + |
| + | + |
| + | + |
| + | + + + | +
| + | + + | +
| + |
+
+ + + |
+
| + | + '', 'destination' => '' ); ?> + + + | +
| + | + + | +
|---|---|
| + | + + | +
| + | + + | +
| + | + |
| + | + + | +
Keep HTML
', + 'post_excerpt' => 'Excerpt', + 'post_status' => 'publish', + 'post_name' => 'hello-world', + 'post_parent' => '7', + 'menu_order' => '3', + 'meta' => array( + '_source_url' => 'https://source.test/page', + ), + ) + ); + + self::assertSame( + array( + 'id' => 42, + 'post_type' => 'post', + 'post_title' => 'Hello World', + 'post_content' => 'Keep HTML
', + 'post_excerpt' => 'Excerpt', + 'post_status' => 'publish', + 'post_name' => 'hello-world', + 'post_parent' => 7, + 'menu_order' => 3, + 'meta' => array( + '_source_url' => 'https://source.test/page', + ), + ), + $record + ); + } + + public function test_it_normalizes_term_records(): void { + $normalizer = new ContentRecordNormalizer(); + + $record = $normalizer->term( + array( + 'id' => '9', + 'taxonomy' => 'category', + 'name' => "News\nUpdates", + 'slug' => 'news-updates', + 'description' => 'Keep description HTML
', + 'parent' => '2', + 'meta' => array( + 'landing_url' => 'https://source.test/news', + ), + ) + ); + + self::assertSame( + array( + 'id' => 9, + 'taxonomy' => 'category', + 'name' => 'News Updates', + 'slug' => 'news-updates', + 'description' => 'Keep description HTML
', + 'parent' => 2, + 'meta' => array( + 'landing_url' => 'https://source.test/news', + ), + ), + $record + ); + } + + public function test_it_normalizes_media_records(): void { + $normalizer = new ContentRecordNormalizer(); + + $record = $normalizer->media( + array( + 'id' => '12', + 'post_title' => "Hero\nImage", + 'post_mime_type' => 'image/jpeg', + 'source_url' => 'https://source.test/uploads/hero.jpg', + 'metadata' => array( + 'width' => 1200, + ), + 'meta' => array( + '_wp_attachment_image_alt' => 'Hero', + ), + ) + ); + + self::assertSame( + array( + 'id' => 12, + 'post_title' => 'Hero Image', + 'post_mime_type' => 'image/jpeg', + 'source_url' => 'https://source.test/uploads/hero.jpg', + 'metadata' => array( + 'width' => 1200, + ), + 'meta' => array( + '_wp_attachment_image_alt' => 'Hero', + ), + ), + $record + ); + } +} diff --git a/tests/Unit/Content/MediaContentHandlerTest.php b/tests/Unit/Content/MediaContentHandlerTest.php new file mode 100644 index 0000000..191ee54 --- /dev/null +++ b/tests/Unit/Content/MediaContentHandlerTest.php @@ -0,0 +1,276 @@ +> */ + private array $logs = array(); + + protected function tearDown(): void { + unset( + $GLOBALS['wpcs_test_posts'], + $GLOBALS['wpcs_test_next_post_id'], + $GLOBALS['wpcs_test_post_meta'], + $GLOBALS['wpcs_test_attachment_files'], + $GLOBALS['wpcs_test_attachment_metadata'] + ); + + $this->logs = array(); + + parent::tearDown(); + } + + public function test_it_creates_attachment_records_without_downloading_files(): void { + $result = $this->handler()->importRecords( + array( + $this->mediaRecord(), + ), + $this->context( 'last_write_wins' ) + ); + + self::assertTrue( $result->isSuccessful() ); + self::assertSame( 1, $result->created() ); + self::assertSame( 'Imported Image', get_post( 1 )['post_title'] ); + self::assertSame( 'attachment', get_post( 1 )['post_type'] ); + self::assertSame( 'image/jpeg', get_post( 1 )['post_mime_type'] ); + self::assertSame( 42, get_post_meta( 1, '_wpcs_source_id', true ) ); + self::assertSame( 'https://source.test', get_post_meta( 1, '_wpcs_source_site', true ) ); + self::assertSame( 'https://destination.test/uploads/image.jpg', get_post_meta( 1, '_wpcs_source_url', true ) ); + self::assertSame( array( false ), $GLOBALS['wpcs_test_attachment_files'] ); + self::assertSame( 'Skipped media binary download; importing attachment metadata only.', $this->logs[0]['message'] ); + } + + public function test_it_updates_attachment_metadata_with_last_write_wins(): void { + $attachment_id = wp_insert_attachment( + array( + 'post_title' => 'Old Image', + 'post_mime_type' => 'image/jpeg', + ), + false, + 0, + true + ); + update_post_meta( $attachment_id, '_wpcs_source_id', 42 ); + update_post_meta( $attachment_id, '_wpcs_source_site', 'https://source.test' ); + + $result = $this->handler()->importRecords( + array( + $this->mediaRecord( + array( + 'post_title' => 'Updated Image', + 'metadata' => array( + 'width' => 1200, + 'height' => 800, + ), + ) + ), + ), + $this->context( 'last_write_wins' ) + ); + + self::assertTrue( $result->isSuccessful() ); + self::assertSame( 1, $result->updated() ); + self::assertSame( 'Updated Image', get_post( $attachment_id )['post_title'] ); + self::assertSame( + array( + 'width' => 1200, + 'height' => 800, + ), + wp_get_attachment_metadata( $attachment_id ) + ); + } + + public function test_it_does_not_match_existing_media_from_a_different_source_site(): void { + $attachment_id = wp_insert_attachment( + array( + 'post_title' => 'Other Site Image', + 'post_mime_type' => 'image/jpeg', + ), + false, + 0, + true + ); + update_post_meta( $attachment_id, '_wpcs_source_id', 42 ); + update_post_meta( $attachment_id, '_wpcs_source_site', 'https://other-source.test' ); + + $result = $this->handler()->importRecords( + array( + $this->mediaRecord( array( 'post_title' => 'Current Site Image' ) ), + ), + $this->context( 'last_write_wins' ) + ); + + self::assertTrue( $result->isSuccessful() ); + self::assertSame( 1, $result->created() ); + self::assertSame( 'Other Site Image', get_post( $attachment_id )['post_title'] ); + self::assertSame( 'Current Site Image', get_post( 2 )['post_title'] ); + } + + public function test_it_rewrites_source_url_metadata_and_meta_urls(): void { + $result = $this->handler()->importRecords( + array( + $this->mediaRecord( + array( + 'metadata' => array( + 'file' => 'https://source.test/uploads/image.jpg', + 'sizes' => array( + 'thumbnail' => array( + 'url' => 'https://source.test/uploads/image-150x150.jpg', + ), + ), + ), + 'meta' => array( + '_source_url' => 'https://source.test/uploads/image.jpg', + '_json_links' => '{"url":"https://source.test/uploads/image.jpg"}', + ), + ) + ), + ), + $this->context( 'last_write_wins' ) + ); + + $metadata = wp_get_attachment_metadata( 1 ); + + self::assertTrue( $result->isSuccessful() ); + self::assertSame( 'https://destination.test/uploads/image.jpg', get_post_meta( 1, '_wpcs_source_url', true ) ); + self::assertSame( 'https://destination.test/uploads/image.jpg', $metadata['file'] ); + self::assertSame( 'https://destination.test/uploads/image-150x150.jpg', $metadata['sizes']['thumbnail']['url'] ); + self::assertSame( 'https://destination.test/uploads/image.jpg', get_post_meta( 1, '_source_url', true ) ); + self::assertSame( '{"url":"https:\/\/destination.test\/uploads\/image.jpg"}', get_post_meta( 1, '_json_links', true ) ); + } + + public function test_it_skips_existing_media_with_manual_review_conflict(): void { + $attachment_id = wp_insert_attachment( + array( + 'post_title' => 'Old Image', + 'post_mime_type' => 'image/jpeg', + ), + false, + 0, + true + ); + update_post_meta( $attachment_id, '_wpcs_source_id', 42 ); + update_post_meta( $attachment_id, '_wpcs_source_site', 'https://source.test' ); + + $result = $this->handler()->importRecords( + array( + $this->mediaRecord( array( 'post_title' => 'Updated Image' ) ), + ), + $this->context( 'manual_review' ) + ); + + self::assertTrue( $result->isSuccessful() ); + self::assertSame( 1, $result->skipped() ); + self::assertSame( 1, $result->conflicts() ); + self::assertSame( 'Old Image', get_post( $attachment_id )['post_title'] ); + self::assertSame( 'Skipped media import because manual review is required.', $this->logs[0]['message'] ); + } + + public function test_it_returns_failure_when_wordpress_rejects_attachment_save(): void { + $result = $this->handler()->importRecords( + array( + $this->mediaRecord( + array( + 'id' => 0, + 'post_mime_type' => '', + ) + ), + ), + $this->context( 'last_write_wins' ) + ); + + self::assertFalse( $result->isSuccessful() ); + self::assertSame( array( 'Media import failed for source ID 0.' ), $result->errors() ); + self::assertSame( array(), get_post_meta( 0, '_wpcs_source_id', false ) ); + } + + private function handler(): MediaContentHandler { + return new MediaContentHandler( + new ContentRecordNormalizer(), + new UrlTransformer(), + new MetadataUrlTransformer( new UrlTransformer() ), + $this->logger() + ); + } + + private function context( string $conflict_strategy ): SyncContext { + return SyncContext::forImport( + array( 'site_url' => 'https://source.test' ), + array( 'site_url' => 'https://destination.test' ), + $conflict_strategy, + 'operation-1' + ); + } + + /** + * @param array