diff --git a/rsscloud/rsscloud.php b/rsscloud/rsscloud.php index 56fd3ff..b812ff6 100644 --- a/rsscloud/rsscloud.php +++ b/rsscloud/rsscloud.php @@ -59,14 +59,16 @@ function rsscloud_parse_request( $wp ) { } } -function rsscloud_notify_result( $success, $msg ) { - $success = esc_attr( ent2ncr( wp_strip_all_tags( $success ) ) ); - $msg = esc_attr( ent2ncr( wp_strip_all_tags( $msg ) ) ); - - header( 'Content-Type: text/xml' ); - echo "\n"; - echo "\n"; - exit; +if ( ! function_exists( 'rsscloud_notify_result' ) ) { + function rsscloud_notify_result( $success, $msg ) { + $success = esc_attr( ent2ncr( wp_strip_all_tags( $success ) ) ); + $msg = esc_attr( ent2ncr( wp_strip_all_tags( $msg ) ) ); + + header( 'Content-Type: text/xml' ); + echo "\n"; + echo "\n"; + exit; + } } add_action( 'rss2_head', 'rsscloud_add_rss_cloud_element' ); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 92e296d..766ee63 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -25,6 +25,29 @@ // Give access to tests_add_filter() function. require_once "{$_tests_dir}/includes/functions.php"; +/** + * Exception used to capture rsscloud_notify_result() calls in tests + * instead of calling exit(). + */ +class RsscloudNotifyResultException extends Exception { + public $success; + public $msg; + + public function __construct( $success, $msg ) { + $this->success = $success; + $this->msg = $msg; + parent::__construct( "notify_result: success=$success msg=$msg" ); + } +} + +/** + * Test-friendly override of rsscloud_notify_result() that throws + * instead of calling exit. + */ +function rsscloud_notify_result( $success, $msg ) { + throw new RsscloudNotifyResultException( $success, $msg ); +} + /** * Manually load the plugin being tested. */ diff --git a/tests/test-notification-request.php b/tests/test-notification-request.php new file mode 100644 index 0000000..337cc09 --- /dev/null +++ b/tests/test-notification-request.php @@ -0,0 +1,379 @@ +http_requests = array(); + $this->feed_url = get_bloginfo( 'rss2_url' ); + $_POST = array(); + $_SERVER['REMOTE_ADDR'] = '192.168.1.100'; + rsscloud_update_hub_notifications( array() ); + } + + public function tear_down() { + $_POST = array(); + unset( $_SERVER['REMOTE_ADDR'] ); + remove_all_filters( 'pre_http_request' ); + parent::tear_down(); + } + + private function mock_http_response( $status_code = 200, $body = '' ) { + add_filter( + 'pre_http_request', + function ( $preempt, $args, $url ) use ( $status_code, $body ) { + $this->http_requests[] = array( + 'url' => $url, + 'args' => $args, + ); + return array( + 'response' => array( + 'code' => $status_code, + 'message' => 'OK', + ), + 'body' => $body, + ); + }, + 10, + 3 + ); + } + + private function mock_http_error() { + add_filter( + 'pre_http_request', + function ( $preempt, $args, $url ) { + $this->http_requests[] = array( + 'url' => $url, + 'args' => $args, + ); + return new WP_Error( 'http_request_failed', 'Connection refused' ); + }, + 10, + 3 + ); + } + + /** + * Helper to call the function and capture the notify result. + * + * @return RsscloudNotifyResultException + */ + private function call_process_notification_request() { + try { + rsscloud_hub_process_notification_request(); + $this->fail( 'Expected RsscloudNotifyResultException to be thrown' ); + } catch ( RsscloudNotifyResultException $e ) { + return $e; + } + } + + public function test_missing_url1_returns_error() { + $_POST = array(); + + $result = $this->call_process_notification_request(); + + $this->assertSame( 'false', $result->success ); + $this->assertSame( 'No feed for url1.', $result->msg ); + } + + public function test_unsupported_protocol_returns_error() { + $_POST = array( + 'url1' => $this->feed_url, + 'protocol' => 'xml-rpc', + 'port' => '80', + 'path' => '/rpc', + ); + + $fired = false; + add_action( + 'rsscloud_protocol_not_post', + function () use ( &$fired ) { + $fired = true; + } + ); + + $result = $this->call_process_notification_request(); + + $this->assertSame( 'false', $result->success ); + $this->assertStringContainsString( 'Only http-post', $result->msg ); + $this->assertTrue( $fired ); + } + + public function test_missing_path_returns_error() { + $_POST = array( + 'url1' => $this->feed_url, + 'protocol' => 'http-post', + 'port' => '80', + ); + + $result = $this->call_process_notification_request(); + + $this->assertSame( 'false', $result->success ); + $this->assertSame( 'No path provided.', $result->msg ); + } + + public function test_successful_ip_based_registration() { + $this->mock_http_response( 200 ); + + $_POST = array( + 'url1' => $this->feed_url, + 'protocol' => 'http-post', + 'port' => '80', + 'path' => '/rpc', + ); + + $result = $this->call_process_notification_request(); + + $this->assertSame( 'true', $result->success ); + $this->assertSame( 'Registration successful.', $result->msg ); + + // Verify the notification was stored. + $notify = rsscloud_get_hub_notifications(); + $this->assertArrayHasKey( $this->feed_url, $notify ); + $this->assertArrayHasKey( 'http://192.168.1.100:80/rpc', $notify[ $this->feed_url ] ); + + $sub = $notify[ $this->feed_url ]['http://192.168.1.100:80/rpc']; + $this->assertSame( 'http-post', $sub['protocol'] ); + $this->assertSame( 'active', $sub['status'] ); + $this->assertSame( 0, $sub['failure_count'] ); + } + + public function test_ip_based_sends_post_request() { + $this->mock_http_response( 200 ); + + $_POST = array( + 'url1' => $this->feed_url, + 'protocol' => 'http-post', + 'port' => '80', + 'path' => '/rpc', + ); + + $this->call_process_notification_request(); + + $this->assertCount( 1, $this->http_requests ); + $this->assertSame( 'POST', $this->http_requests[0]['args']['method'] ); + $this->assertSame( $this->feed_url, $this->http_requests[0]['args']['body']['url'] ); + } + + public function test_domain_based_sends_get_with_challenge() { + // Mock response that returns the challenge — we need to capture it. + add_filter( + 'pre_http_request', + function ( $preempt, $args, $url ) { + $this->http_requests[] = array( + 'url' => $url, + 'args' => $args, + ); + // Extract challenge from URL query string. + $query = wp_parse_url( $url, PHP_URL_QUERY ); + parse_str( $query, $params ); + return array( + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'body' => isset( $params['challenge'] ) ? $params['challenge'] : '', + ); + }, + 10, + 3 + ); + + $_POST = array( + 'url1' => $this->feed_url, + 'port' => '80', + 'path' => '/rpc', + 'domain' => 'subscriber.example.com', + ); + + $result = $this->call_process_notification_request(); + + $this->assertSame( 'true', $result->success ); + $this->assertCount( 1, $this->http_requests ); + $this->assertSame( 'GET', $this->http_requests[0]['args']['method'] ); + $this->assertStringContainsString( 'challenge=', $this->http_requests[0]['url'] ); + } + + public function test_domain_challenge_mismatch_returns_error() { + $this->mock_http_response( 200, 'wrong-challenge' ); + + $_POST = array( + 'url1' => $this->feed_url, + 'port' => '80', + 'path' => '/rpc', + 'domain' => 'subscriber.example.com', + ); + + $result = $this->call_process_notification_request(); + + $this->assertSame( 'false', $result->success ); + $this->assertStringContainsString( 'challenge', $result->msg ); + } + + public function test_http_request_failure_returns_error() { + $this->mock_http_error(); + + $_POST = array( + 'url1' => $this->feed_url, + 'protocol' => 'http-post', + 'port' => '80', + 'path' => '/rpc', + ); + + $result = $this->call_process_notification_request(); + + $this->assertSame( 'false', $result->success ); + $this->assertStringContainsString( 'Error testing notification URL', $result->msg ); + } + + public function test_http_status_error_returns_error() { + $this->mock_http_response( 500 ); + + $_POST = array( + 'url1' => $this->feed_url, + 'protocol' => 'http-post', + 'port' => '80', + 'path' => '/rpc', + ); + + $result = $this->call_process_notification_request(); + + $this->assertSame( 'false', $result->success ); + $this->assertStringContainsString( 'HTTP status code: 500', $result->msg ); + } + + public function test_wrong_feed_url_returns_error() { + $this->mock_http_response( 200 ); + + $_POST = array( + 'url1' => 'http://other-site.example.com/feed', + 'protocol' => 'http-post', + 'port' => '80', + 'path' => '/rpc', + ); + + $result = $this->call_process_notification_request(); + + $this->assertSame( 'false', $result->success ); + $this->assertStringContainsString( 'You can only request updates for', $result->msg ); + } + + public function test_default_port_is_80() { + $this->mock_http_response( 200 ); + + $_POST = array( + 'url1' => $this->feed_url, + 'protocol' => 'http-post', + 'path' => '/rpc', + ); + + $result = $this->call_process_notification_request(); + + $this->assertSame( 'true', $result->success ); + + $notify = rsscloud_get_hub_notifications(); + $this->assertArrayHasKey( 'http://192.168.1.100:80/rpc', $notify[ $this->feed_url ] ); + } + + public function test_path_gets_leading_slash_prepended() { + $this->mock_http_response( 200 ); + + $_POST = array( + 'url1' => $this->feed_url, + 'protocol' => 'http-post', + 'port' => '80', + 'path' => 'rpc', + ); + + $result = $this->call_process_notification_request(); + + $this->assertSame( 'true', $result->success ); + + $notify = rsscloud_get_hub_notifications(); + $this->assertArrayHasKey( 'http://192.168.1.100:80/rpc', $notify[ $this->feed_url ] ); + } + + public function test_fires_add_notify_subscription_action() { + $this->mock_http_response( 200 ); + $fired = false; + add_action( + 'rsscloud_add_notify_subscription', + function () use ( &$fired ) { + $fired = true; + } + ); + + $_POST = array( + 'url1' => $this->feed_url, + 'protocol' => 'http-post', + 'port' => '80', + 'path' => '/rpc', + ); + + $this->call_process_notification_request(); + + $this->assertTrue( $fired ); + } + + public function test_http_post_protocol_is_accepted() { + $this->mock_http_response( 200 ); + + $_POST = array( + 'url1' => $this->feed_url, + 'protocol' => 'HTTP-POST', + 'port' => '80', + 'path' => '/rpc', + ); + + $result = $this->call_process_notification_request(); + + $this->assertSame( 'true', $result->success ); + } + + public function test_domain_based_builds_correct_notify_url() { + // Mock that echoes back challenge. + add_filter( + 'pre_http_request', + function ( $preempt, $args, $url ) { + $this->http_requests[] = array( + 'url' => $url, + 'args' => $args, + ); + $query = wp_parse_url( $url, PHP_URL_QUERY ); + parse_str( $query, $params ); + return array( + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'body' => isset( $params['challenge'] ) ? $params['challenge'] : '', + ); + }, + 10, + 3 + ); + + $_POST = array( + 'url1' => $this->feed_url, + 'port' => '9000', + 'path' => '/notify', + 'domain' => 'callback.example.com', + ); + + $result = $this->call_process_notification_request(); + + $this->assertSame( 'true', $result->success ); + + $notify = rsscloud_get_hub_notifications(); + $this->assertArrayHasKey( 'http://callback.example.com:9000/notify', $notify[ $this->feed_url ] ); + } +}