> */ 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 $overrides Record overrides. * @return array */ private function mediaRecord( array $overrides = array() ): array { return array_merge( array( 'id' => 42, 'post_title' => 'Imported Image', 'post_mime_type' => 'image/jpeg', 'source_url' => 'https://source.test/uploads/image.jpg', 'metadata' => array(), 'meta' => array(), ), $overrides ); } private function logger(): LoggerInterface { return new class( $this->logs ) implements LoggerInterface { /** @var array> */ private array $logs; /** * @param array> $logs Logs. */ public function __construct( array &$logs ) { $this->logs = &$logs; } public function error( string $message, array $context = array() ): void { $this->record( 'error', $message, $context ); } public function warning( string $message, array $context = array() ): void { $this->record( 'warning', $message, $context ); } public function info( string $message, array $context = array() ): void { $this->record( 'info', $message, $context ); } public function debug( string $message, array $context = array() ): void { $this->record( 'debug', $message, $context ); } /** * @param array $context Context. */ private function record( string $level, string $message, array $context ): void { $this->logs[] = array( 'level' => $level, 'message' => $message, 'context' => $context, ); } }; } }