assertTrue( version_compare( PHP_VERSION, '7.4', '>=' ) ); } /** * Verify the persisted option name remains stable. */ public function test_option_key_is_stable(): void { $this->assertSame( 'rss2cpt_settings', RSS2CPT\Options::KEY ); } /** * Verify backfills use a safe bounded default. */ public function test_default_import_limit_is_bounded(): void { $settings = RSS2CPT\Options::get(); $this->assertSame( 25, $settings['item_limit'] ); } /** * Verify ongoing imports do not inherit WordPress's long feed cache. */ public function test_importer_uses_short_feed_cache(): void { $importer = new RSS2CPT\Import\Importer(); $this->assertSame( 300, $importer->filter_feed_cache_lifetime( 43200, 'https://example.test/feed.xml' ) ); } /** * Verify the imported post_date/post_date_gmt come from the feed timestamp * on both create and update paths, with no timezone drift. */ public function test_postarr_uses_feed_timestamp_when_present(): void { $importer = new RSS2CPT\Import\Importer(); $post_type = ( new \ReflectionClass( $importer ) )->getProperty( 'post_type' ); $post_type->setAccessible( true ); $post_type->setValue( $importer, 'podcast_episode' ); $method = ( new \ReflectionClass( $importer ) )->getMethod( 'build_postarr' ); $method->setAccessible( true ); $timestamp = 1700000000; // 2023-11-14T22:13:20Z, a known UTC moment. $data = self::sample_item_data( $timestamp ); $expected_gmt = gmdate( 'Y-m-d H:i:s', $timestamp ); $postarr = $method->invoke( $importer, $data, 'https://example.test/feed.xml', array( 'post_status' => 'publish', 'post_author' => 1, ) ); $this->assertSame( $expected_gmt, $postarr['post_date_gmt'] ); $this->assertSame( get_date_from_gmt( $expected_gmt ), $postarr['post_date'] ); } /** * Verify that a feed item without a timestamp leaves post_date unset, * letting WordPress default to the import time on insert/update. */ public function test_postarr_omits_post_date_when_feed_timestamp_missing(): void { $importer = new RSS2CPT\Import\Importer(); $post_type = ( new \ReflectionClass( $importer ) )->getProperty( 'post_type' ); $post_type->setAccessible( true ); $post_type->setValue( $importer, 'podcast_episode' ); $method = ( new \ReflectionClass( $importer ) )->getMethod( 'build_postarr' ); $method->setAccessible( true ); $postarr = $method->invoke( $importer, self::sample_item_data( 0 ), 'https://example.test/feed.xml', array( 'post_status' => 'publish', 'post_author' => 1, ) ); $this->assertArrayNotHasKey( 'post_date', $postarr ); $this->assertArrayNotHasKey( 'post_date_gmt', $postarr ); } /** * Build a complete sample item-data array. * * Both build_postarr() and the helper it calls, build_meta_input(), read * every field that map_item() produces. Supplying the full set keeps the * tests focused on the post_date behavior we are pinning down. * * @param int $timestamp Optional feed timestamp (zero means "no date"). * @return array */ private static function sample_item_data( int $timestamp ): array { return array( 'title' => 'Episode', 'content' => 'Body', 'excerpt' => 'Teaser', 'timestamp' => $timestamp, 'link' => 'https://example.test/episode', 'audio_url' => 'https://example.test/episode.mp3', 'audio_type' => 'audio/mpeg', 'audio_length' => 0, 'duration' => '', 'duration_seconds' => 0, 'season' => 0, 'episode' => 0, 'episode_type' => '', 'explicit' => '0', 'source_id' => 'src-1', 'source_key' => str_repeat( 'a', 64 ), 'creator' => '', 'fingerprint' => str_repeat( 'b', 64 ), ); } /** * Verify that clear_feed_cache deletes the SimplePie feed-cache option * rows, the importer's per-feed lock, and the multisite site-transient * variants derived from the configured feed URL. */ public function test_clear_feed_cache_removes_lock_and_simplepie_transient_keys(): void { rss2cpt_test_reset_delete_logs(); $importer = new RSS2CPT\Import\Importer(); $settings = new RSS2CPT\Admin\Settings( $importer, new RSS2CPT\Scheduler( $importer ) ); $cleared = $settings->clear_feed_cache( 'https://example.test/feed.xml', 'podcast_episode' ); $this->assertTrue( $cleared ); $this->assertContains( '_transient_feed_' . md5( 'https://example.test/feed.xml' ), $GLOBALS['rss2cpt_test_deleted_options'] ); $this->assertContains( '_transient_timeout_feed_' . md5( 'https://example.test/feed.xml' ), $GLOBALS['rss2cpt_test_deleted_options'] ); $this->assertContains( '_transient_feed_mod_' . md5( 'https://example.test/feed.xml' ), $GLOBALS['rss2cpt_test_deleted_options'] ); $this->assertContains( '_transient_timeout_feed_mod_' . md5( 'https://example.test/feed.xml' ), $GLOBALS['rss2cpt_test_deleted_options'] ); $this->assertContains( 'rss2cpt_lock_' . md5( 'https://example.test/feed.xml|podcast_episode' ), $GLOBALS['rss2cpt_test_deleted_options'] ); $this->assertContains( 'feed_' . md5( 'https://example.test/feed.xml' ), $GLOBALS['rss2cpt_test_deleted_site_transients'] ); $this->assertContains( 'feed_mod_' . md5( 'https://example.test/feed.xml' ), $GLOBALS['rss2cpt_test_deleted_site_transients'] ); } /** * Verify that clear_feed_cache refuses to do anything destructive when * the feed URL is empty (so an unconfigured install cannot wipe someone * else's transient rows by accident). */ public function test_clear_feed_cache_is_noop_when_feed_url_empty(): void { rss2cpt_test_reset_delete_logs(); $importer = new RSS2CPT\Import\Importer(); $settings = new RSS2CPT\Admin\Settings( $importer, new RSS2CPT\Scheduler( $importer ) ); $cleared = $settings->clear_feed_cache( '', 'podcast_episode' ); $this->assertFalse( $cleared ); $this->assertSame( array(), $GLOBALS['rss2cpt_test_deleted_options'] ); $this->assertSame( array(), $GLOBALS['rss2cpt_test_deleted_site_transients'] ); } }