Files
prautoblogger/tests/unit/Core/PublisherTest.php
peptiderepo b21d102afb feat: v0.2.2 — CI hardening, remove dead stubs, strengthen tests
Address CTO audit findings for PRAutoBlogger:
- CI pipeline now runs PHPCS (WordPress standards) and PHPUnit before deploy
- Removed TikTok/Instagram/YouTube stub providers (dead code, not registered)
- Rewrote PublisherTest with real behavioral assertions (post_status, metadata,
  taxonomy, log linking, hooks) instead of method-existence checks
- Added cross-system LLM budget coordination docs to ARCHITECTURE.md
- Bumped version to 0.2.2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 22:41:10 +07:00

285 lines
11 KiB
PHP

<?php
/**
* Tests for PRAutoBlogger_Publisher.
*
* Validates WordPress post creation via publish() and save_as_draft() methods,
* including correct post_status, metadata storage, taxonomy assignment, and
* generation log linking.
*
* @package PRAutoBlogger\Tests\Core
*/
namespace PRAutoBlogger\Tests\Core;
use PRAutoBlogger\Tests\BaseTestCase;
use Brain\Monkey\Functions;
use Brain\Monkey\Actions;
use Brain\Monkey\Filters;
class PublisherTest extends BaseTestCase {
private \PRAutoBlogger_Article_Idea $idea;
private \PRAutoBlogger_Editorial_Review $review;
protected function setUp(): void {
parent::setUp();
$this->idea = new \PRAutoBlogger_Article_Idea( $this->get_article_idea_fixture() );
$this->review = new \PRAutoBlogger_Editorial_Review( $this->get_editorial_review_fixture() );
// Common stubs needed by Publisher.
$this->stub_get_option( [
'prautoblogger_writing_pipeline' => 'multi_step',
'prautoblogger_writing_model' => 'anthropic/claude-sonnet-4',
'prautoblogger_default_author' => 1,
] );
Functions\when( 'sanitize_text_field' )->returnArg();
Functions\when( 'wp_kses_post' )->returnArg();
Functions\when( 'is_wp_error' )->justReturn( false );
Functions\when( 'apply_filters' )->returnArg( 2 );
Functions\when( 'absint' )->alias( function ( $val ) { return abs( (int) $val ); } );
Functions\when( 'get_users' )->justReturn( [ 1 ] );
Functions\when( 'get_term_by' )->justReturn( false );
Functions\when( 'wp_insert_term' )->justReturn( [ 'term_id' => 5 ] );
Functions\when( 'wp_set_post_categories' )->justReturn( true );
Functions\when( 'wp_set_post_tags' )->justReturn( true );
}
/**
* Test publish() creates a post with 'publish' status and returns post ID.
*/
public function test_publish_returns_post_id(): void {
$captured_args = null;
Functions\when( 'wp_insert_post' )->alias( function ( $args ) use ( &$captured_args ) {
$captured_args = $args;
return 42;
} );
$wpdb = $this->create_mock_wpdb();
$wpdb->method( 'query' )->willReturn( 0 );
$wpdb->method( 'prepare' )->willReturn( '' );
$GLOBALS['wpdb'] = $wpdb;
$publisher = new \PRAutoBlogger_Publisher();
$post_id = $publisher->publish( '<p>Test content</p>', $this->idea, $this->review, 'run_test_123' );
$this->assertSame( 42, $post_id );
$this->assertSame( 'publish', $captured_args['post_status'] );
$this->assertSame( 'post', $captured_args['post_type'] );
}
/**
* Test save_as_draft() creates a post with 'draft' status.
*/
public function test_save_as_draft_creates_draft(): void {
$captured_args = null;
Functions\when( 'wp_insert_post' )->alias( function ( $args ) use ( &$captured_args ) {
$captured_args = $args;
return 43;
} );
$wpdb = $this->create_mock_wpdb();
$wpdb->method( 'query' )->willReturn( 0 );
$wpdb->method( 'prepare' )->willReturn( '' );
$GLOBALS['wpdb'] = $wpdb;
$publisher = new \PRAutoBlogger_Publisher();
$post_id = $publisher->save_as_draft( '<p>Draft content</p>', $this->idea, $this->review );
$this->assertSame( 43, $post_id );
$this->assertSame( 'draft', $captured_args['post_status'] );
}
/**
* Test that generation metadata is stored in meta_input.
*/
public function test_publish_stores_generation_metadata(): void {
$captured_args = null;
Functions\when( 'wp_insert_post' )->alias( function ( $args ) use ( &$captured_args ) {
$captured_args = $args;
return 44;
} );
$wpdb = $this->create_mock_wpdb();
$wpdb->method( 'query' )->willReturn( 0 );
$wpdb->method( 'prepare' )->willReturn( '' );
$GLOBALS['wpdb'] = $wpdb;
$publisher = new \PRAutoBlogger_Publisher();
$publisher->publish( '<p>Content</p>', $this->idea, $this->review, 'run_meta_test' );
$meta = $captured_args['meta_input'];
// Verify all expected meta keys exist.
$this->assertSame( '1', $meta['_prautoblogger_generated'] );
$this->assertSame( 'approved', $meta['_prautoblogger_editor_verdict'] );
$this->assertSame( 0.88, $meta['_prautoblogger_quality_score'] );
$this->assertSame( 0.92, $meta['_prautoblogger_seo_score'] );
$this->assertSame( 'anthropic/claude-sonnet-4', $meta['_prautoblogger_model_used'] );
$this->assertSame( 'multi_step', $meta['_prautoblogger_pipeline_mode'] );
$this->assertSame( 'Test Article Topic', $meta['_prautoblogger_topic'] );
$this->assertSame( 'guide', $meta['_prautoblogger_article_type'] );
}
/**
* Test that post title comes from the idea's suggested_title.
*/
public function test_publish_uses_idea_title(): void {
$captured_args = null;
Functions\when( 'wp_insert_post' )->alias( function ( $args ) use ( &$captured_args ) {
$captured_args = $args;
return 45;
} );
$wpdb = $this->create_mock_wpdb();
$wpdb->method( 'query' )->willReturn( 0 );
$wpdb->method( 'prepare' )->willReturn( '' );
$GLOBALS['wpdb'] = $wpdb;
$publisher = new \PRAutoBlogger_Publisher();
$publisher->publish( '<p>Content</p>', $this->idea, $this->review );
$this->assertSame( 'Ultimate Guide to Test Topic', $captured_args['post_title'] );
}
/**
* Test that publish throws RuntimeException when wp_insert_post returns WP_Error.
*/
public function test_publish_throws_on_wp_error(): void {
Functions\when( 'wp_insert_post' )->justReturn( new \stdClass() );
Functions\when( 'is_wp_error' )->justReturn( true );
$wpdb = $this->create_mock_wpdb();
$GLOBALS['wpdb'] = $wpdb;
// Mock the WP_Error-like object to have get_error_message.
$error = $this->getMockBuilder( \stdClass::class )
->addMethods( [ 'get_error_message' ] )
->getMock();
$error->method( 'get_error_message' )->willReturn( 'DB insert failed' );
Functions\when( 'wp_insert_post' )->justReturn( $error );
$this->expectException( \RuntimeException::class );
$publisher = new \PRAutoBlogger_Publisher();
$publisher->publish( '<p>Content</p>', $this->idea, $this->review );
}
/**
* Test that taxonomy terms are assigned based on article_type from the idea.
*/
public function test_publish_assigns_category_from_article_type(): void {
Functions\when( 'wp_insert_post' )->justReturn( 46 );
// get_term_by returns false (category doesn't exist), so wp_insert_term is called.
$captured_category = null;
Functions\when( 'wp_insert_term' )->alias( function ( $name, $taxonomy ) use ( &$captured_category ) {
$captured_category = $name;
return [ 'term_id' => 10 ];
} );
$captured_category_ids = null;
Functions\when( 'wp_set_post_categories' )->alias( function ( $post_id, $cat_ids ) use ( &$captured_category_ids ) {
$captured_category_ids = $cat_ids;
return true;
} );
$wpdb = $this->create_mock_wpdb();
$wpdb->method( 'query' )->willReturn( 0 );
$wpdb->method( 'prepare' )->willReturn( '' );
$GLOBALS['wpdb'] = $wpdb;
$publisher = new \PRAutoBlogger_Publisher();
$publisher->publish( '<p>Content</p>', $this->idea, $this->review );
// Article type is 'guide' → category should be 'Guides'.
$this->assertSame( 'Guides', $captured_category );
$this->assertSame( [ 10 ], $captured_category_ids );
}
/**
* Test that target keywords are set as post tags.
*/
public function test_publish_sets_tags_from_keywords(): void {
Functions\when( 'wp_insert_post' )->justReturn( 47 );
$captured_tags = null;
Functions\when( 'wp_set_post_tags' )->alias( function ( $post_id, $tags, $append ) use ( &$captured_tags ) {
$captured_tags = $tags;
return true;
} );
$wpdb = $this->create_mock_wpdb();
$wpdb->method( 'query' )->willReturn( 0 );
$wpdb->method( 'prepare' )->willReturn( '' );
$GLOBALS['wpdb'] = $wpdb;
$publisher = new \PRAutoBlogger_Publisher();
$publisher->publish( '<p>Content</p>', $this->idea, $this->review );
$this->assertSame( [ 'test', 'keyword', 'example' ], $captured_tags );
}
/**
* Test that generation log entries are linked via run_id.
*/
public function test_publish_links_generation_logs_by_run_id(): void {
Functions\when( 'wp_insert_post' )->justReturn( 48 );
$captured_query = null;
$wpdb = $this->create_mock_wpdb();
$wpdb->method( 'prepare' )->willReturnCallback( function ( $sql, ...$args ) use ( &$captured_query ) {
$captured_query = $sql;
return $sql;
} );
$wpdb->method( 'query' )->willReturn( 1 );
$GLOBALS['wpdb'] = $wpdb;
$publisher = new \PRAutoBlogger_Publisher();
$publisher->publish( '<p>Content</p>', $this->idea, $this->review, 'run_link_test' );
// Should use run_id-based query, not timestamp-based.
$this->assertStringContainsString( 'run_id', $captured_query );
$this->assertStringNotContainsString( 'created_at', $captured_query );
}
/**
* Test that the prautoblogger_post_created action is fired after publishing.
*/
public function test_publish_fires_post_created_action(): void {
Functions\when( 'wp_insert_post' )->justReturn( 49 );
Functions\when( 'do_action' )->alias( function () {} );
Actions\expectDone( 'prautoblogger_post_created' )
->once()
->with( 49, 'publish', $this->idea, $this->review );
$wpdb = $this->create_mock_wpdb();
$wpdb->method( 'query' )->willReturn( 0 );
$wpdb->method( 'prepare' )->willReturn( '' );
$GLOBALS['wpdb'] = $wpdb;
$publisher = new \PRAutoBlogger_Publisher();
$publisher->publish( '<p>Content</p>', $this->idea, $this->review );
}
/**
* Test that the prautoblogger_filter_post_data filter is applied.
*/
public function test_publish_applies_post_data_filter(): void {
Functions\when( 'wp_insert_post' )->justReturn( 50 );
Filters\expectApplied( 'prautoblogger_filter_post_data' )->once();
$wpdb = $this->create_mock_wpdb();
$wpdb->method( 'query' )->willReturn( 0 );
$wpdb->method( 'prepare' )->willReturn( '' );
$GLOBALS['wpdb'] = $wpdb;
$publisher = new \PRAutoBlogger_Publisher();
$publisher->publish( '<p>Content</p>', $this->idea, $this->review );
}
}