Adds a 'Season taxonomy' setting (default empty) that points at an existing taxonomy registered against the destination post type. On every successful import the plugin reads itunes:season, looks up (or creates) a term with that numeric name in the configured taxonomy, and assigns it to the imported post via wp_set_object_terms. Behavior: - Empty option: no-op. Existing posts and installs without a season taxonomy are unaffected. - Tax that doesn't exist or isn't attached to the post type: no-op. - Feed item with no itunes:season: no-op. Manual taxonomy assignments are preserved on subsequent refreshes. - Otherwise: the feed value wins, on both create and update paths. The new assign_season_term() helper is public so it is unit-testable, and four smoke tests cover the lookup, create-on-miss, unconfigured, and no-season paths.
305 lines
11 KiB
PHP
305 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* Basic project smoke tests.
|
|
*
|
|
* @package RSS2CPT
|
|
*/
|
|
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
require_once dirname( __DIR__ ) . '/src/Scheduler.php';
|
|
require_once dirname( __DIR__ ) . '/src/Admin/Settings.php';
|
|
|
|
/**
|
|
* Verifies project-level invariants without loading WordPress.
|
|
*/
|
|
final class SmokeTest extends TestCase {
|
|
/**
|
|
* Verify the runtime supports the plugin baseline.
|
|
*/
|
|
public function test_supported_php_runtime(): void {
|
|
$this->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<string,mixed>
|
|
*/
|
|
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'] );
|
|
}
|
|
|
|
/**
|
|
* Verify that assign_season_term resolves an existing numeric term and
|
|
* assigns it to the post without creating a duplicate.
|
|
*/
|
|
public function test_assign_season_term_uses_existing_term(): void {
|
|
rss2cpt_test_reset_term_stores();
|
|
|
|
$GLOBALS['rss2cpt_test_taxonomies']['series_season'] = true;
|
|
$GLOBALS['rss2cpt_test_object_in_taxonomy']['podcast_episode']['series_season'] = true;
|
|
$GLOBALS['rss2cpt_test_terms']['series_season']['8'] = 42;
|
|
|
|
$importer = new RSS2CPT\Import\Importer();
|
|
$method = ( new \ReflectionClass( $importer ) )->getMethod( 'assign_season_term' );
|
|
$method->setAccessible( true );
|
|
|
|
$post_type = ( new \ReflectionClass( $importer ) )->getProperty( 'post_type' );
|
|
$post_type->setAccessible( true );
|
|
$post_type->setValue( $importer, 'podcast_episode' );
|
|
|
|
$term_id = $method->invoke(
|
|
$importer,
|
|
123,
|
|
array( 'season' => 8 ),
|
|
array( 'season_taxonomy' => 'series_season' )
|
|
);
|
|
|
|
$this->assertSame( 42, $term_id );
|
|
$this->assertCount( 1, $GLOBALS['rss2cpt_test_object_terms'] );
|
|
$this->assertSame(
|
|
array(
|
|
'object_id' => 123,
|
|
'term_ids' => array( 42 ),
|
|
'taxonomy' => 'series_season',
|
|
),
|
|
$GLOBALS['rss2cpt_test_object_terms'][0]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Verify that assign_season_term creates the term when the feed exposes
|
|
* a season number whose term does not yet exist (e.g. a brand-new season).
|
|
*/
|
|
public function test_assign_season_term_creates_missing_term(): void {
|
|
rss2cpt_test_reset_term_stores();
|
|
|
|
$GLOBALS['rss2cpt_test_taxonomies']['series_season'] = true;
|
|
$GLOBALS['rss2cpt_test_object_in_taxonomy']['podcast_episode']['series_season'] = true;
|
|
|
|
$importer = new RSS2CPT\Import\Importer();
|
|
$method = ( new \ReflectionClass( $importer ) )->getMethod( 'assign_season_term' );
|
|
$method->setAccessible( true );
|
|
|
|
$post_type = ( new \ReflectionClass( $importer ) )->getProperty( 'post_type' );
|
|
$post_type->setAccessible( true );
|
|
$post_type->setValue( $importer, 'podcast_episode' );
|
|
|
|
$term_id = $method->invoke(
|
|
$importer,
|
|
456,
|
|
array( 'season' => 9 ),
|
|
array( 'season_taxonomy' => 'series_season' )
|
|
);
|
|
|
|
$this->assertGreaterThan( 0, $term_id );
|
|
$this->assertArrayHasKey( '9', $GLOBALS['rss2cpt_test_terms']['series_season'] );
|
|
$this->assertSame( $term_id, $GLOBALS['rss2cpt_test_terms']['series_season']['9'] );
|
|
$this->assertCount( 1, $GLOBALS['rss2cpt_test_object_terms'] );
|
|
$this->assertSame( array( 456, $term_id, 'series_season' ), array( $GLOBALS['rss2cpt_test_object_terms'][0]['object_id'], $GLOBALS['rss2cpt_test_object_terms'][0]['term_ids'][0], $GLOBALS['rss2cpt_test_object_terms'][0]['taxonomy'] ) );
|
|
}
|
|
|
|
/**
|
|
* Verify that assign_season_term is a no-op when no season taxonomy is
|
|
* configured (the option is empty by default for installs that don't use one).
|
|
*/
|
|
public function test_assign_season_term_is_noop_when_taxonomy_unconfigured(): void {
|
|
rss2cpt_test_reset_term_stores();
|
|
|
|
$importer = new RSS2CPT\Import\Importer();
|
|
$method = ( new \ReflectionClass( $importer ) )->getMethod( 'assign_season_term' );
|
|
$method->setAccessible( true );
|
|
|
|
$post_type = ( new \ReflectionClass( $importer ) )->getProperty( 'post_type' );
|
|
$post_type->setAccessible( true );
|
|
$post_type->setValue( $importer, 'podcast_episode' );
|
|
|
|
$result = $method->invoke(
|
|
$importer,
|
|
789,
|
|
array( 'season' => 3 ),
|
|
array( 'season_taxonomy' => '' )
|
|
);
|
|
|
|
$this->assertFalse( $result );
|
|
$this->assertSame( array(), $GLOBALS['rss2cpt_test_object_terms'] );
|
|
}
|
|
|
|
/**
|
|
* Verify that assign_season_term is a no-op when the feed item has no
|
|
* itunes:season value (so existing manual taxonomy assignments are not
|
|
* stripped).
|
|
*/
|
|
public function test_assign_season_term_is_noop_when_feed_has_no_season(): void {
|
|
rss2cpt_test_reset_term_stores();
|
|
|
|
$GLOBALS['rss2cpt_test_taxonomies']['series_season'] = true;
|
|
$GLOBALS['rss2cpt_test_object_in_taxonomy']['podcast_episode']['series_season'] = true;
|
|
|
|
$importer = new RSS2CPT\Import\Importer();
|
|
$method = ( new \ReflectionClass( $importer ) )->getMethod( 'assign_season_term' );
|
|
$method->setAccessible( true );
|
|
|
|
$post_type = ( new \ReflectionClass( $importer ) )->getProperty( 'post_type' );
|
|
$post_type->setAccessible( true );
|
|
$post_type->setValue( $importer, 'podcast_episode' );
|
|
|
|
$result = $method->invoke(
|
|
$importer,
|
|
321,
|
|
array( 'season' => 0 ),
|
|
array( 'season_taxonomy' => 'series_season' )
|
|
);
|
|
|
|
$this->assertFalse( $result );
|
|
$this->assertSame( array(), $GLOBALS['rss2cpt_test_object_terms'] );
|
|
}
|
|
}
|