code = $code; $this->message = $message; $this->data = (array) $data; } public function get_error_code(): string { return $this->code; } public function get_error_message(): string { return $this->message; } public function get_error_data() { return $this->data; } } } if ( ! function_exists( 'is_wp_error' ) ) { function is_wp_error( $thing ): bool { return $thing instanceof WP_Error; } } // Mock wp_remote_post — captures into $GLOBALS['_wpdo_remote_posts'] and returns simulated response. if ( ! function_exists( 'wp_remote_post' ) ) { function wp_remote_post( $url, $args = array() ) { $GLOBALS['_wpdo_remote_posts'][] = array( 'url' => $url, 'args' => $args ); // Default 200 OK; test can override via $GLOBALS['_wpdo_remote_status']. return array( 'response' => array( 'code' => $GLOBALS['_wpdo_remote_status'] ?? 200 ) ); } } if ( ! function_exists( 'wp_remote_retrieve_response_code' ) ) { function wp_remote_retrieve_response_code( $resp ) { return $resp['response']['code'] ?? 0; } } require_once dirname( __DIR__, 3 ) . '/includes/class-tmdo-logger.php'; require_once dirname( __DIR__, 3 ) . '/includes/notifications/abstract-class-tmdo-notifier.php'; require_once dirname( __DIR__, 3 ) . '/includes/notifications/class-tmdo-slack-notifier.php'; require_once dirname( __DIR__, 3 ) . '/includes/notifications/class-tmdo-discord-notifier.php'; require_once dirname( __DIR__, 3 ) . '/includes/notifications/class-tmdo-telegram-notifier.php'; /** * Unit tests for v2.5.0 M15 multi-channel notifiers. */ class MultiChannelNotifierTest extends TestCase { protected function setUp(): void { $GLOBALS['_wp_options'] = array(); $GLOBALS['_wpdo_remote_posts'] = array(); $GLOBALS['_wpdo_remote_status'] = 200; } private function summary(): array { return array( 'critical_count' => 1, 'recommended_count' => 0, 'ran_at' => '2026-04-28 03:30:00', 'tests' => array( 'wpdo_schema_drift' => array( 'status' => 'critical', 'description' => 'Missing tables: wpdo_audit', ), ), ); } // ─── Slack ──────────────────────────────────────────────────────── public function test_slack_default_disabled(): void { $this->assertFalse( WPDO_Slack_Notifier::is_enabled() ); } public function test_slack_skips_send_when_disabled(): void { $result = WPDO_Slack_Notifier::maybe_send( $this->summary() ); $this->assertFalse( $result ); $this->assertEmpty( $GLOBALS['_wpdo_remote_posts'] ); } public function test_slack_skips_when_webhook_invalid(): void { update_option( 'wpdo_slack_enabled', '1', false ); update_option( 'wpdo_slack_webhook', 'http://evil.com/wh', false ); $result = WPDO_Slack_Notifier::maybe_send( $this->summary() ); $this->assertFalse( $result ); } public function test_slack_sends_with_valid_webhook(): void { update_option( 'wpdo_slack_enabled', '1', false ); update_option( 'wpdo_slack_webhook', 'https://hooks.slack.com/services/T/B/X', false ); $result = WPDO_Slack_Notifier::maybe_send( $this->summary() ); $this->assertTrue( $result ); $this->assertCount( 1, $GLOBALS['_wpdo_remote_posts'] ); $captured = $GLOBALS['_wpdo_remote_posts'][0]; $this->assertSame( 'https://hooks.slack.com/services/T/B/X', $captured['url'] ); $payload = json_decode( (string) $captured['args']['body'], true ); $this->assertArrayHasKey( 'text', $payload ); $this->assertStringContainsString( 'WPDO 警告', $payload['text'] ); $this->assertStringContainsString( 'wpdo_schema_drift', $payload['text'] ); } public function test_slack_throttle_dedupes(): void { update_option( 'wpdo_slack_enabled', '1', false ); update_option( 'wpdo_slack_webhook', 'https://hooks.slack.com/services/T/B/X', false ); $first = WPDO_Slack_Notifier::maybe_send( $this->summary() ); $second = WPDO_Slack_Notifier::maybe_send( $this->summary() ); $this->assertTrue( $first ); $this->assertFalse( $second ); $this->assertCount( 1, $GLOBALS['_wpdo_remote_posts'] ); } // ─── Discord ───────────────────────────────────────────────────── public function test_discord_validates_webhook_prefix(): void { update_option( 'wpdo_discord_enabled', '1', false ); update_option( 'wpdo_discord_webhook', 'https://attack.example.com/x', false ); $this->assertFalse( WPDO_Discord_Notifier::maybe_send( $this->summary() ) ); } public function test_discord_sends_content_payload(): void { update_option( 'wpdo_discord_enabled', '1', false ); update_option( 'wpdo_discord_webhook', 'https://discord.com/api/webhooks/123/abc', false ); $result = WPDO_Discord_Notifier::maybe_send( $this->summary() ); $this->assertTrue( $result ); $captured = $GLOBALS['_wpdo_remote_posts'][0]; $payload = json_decode( (string) $captured['args']['body'], true ); $this->assertArrayHasKey( 'content', $payload ); } // ─── Telegram ──────────────────────────────────────────────────── public function test_telegram_skips_when_token_missing(): void { update_option( 'wpdo_telegram_enabled', '1', false ); // No token / chat_id. $this->assertFalse( WPDO_Telegram_Notifier::maybe_send( $this->summary() ) ); } public function test_telegram_validates_token_format(): void { update_option( 'wpdo_telegram_enabled', '1', false ); update_option( 'wpdo_telegram_bot_token', 'not_a_token', false ); update_option( 'wpdo_telegram_chat_id', '123', false ); $this->assertFalse( WPDO_Telegram_Notifier::maybe_send( $this->summary() ) ); } public function test_telegram_sends_when_valid(): void { update_option( 'wpdo_telegram_enabled', '1', false ); update_option( 'wpdo_telegram_bot_token', '123456:ABCDEFghijklmnopqrstuvwxyz0123456789', false ); update_option( 'wpdo_telegram_chat_id', '-1001234567890', false ); $result = WPDO_Telegram_Notifier::maybe_send( $this->summary() ); $this->assertTrue( $result ); $captured = $GLOBALS['_wpdo_remote_posts'][0]; $this->assertStringStartsWith( 'https://api.telegram.org/bot', $captured['url'] ); $payload = json_decode( (string) $captured['args']['body'], true ); $this->assertSame( '-1001234567890', $payload['chat_id'] ); $this->assertStringContainsString( '🚨', $payload['text'] ); } // ─── Severity filter ───────────────────────────────────────────── public function test_severity_critical_only_skips_when_no_critical(): void { update_option( 'wpdo_slack_enabled', '1', false ); update_option( 'wpdo_slack_webhook', 'https://hooks.slack.com/services/T/B/X', false ); update_option( 'wpdo_slack_severity', 'critical_only', false ); $summary_recommended_only = array( 'critical_count' => 0, 'recommended_count' => 2, 'ran_at' => '2026-04-28 03:30:00', 'tests' => array( 'wpdo_error_budget' => array( 'status' => 'recommended', 'description' => 'too many errors' ), ), ); $this->assertFalse( WPDO_Slack_Notifier::maybe_send( $summary_recommended_only ) ); } // ─── Channel ID identity ──────────────────────────────────────── public function test_channel_ids(): void { $this->assertSame( 'slack', WPDO_Slack_Notifier::channel_id() ); $this->assertSame( 'discord', WPDO_Discord_Notifier::channel_id() ); $this->assertSame( 'telegram', WPDO_Telegram_Notifier::channel_id() ); } }