From 828d15143e555a60e6a98b22166c57c2099fbe05 Mon Sep 17 00:00:00 2001 From: Danny Gershman Date: Sun, 18 Jan 2026 21:43:06 -0500 Subject: [PATCH] improgin code coverage --- includes/RssFeed.php | 100 ++- includes/Subscriber.php | 38 +- tests/Unit/Rest/SubscribersControllerTest.php | 526 +++++++++++++++ tests/Unit/RssFeedTest.php | 287 ++++++++ tests/Unit/SubscriberTest.php | 633 ++++++++++++++++++ tests/bootstrap.php | 4 + 6 files changed, 1541 insertions(+), 47 deletions(-) diff --git a/includes/RssFeed.php b/includes/RssFeed.php index 9364dbc..94ee456 100644 --- a/includes/RssFeed.php +++ b/includes/RssFeed.php @@ -14,7 +14,19 @@ public static function register_feed() { } - private static function get_rss_items_from_rest_api($eventType = '', $serviceBody = '', $sourceIds = '', $relation = 'AND', $categories = '', $tags = '', $per_page = 10) { + /** + * Get RSS items from the REST API + * + * @param string $eventType Event type filter + * @param string $serviceBody Service body filter + * @param string $sourceIds Source IDs filter + * @param string $relation Taxonomy relation (AND/OR) + * @param string $categories Category filter + * @param string $tags Tag filter + * @param int $per_page Number of items per page + * @return array Array of RSS item data + */ + public static function get_rss_items_from_rest_api($eventType = '', $serviceBody = '', $sourceIds = '', $relation = 'AND', $categories = '', $tags = '', $per_page = 10) { // Store original $_GET to restore later $original_get = $_GET; @@ -187,6 +199,55 @@ private static function get_rss_items_from_rest_api($eventType = '', $serviceBod } + /** + * Build the RSS XML string from events data + * + * This method is separated from generate_rss_feed() to allow for unit testing. + * + * @param array $events Array of event data from get_rss_items_from_rest_api() + * @param string $site_name Site name for feed title + * @param string $site_url Site URL for feed link + * @param string $feed_description Description for the feed + * @param string $language Language code + * @param string|null $build_date Optional build date (defaults to current time) + * @return string Complete RSS XML document + */ + public static function build_rss_xml($events, $site_name, $site_url, $feed_description, $language = 'en_US', $build_date = null) { + $xml = '' . "\n"; + $xml .= '' . "\n"; + $xml .= '' . "\n"; + $xml .= '' . self::escape_xml_text($site_name . ' - Events') . '' . "\n"; + $xml .= '' . self::escape_xml_text($site_url) . '' . "\n"; + $xml .= '' . self::escape_xml_text($feed_description) . '' . "\n"; + $xml .= '' . ($build_date ?? date('D, d M Y H:i:s O')) . '' . "\n"; + $xml .= '' . $language . '' . "\n"; + $xml .= 'Mayo Events Manager' . "\n"; + + foreach ($events as $event) { + $xml .= '' . "\n"; + $xml .= '' . self::escape_xml_text($event['title']) . '' . "\n"; + $xml .= '' . self::escape_xml_text($event['link']) . '' . "\n"; + $xml .= '' . self::escape_xml_text($event['link']) . '' . "\n"; + $xml .= '' . $event['pub_date'] . '' . "\n"; + $xml .= '' . "\n"; + $xml .= '' . "\n"; + + // Add categories if available + if (!empty($event['categories'])) { + foreach ($event['categories'] as $category) { + $xml .= '' . self::escape_xml_text($category) . '' . "\n"; + } + } + + $xml .= '' . "\n"; + } + + $xml .= '' . "\n"; + $xml .= '' . "\n"; + + return $xml; + } + public static function generate_rss_feed() { // Get parameters from URL (for manual overrides) or detect from page shortcode $eventType = isset($_GET['event_type']) ? \sanitize_text_field($_GET['event_type']) : ''; @@ -196,7 +257,7 @@ public static function generate_rss_feed() { $categories = isset($_GET['categories']) ? \sanitize_text_field($_GET['categories']) : ''; $tags = isset($_GET['tags']) ? \sanitize_text_field($_GET['tags']) : ''; $per_page = isset($_GET['per_page']) ? \sanitize_text_field($_GET['per_page']) : ''; - + // If no URL parameters provided, try to get them from the current page's shortcode if (empty($eventType) && empty($serviceBody) && empty($sourceIds) && empty($categories) && empty($tags) && empty($per_page)) { $shortcode_params = self::get_shortcode_params_from_current_page(); @@ -221,42 +282,11 @@ public static function generate_rss_feed() { } $site_name = \get_bloginfo('name'); $site_url = \home_url(); - $site_description = \get_bloginfo('description'); - + // Build descriptive feed description with active filters $feed_description = self::build_feed_description($site_name, $eventType, $serviceBody, $sourceIds, $relation, $categories, $tags, $per_page); - echo '' . "\n"; - echo '' . "\n"; - echo '' . "\n"; - echo '' . self::escape_xml_text($site_name . ' - Events') . '' . "\n"; - echo '' . self::escape_xml_text($site_url) . '' . "\n"; - echo '' . self::escape_xml_text($feed_description) . '' . "\n"; - echo '' . date('D, d M Y H:i:s O') . '' . "\n"; - echo '' . \get_locale() . '' . "\n"; - echo 'Mayo Events Manager' . "\n"; - - foreach ($events as $event) { - echo '' . "\n"; - echo '' . self::escape_xml_text($event['title']) . '' . "\n"; - echo '' . self::escape_xml_text($event['link']) . '' . "\n"; - echo '' . self::escape_xml_text($event['link']) . '' . "\n"; - echo '' . $event['pub_date'] . '' . "\n"; - echo '' . "\n"; - echo '' . "\n"; - - // Add categories if available - if (!empty($event['categories'])) { - foreach ($event['categories'] as $category) { - echo '' . self::escape_xml_text($category) . '' . "\n"; - } - } - - echo '' . "\n"; - } - - echo '' . "\n"; - echo '' . "\n"; + echo self::build_rss_xml($events, $site_name, $site_url, $feed_description, \get_locale()); exit; } diff --git a/includes/Subscriber.php b/includes/Subscriber.php index 13c31a4..08abe51 100644 --- a/includes/Subscriber.php +++ b/includes/Subscriber.php @@ -14,12 +14,26 @@ class Subscriber */ const TABLE_NAME = 'mayo_subscribers'; + /** + * Get the wpdb instance + * + * This method is separated to allow for mocking in unit tests. + * Tests can extend this class and override this method to return a mock. + * + * @return \wpdb The WordPress database object + */ + protected static function get_wpdb() + { + global $wpdb; + return $wpdb; + } + /** * Get the full table name with prefix */ public static function get_table_name() { - global $wpdb; + $wpdb = static::get_wpdb(); return $wpdb->prefix . self::TABLE_NAME; } @@ -29,7 +43,7 @@ public static function get_table_name() */ public static function create_table() { - global $wpdb; + $wpdb = static::get_wpdb(); $table_name = self::get_table_name(); $charset_collate = $wpdb->get_charset_collate(); @@ -69,7 +83,7 @@ public static function generate_token() */ public static function subscribe($email, $preferences = null) { - global $wpdb; + $wpdb = static::get_wpdb(); $email = sanitize_email($email); @@ -189,7 +203,7 @@ public static function subscribe($email, $preferences = null) */ public static function get_by_token($token) { - global $wpdb; + $wpdb = static::get_wpdb(); $token = sanitize_text_field($token); $table_name = self::get_table_name(); @@ -209,7 +223,7 @@ public static function get_by_token($token) */ public static function update_preferences($token, $preferences) { - global $wpdb; + $wpdb = static::get_wpdb(); $token = sanitize_text_field($token); $table_name = self::get_table_name(); @@ -233,7 +247,7 @@ public static function update_preferences($token, $preferences) */ public static function confirm($token) { - global $wpdb; + $wpdb = static::get_wpdb(); $token = sanitize_text_field($token); $table_name = self::get_table_name(); @@ -288,7 +302,7 @@ public static function confirm($token) */ public static function unsubscribe($token) { - global $wpdb; + $wpdb = static::get_wpdb(); $token = sanitize_text_field($token); $table_name = self::get_table_name(); @@ -336,7 +350,7 @@ public static function unsubscribe($token) */ public static function get_active_subscribers() { - global $wpdb; + $wpdb = static::get_wpdb(); $table_name = self::get_table_name(); @@ -352,7 +366,7 @@ public static function get_active_subscribers() */ public static function get_all_subscribers() { - global $wpdb; + $wpdb = static::get_wpdb(); $table_name = self::get_table_name(); @@ -370,7 +384,7 @@ public static function get_all_subscribers() */ public static function update_status($id, $status) { - global $wpdb; + $wpdb = static::get_wpdb(); $id = intval($id); $status = sanitize_text_field($status); @@ -405,7 +419,7 @@ public static function update_status($id, $status) */ public static function update_preferences_by_id($id, $preferences) { - global $wpdb; + $wpdb = static::get_wpdb(); $id = intval($id); $table_name = self::get_table_name(); @@ -429,7 +443,7 @@ public static function update_preferences_by_id($id, $preferences) */ public static function delete($id) { - global $wpdb; + $wpdb = static::get_wpdb(); $id = intval($id); $table_name = self::get_table_name(); diff --git a/tests/Unit/Rest/SubscribersControllerTest.php b/tests/Unit/Rest/SubscribersControllerTest.php index 62afb82..38e232c 100644 --- a/tests/Unit/Rest/SubscribersControllerTest.php +++ b/tests/Unit/Rest/SubscribersControllerTest.php @@ -326,4 +326,530 @@ public function testGetSubscriptionOptionsFormatsTagsCorrectly(): void { $this->assertEquals('Tag One', $data['tags'][0]['name']); $this->assertEquals('tag-one', $data['tags'][0]['slug']); } + + /** + * Test get_subscriber returns 404 for non-existent token + */ + public function testGetSubscriberReturnsNotFoundForInvalidToken(): void { + // Mock the Subscriber::get_by_token to return null via global $wpdb mock + global $wpdb; + $wpdb = \Mockery::mock('wpdb'); + $wpdb->prefix = 'wp_'; + $wpdb->shouldReceive('prepare')->andReturn('SQL'); + $wpdb->shouldReceive('get_row')->andReturn(null); + + $request = new \WP_REST_Request('GET', '/event-manager/v1/subscriber/invalidtoken123'); + $request->set_param('token', 'invalidtoken123'); + + $response = SubscribersController::get_subscriber($request); + + $this->assertEquals(404, $response->get_status()); + $data = $response->get_data(); + $this->assertFalse($data['success']); + $this->assertEquals('not_found', $data['code']); + } + + /** + * Test get_subscriber returns subscriber data for valid token + */ + public function testGetSubscriberReturnsDataForValidToken(): void { + global $wpdb; + $wpdb = \Mockery::mock('wpdb'); + $wpdb->prefix = 'wp_'; + + $subscriber = (object)[ + 'id' => 1, + 'email' => 'test@example.com', + 'status' => 'active', + 'preferences' => json_encode(['categories' => [1, 2]]), + 'token' => 'validtoken123' + ]; + + $wpdb->shouldReceive('prepare')->andReturn('SQL'); + $wpdb->shouldReceive('get_row')->andReturn($subscriber); + + $request = new \WP_REST_Request('GET', '/event-manager/v1/subscriber/validtoken123'); + $request->set_param('token', 'validtoken123'); + + $response = SubscribersController::get_subscriber($request); + + $this->assertEquals(200, $response->get_status()); + $data = $response->get_data(); + $this->assertEquals('test@example.com', $data['email']); + $this->assertEquals('active', $data['status']); + $this->assertIsArray($data['preferences']); + } + + /** + * Test get_subscriber handles subscriber without preferences + */ + public function testGetSubscriberHandlesNoPreferences(): void { + global $wpdb; + $wpdb = \Mockery::mock('wpdb'); + $wpdb->prefix = 'wp_'; + + $subscriber = (object)[ + 'id' => 1, + 'email' => 'legacy@example.com', + 'status' => 'active', + 'preferences' => null, + 'token' => 'validtoken123' + ]; + + $wpdb->shouldReceive('prepare')->andReturn('SQL'); + $wpdb->shouldReceive('get_row')->andReturn($subscriber); + + $request = new \WP_REST_Request('GET', '/event-manager/v1/subscriber/validtoken123'); + $request->set_param('token', 'validtoken123'); + + $response = SubscribersController::get_subscriber($request); + + $this->assertEquals(200, $response->get_status()); + $data = $response->get_data(); + $this->assertNull($data['preferences']); + } + + /** + * Test update_subscriber returns 404 for non-existent token + */ + public function testUpdateSubscriberReturnsNotFoundForInvalidToken(): void { + global $wpdb; + $wpdb = \Mockery::mock('wpdb'); + $wpdb->prefix = 'wp_'; + $wpdb->shouldReceive('prepare')->andReturn('SQL'); + $wpdb->shouldReceive('get_row')->andReturn(null); + + $request = new \WP_REST_Request('PUT', '/event-manager/v1/subscriber/invalidtoken123'); + $request->set_param('token', 'invalidtoken123'); + $request->set_body_params(['preferences' => ['categories' => [1]]]); + + $response = SubscribersController::update_subscriber($request); + + $this->assertEquals(404, $response->get_status()); + $data = $response->get_data(); + $this->assertEquals('not_found', $data['code']); + } + + /** + * Test update_subscriber returns error for non-active subscriber + */ + public function testUpdateSubscriberReturnsErrorForNonActive(): void { + global $wpdb; + $wpdb = \Mockery::mock('wpdb'); + $wpdb->prefix = 'wp_'; + + $subscriber = (object)[ + 'id' => 1, + 'email' => 'test@example.com', + 'status' => 'pending', + 'token' => 'pendingtoken123' + ]; + + $wpdb->shouldReceive('prepare')->andReturn('SQL'); + $wpdb->shouldReceive('get_row')->andReturn($subscriber); + + $request = new \WP_REST_Request('PUT', '/event-manager/v1/subscriber/pendingtoken123'); + $request->set_param('token', 'pendingtoken123'); + $request->set_body_params(['preferences' => ['categories' => [1]]]); + + $response = SubscribersController::update_subscriber($request); + + $this->assertEquals(400, $response->get_status()); + $data = $response->get_data(); + $this->assertEquals('not_active', $data['code']); + } + + /** + * Test update_subscriber returns error when preferences missing + */ + public function testUpdateSubscriberReturnsErrorWhenPreferencesMissing(): void { + global $wpdb; + $wpdb = \Mockery::mock('wpdb'); + $wpdb->prefix = 'wp_'; + + $subscriber = (object)[ + 'id' => 1, + 'email' => 'test@example.com', + 'status' => 'active', + 'token' => 'activetoken123' + ]; + + $wpdb->shouldReceive('prepare')->andReturn('SQL'); + $wpdb->shouldReceive('get_row')->andReturn($subscriber); + + $request = new \WP_REST_Request('PUT', '/event-manager/v1/subscriber/activetoken123'); + $request->set_param('token', 'activetoken123'); + // No preferences in body params + + $response = SubscribersController::update_subscriber($request); + + $this->assertEquals(400, $response->get_status()); + $data = $response->get_data(); + $this->assertEquals('invalid_preferences', $data['code']); + } + + /** + * Test update_subscriber returns error when preferences empty + */ + public function testUpdateSubscriberReturnsErrorWhenPreferencesEmpty(): void { + global $wpdb; + $wpdb = \Mockery::mock('wpdb'); + $wpdb->prefix = 'wp_'; + + $subscriber = (object)[ + 'id' => 1, + 'email' => 'test@example.com', + 'status' => 'active', + 'token' => 'activetoken123' + ]; + + $wpdb->shouldReceive('prepare')->andReturn('SQL'); + $wpdb->shouldReceive('get_row')->andReturn($subscriber); + + $request = new \WP_REST_Request('PUT', '/event-manager/v1/subscriber/activetoken123'); + $request->set_param('token', 'activetoken123'); + $request->set_body_params(['preferences' => ['categories' => [], 'tags' => [], 'service_bodies' => []]]); + + $response = SubscribersController::update_subscriber($request); + + $this->assertEquals(400, $response->get_status()); + $data = $response->get_data(); + $this->assertEquals('no_preferences', $data['code']); + } + + /** + * Test update_subscriber success + */ + public function testUpdateSubscriberSuccess(): void { + global $wpdb; + $wpdb = \Mockery::mock('wpdb'); + $wpdb->prefix = 'wp_'; + + $subscriber = (object)[ + 'id' => 1, + 'email' => 'test@example.com', + 'status' => 'active', + 'token' => 'activetoken123' + ]; + + $wpdb->shouldReceive('prepare')->andReturn('SQL'); + $wpdb->shouldReceive('get_row')->andReturn($subscriber); + $wpdb->shouldReceive('update')->andReturn(1); + + $request = new \WP_REST_Request('PUT', '/event-manager/v1/subscriber/activetoken123'); + $request->set_param('token', 'activetoken123'); + $request->set_body_params(['preferences' => ['categories' => [1, 2], 'tags' => [], 'service_bodies' => []]]); + + $response = SubscribersController::update_subscriber($request); + + $this->assertEquals(200, $response->get_status()); + $data = $response->get_data(); + $this->assertTrue($data['success']); + } + + /** + * Test update_subscriber handles update failure + */ + public function testUpdateSubscriberHandlesUpdateFailure(): void { + global $wpdb; + $wpdb = \Mockery::mock('wpdb'); + $wpdb->prefix = 'wp_'; + + $subscriber = (object)[ + 'id' => 1, + 'email' => 'test@example.com', + 'status' => 'active', + 'token' => 'activetoken123' + ]; + + $wpdb->shouldReceive('prepare')->andReturn('SQL'); + $wpdb->shouldReceive('get_row')->andReturn($subscriber); + $wpdb->shouldReceive('update')->andReturn(false); + + $request = new \WP_REST_Request('PUT', '/event-manager/v1/subscriber/activetoken123'); + $request->set_param('token', 'activetoken123'); + $request->set_body_params(['preferences' => ['categories' => [1], 'tags' => [], 'service_bodies' => []]]); + + $response = SubscribersController::update_subscriber($request); + + $this->assertEquals(500, $response->get_status()); + $data = $response->get_data(); + $this->assertEquals('update_failed', $data['code']); + } + + /** + * Test get_all_subscribers returns formatted subscribers + */ + public function testGetAllSubscribersReturnsFormattedSubscribers(): void { + $this->loginAsAdmin(); + + global $wpdb; + $wpdb = \Mockery::mock('wpdb'); + $wpdb->prefix = 'wp_'; + + $subscribers = [ + (object)[ + 'id' => 1, + 'email' => 'a@example.com', + 'status' => 'active', + 'created_at' => '2024-01-01 12:00:00', + 'confirmed_at' => '2024-01-01 12:05:00', + 'preferences' => json_encode(['categories' => [1], 'tags' => [], 'service_bodies' => []]) + ], + (object)[ + 'id' => 2, + 'email' => 'b@example.com', + 'status' => 'pending', + 'created_at' => '2024-01-02 10:00:00', + 'confirmed_at' => null, + 'preferences' => null + ] + ]; + + $wpdb->shouldReceive('get_results')->andReturn($subscribers); + + Functions\when('get_terms')->alias(function($args) { + if ($args['taxonomy'] === 'category') { + return [(object)['term_id' => 1, 'name' => 'News']]; + } + return []; + }); + + $this->mockWpRemoteGet([ + 'GetServiceBodies' => ['code' => 200, 'body' => []] + ]); + + $request = new \WP_REST_Request('GET', '/event-manager/v1/subscribers'); + $response = SubscribersController::get_all_subscribers($request); + + $this->assertEquals(200, $response->get_status()); + $data = $response->get_data(); + $this->assertCount(2, $data); + $this->assertEquals('a@example.com', $data[0]['email']); + $this->assertArrayHasKey('preferences_display', $data[0]); + } + + /** + * Test count_matching_subscribers returns count and list + */ + public function testCountMatchingSubscribersReturnsCountAndList(): void { + $this->loginAsEditor(); + + global $wpdb; + $wpdb = \Mockery::mock('wpdb'); + $wpdb->prefix = 'wp_'; + + $subscribers = [ + (object)[ + 'id' => 1, + 'email' => 'match@example.com', + 'status' => 'active', + 'preferences' => null + ] + ]; + + $wpdb->shouldReceive('get_results')->andReturn($subscribers); + + $this->mockWpRemoteGet([ + 'GetServiceBodies' => ['code' => 200, 'body' => []] + ]); + + $request = new \WP_REST_Request('POST', '/event-manager/v1/subscribers/count'); + $request->set_header('Content-Type', 'application/json'); + + // Simulate JSON body + $reflectionClass = new \ReflectionClass($request); + $property = $reflectionClass->getProperty('body_params'); + $property->setAccessible(true); + $property->setValue($request, ['categories' => [1], 'tags' => [], 'service_body' => null]); + + // Need to mock get_json_params + $response = SubscribersController::count_matching_subscribers($request); + + $this->assertEquals(200, $response->get_status()); + $data = $response->get_data(); + $this->assertArrayHasKey('count', $data); + $this->assertArrayHasKey('subscribers', $data); + } + + /** + * Test admin_update_subscriber updates status + */ + public function testAdminUpdateSubscriberUpdatesStatus(): void { + $this->loginAsAdmin(); + + global $wpdb; + $wpdb = \Mockery::mock('wpdb'); + $wpdb->prefix = 'wp_'; + $wpdb->shouldReceive('update')->andReturn(1); + + $request = new \WP_REST_Request('PUT', '/event-manager/v1/subscribers/1'); + $request->set_param('id', 1); + + $reflectionClass = new \ReflectionClass($request); + $property = $reflectionClass->getProperty('body_params'); + $property->setAccessible(true); + $property->setValue($request, ['status' => 'active']); + + $response = SubscribersController::admin_update_subscriber($request); + + $this->assertEquals(200, $response->get_status()); + $data = $response->get_data(); + $this->assertTrue($data['success']); + } + + /** + * Test admin_update_subscriber updates preferences + */ + public function testAdminUpdateSubscriberUpdatesPreferences(): void { + $this->loginAsAdmin(); + + global $wpdb; + $wpdb = \Mockery::mock('wpdb'); + $wpdb->prefix = 'wp_'; + $wpdb->shouldReceive('update')->andReturn(1); + + $request = new \WP_REST_Request('PUT', '/event-manager/v1/subscribers/1'); + $request->set_param('id', 1); + + $reflectionClass = new \ReflectionClass($request); + $property = $reflectionClass->getProperty('body_params'); + $property->setAccessible(true); + $property->setValue($request, ['preferences' => ['categories' => [1, 2]]]); + + $response = SubscribersController::admin_update_subscriber($request); + + $this->assertEquals(200, $response->get_status()); + $data = $response->get_data(); + $this->assertTrue($data['success']); + } + + /** + * Test admin_update_subscriber ignores invalid status + */ + public function testAdminUpdateSubscriberIgnoresInvalidStatus(): void { + $this->loginAsAdmin(); + + global $wpdb; + $wpdb = \Mockery::mock('wpdb'); + $wpdb->prefix = 'wp_'; + + $request = new \WP_REST_Request('PUT', '/event-manager/v1/subscribers/1'); + $request->set_param('id', 1); + + $reflectionClass = new \ReflectionClass($request); + $property = $reflectionClass->getProperty('body_params'); + $property->setAccessible(true); + $property->setValue($request, ['status' => 'invalid_status']); + + $response = SubscribersController::admin_update_subscriber($request); + + $this->assertEquals(200, $response->get_status()); + $data = $response->get_data(); + // Update wasn't actually called because status is invalid + $this->assertFalse($data['updated']); + } + + /** + * Test admin_delete_subscriber deletes subscriber + */ + public function testAdminDeleteSubscriberDeletesSubscriber(): void { + $this->loginAsAdmin(); + + global $wpdb; + $wpdb = \Mockery::mock('wpdb'); + $wpdb->prefix = 'wp_'; + $wpdb->shouldReceive('delete')->andReturn(1); + + $request = new \WP_REST_Request('DELETE', '/event-manager/v1/subscribers/1'); + $request->set_param('id', 1); + + $response = SubscribersController::admin_delete_subscriber($request); + + $this->assertEquals(200, $response->get_status()); + $data = $response->get_data(); + $this->assertTrue($data['success']); + } + + /** + * Test admin_delete_subscriber returns 404 when subscriber not found + */ + public function testAdminDeleteSubscriberReturns404WhenNotFound(): void { + $this->loginAsAdmin(); + + global $wpdb; + $wpdb = \Mockery::mock('wpdb'); + $wpdb->prefix = 'wp_'; + $wpdb->shouldReceive('delete')->andReturn(false); + + $request = new \WP_REST_Request('DELETE', '/event-manager/v1/subscribers/999'); + $request->set_param('id', 999); + + $response = SubscribersController::admin_delete_subscriber($request); + + $this->assertEquals(404, $response->get_status()); + $data = $response->get_data(); + $this->assertFalse($data['success']); + } + + /** + * Test get_subscription_options with service bodies from BMLT + */ + public function testGetSubscriptionOptionsWithServiceBodies(): void { + $this->mockWpRemoteGet([ + 'GetServiceBodies' => [ + 'code' => 200, + 'body' => [ + ['id' => '10', 'name' => 'Region A', 'url' => 'https://region-a.example.com'], + ['id' => '20', 'name' => 'Region B', 'url' => 'https://region-b.example.com'], + ['id' => '30', 'name' => 'Region C', 'url' => 'https://region-c.example.com'] + ] + ] + ]); + + Functions\when('get_terms')->justReturn([]); + + $request = new \WP_REST_Request('GET', '/event-manager/v1/subscription-options'); + $response = SubscribersController::get_subscription_options($request); + + $this->assertEquals(200, $response->get_status()); + $data = $response->get_data(); + $this->assertArrayHasKey('service_bodies', $data); + $this->assertCount(2, $data['service_bodies']); + } + + /** + * Test subscribe with valid preferences calls Subscriber::subscribe + */ + public function testSubscribeWithValidPreferencesCallsSubscriber(): void { + global $wpdb; + $wpdb = \Mockery::mock('wpdb'); + $wpdb->prefix = 'wp_'; + $wpdb->shouldReceive('prepare')->andReturn('SQL'); + $wpdb->shouldReceive('get_row')->andReturn(null); + $wpdb->shouldReceive('insert')->andReturn(1); + + Functions\when('get_bloginfo')->alias(function($show) { + if ($show === 'name') return 'Test Site'; + return ''; + }); + + // Mock wp_mail for email sending + $this->mockWpMail(); + + $request = $this->createRestRequest('POST', '/event-manager/v1/subscribe', [ + 'email' => 'newuser@example.com', + 'preferences' => [ + 'categories' => [1], + 'tags' => [], + 'service_bodies' => [] + ] + ]); + + $response = SubscribersController::subscribe($request); + + $this->assertEquals(200, $response->get_status()); + $data = $response->get_data(); + $this->assertTrue($data['success']); + } } diff --git a/tests/Unit/RssFeedTest.php b/tests/Unit/RssFeedTest.php index b29cd6e..0fec7ed 100644 --- a/tests/Unit/RssFeedTest.php +++ b/tests/Unit/RssFeedTest.php @@ -380,6 +380,293 @@ public function testGetShortcodeParamsWithMultipleShortcodes(): void { $this->assertIsArray($result); } + /** + * Test build_rss_xml returns valid XML structure + */ + public function testBuildRssXmlReturnsValidXmlStructure(): void { + $events = []; + $xml = RssFeed::build_rss_xml( + $events, + 'Test Site', + 'https://example.com', + 'Test description', + 'en_US', + 'Mon, 01 Jan 2024 12:00:00 +0000' + ); + + $this->assertStringContainsString('', $xml); + $this->assertStringContainsString('assertStringContainsString('', $xml); + $this->assertStringContainsString('', $xml); + $this->assertStringContainsString('', $xml); + } + + /** + * Test build_rss_xml includes channel elements + */ + public function testBuildRssXmlIncludesChannelElements(): void { + $events = []; + $xml = RssFeed::build_rss_xml( + $events, + 'My Test Site', + 'https://mysite.example.com', + 'Events from our site', + 'fr_FR', + 'Tue, 15 Jan 2024 10:30:00 +0000' + ); + + $this->assertStringContainsString('My Test Site - Events', $xml); + $this->assertStringContainsString('https://mysite.example.com', $xml); + $this->assertStringContainsString('Events from our site', $xml); + $this->assertStringContainsString('Tue, 15 Jan 2024 10:30:00 +0000', $xml); + $this->assertStringContainsString('fr_FR', $xml); + $this->assertStringContainsString('Mayo Events Manager', $xml); + } + + /** + * Test build_rss_xml formats events correctly + */ + public function testBuildRssXmlFormatsEventsCorrectly(): void { + $events = [ + [ + 'title' => 'Test Event', + 'link' => 'https://example.com/event/1', + 'pub_date' => 'Wed, 20 Jan 2024 14:00:00 +0000', + 'description' => 'Event on January 20', + 'content' => '

Full event content here

', + 'categories' => [] + ] + ]; + + $xml = RssFeed::build_rss_xml( + $events, + 'Test Site', + 'https://example.com', + 'Test description', + 'en_US', + 'Mon, 01 Jan 2024 12:00:00 +0000' + ); + + $this->assertStringContainsString('', $xml); + $this->assertStringContainsString('Test Event', $xml); + $this->assertStringContainsString('https://example.com/event/1', $xml); + $this->assertStringContainsString('https://example.com/event/1', $xml); + $this->assertStringContainsString('Wed, 20 Jan 2024 14:00:00 +0000', $xml); + $this->assertStringContainsString('', $xml); + $this->assertStringContainsString('Full event content here

]]>
', $xml); + $this->assertStringContainsString('
', $xml); + } + + /** + * Test build_rss_xml includes multiple items + */ + public function testBuildRssXmlIncludesMultipleItems(): void { + $events = [ + [ + 'title' => 'First Event', + 'link' => 'https://example.com/event/1', + 'pub_date' => 'Wed, 20 Jan 2024 14:00:00 +0000', + 'description' => 'First event', + 'content' => 'Content 1', + 'categories' => [] + ], + [ + 'title' => 'Second Event', + 'link' => 'https://example.com/event/2', + 'pub_date' => 'Thu, 21 Jan 2024 14:00:00 +0000', + 'description' => 'Second event', + 'content' => 'Content 2', + 'categories' => [] + ], + [ + 'title' => 'Third Event', + 'link' => 'https://example.com/event/3', + 'pub_date' => 'Fri, 22 Jan 2024 14:00:00 +0000', + 'description' => 'Third event', + 'content' => 'Content 3', + 'categories' => [] + ] + ]; + + $xml = RssFeed::build_rss_xml( + $events, + 'Test Site', + 'https://example.com', + 'Test description', + 'en_US' + ); + + $this->assertStringContainsString('First Event', $xml); + $this->assertStringContainsString('Second Event', $xml); + $this->assertStringContainsString('Third Event', $xml); + $this->assertEquals(3, substr_count($xml, '')); + $this->assertEquals(3, substr_count($xml, '')); + } + + /** + * Test build_rss_xml includes categories + */ + public function testBuildRssXmlIncludesCategories(): void { + $events = [ + [ + 'title' => 'Categorized Event', + 'link' => 'https://example.com/event/1', + 'pub_date' => 'Wed, 20 Jan 2024 14:00:00 +0000', + 'description' => 'Event with categories', + 'content' => 'Content', + 'categories' => ['News', 'Events', 'Featured'] + ] + ]; + + $xml = RssFeed::build_rss_xml( + $events, + 'Test Site', + 'https://example.com', + 'Test description', + 'en_US' + ); + + $this->assertStringContainsString('News', $xml); + $this->assertStringContainsString('Events', $xml); + $this->assertStringContainsString('Featured', $xml); + } + + /** + * Test build_rss_xml escapes XML special characters in title + */ + public function testBuildRssXmlEscapesSpecialCharactersInTitle(): void { + $events = [ + [ + 'title' => 'Event & "Special" ', + 'link' => 'https://example.com/event/1', + 'pub_date' => 'Wed, 20 Jan 2024 14:00:00 +0000', + 'description' => 'Description', + 'content' => 'Content', + 'categories' => [] + ] + ]; + + $xml = RssFeed::build_rss_xml( + $events, + 'Site & "Name"', + 'https://example.com', + 'Description with ', + 'en_US' + ); + + $this->assertStringContainsString('&', $xml); + $this->assertStringContainsString('"', $xml); + $this->assertStringContainsString('<', $xml); + $this->assertStringContainsString('>', $xml); + } + + /** + * Test build_rss_xml handles empty events array + */ + public function testBuildRssXmlHandlesEmptyEventsArray(): void { + $xml = RssFeed::build_rss_xml( + [], + 'Test Site', + 'https://example.com', + 'Test description', + 'en_US' + ); + + $this->assertStringContainsString('', $xml); + $this->assertStringContainsString('', $xml); + $this->assertStringNotContainsString('', $xml); + } + + /** + * Test build_rss_xml uses current date when build_date is null + */ + public function testBuildRssXmlUsesCurrentDateWhenBuildDateIsNull(): void { + $xml = RssFeed::build_rss_xml( + [], + 'Test Site', + 'https://example.com', + 'Test description', + 'en_US', + null + ); + + // Should contain a date in RFC 2822 format + $this->assertMatchesRegularExpression('/[A-Za-z]{3}, \d{2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2} [+-]\d{4}<\/lastBuildDate>/', $xml); + } + + /** + * Test build_rss_xml is well-formed XML + */ + public function testBuildRssXmlIsWellFormedXml(): void { + $events = [ + [ + 'title' => 'Test Event', + 'link' => 'https://example.com/event/1', + 'pub_date' => 'Wed, 20 Jan 2024 14:00:00 +0000', + 'description' => 'Event description', + 'content' => '

Content

', + 'categories' => ['Category1'] + ] + ]; + + $xml = RssFeed::build_rss_xml( + $events, + 'Test Site', + 'https://example.com', + 'Test description', + 'en_US', + 'Mon, 01 Jan 2024 12:00:00 +0000' + ); + + // Parse XML to verify it's well-formed + $doc = new \DOMDocument(); + $result = @$doc->loadXML($xml); + $this->assertTrue($result, 'XML should be well-formed and parseable'); + } + + /** + * Test build_rss_xml RSS namespace for content:encoded + */ + public function testBuildRssXmlHasContentNamespace(): void { + $xml = RssFeed::build_rss_xml( + [], + 'Test Site', + 'https://example.com', + 'Test description', + 'en_US' + ); + + $this->assertStringContainsString('xmlns:content="http://purl.org/rss/1.0/modules/content/"', $xml); + $this->assertStringContainsString('xmlns:dc="http://purl.org/dc/elements/1.1/"', $xml); + } + + /** + * Test build_rss_xml handles event with empty categories + */ + public function testBuildRssXmlHandlesEventWithEmptyCategories(): void { + $events = [ + [ + 'title' => 'Event Without Categories', + 'link' => 'https://example.com/event/1', + 'pub_date' => 'Wed, 20 Jan 2024 14:00:00 +0000', + 'description' => 'Description', + 'content' => 'Content', + 'categories' => [] + ] + ]; + + $xml = RssFeed::build_rss_xml( + $events, + 'Test Site', + 'https://example.com', + 'Test description', + 'en_US' + ); + + $this->assertStringContainsString('', $xml); + $this->assertStringNotContainsString('', $xml); + } + /** * Helper to get private method */ diff --git a/tests/Unit/SubscriberTest.php b/tests/Unit/SubscriberTest.php index 34cf730..561ba81 100644 --- a/tests/Unit/SubscriberTest.php +++ b/tests/Unit/SubscriberTest.php @@ -5,6 +5,25 @@ use BmltEnabled\Mayo\Subscriber; use Brain\Monkey\Functions; use ReflectionClass; +use Mockery; + +/** + * Testable Subscriber class that allows mocking $wpdb + */ +class TestableSubscriber extends Subscriber { + public static $mockWpdb = null; + + protected static function get_wpdb() { + if (self::$mockWpdb !== null) { + return self::$mockWpdb; + } + return parent::get_wpdb(); + } + + public static function resetMock() { + self::$mockWpdb = null; + } +} class SubscriberTest extends TestCase { @@ -33,6 +52,29 @@ protected function setUp(): void { // Mock get_term Functions\when('get_term')->justReturn(null); + + // Mock get_bloginfo for email methods + Functions\when('get_bloginfo')->alias(function($show) { + if ($show === 'name') return 'Test Site'; + return ''; + }); + + // Reset testable subscriber mock + TestableSubscriber::resetMock(); + } + + protected function tearDown(): void { + TestableSubscriber::resetMock(); + parent::tearDown(); + } + + /** + * Create a mock wpdb object for testing + */ + private function createMockWpdb() { + $mock = Mockery::mock('wpdb'); + $mock->prefix = 'wp_'; + return $mock; } /** @@ -392,6 +434,597 @@ public function testTableNameConstant(): void { $this->assertEquals('mayo_subscribers', Subscriber::TABLE_NAME); } + /** + * Test get_table_name returns prefixed table name + */ + public function testGetTableNameReturnsPrefixedTableName(): void { + $mockWpdb = $this->createMockWpdb(); + $mockWpdb->prefix = 'test_prefix_'; + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::get_table_name(); + + $this->assertEquals('test_prefix_mayo_subscribers', $result); + } + + /** + * Test subscribe returns error for invalid email + */ + public function testSubscribeReturnsErrorForInvalidEmail(): void { + $result = TestableSubscriber::subscribe('not-an-email'); + + $this->assertIsArray($result); + $this->assertFalse($result['success']); + $this->assertEquals('invalid_email', $result['code']); + } + + /** + * Test subscribe creates new subscriber + */ + public function testSubscribeCreatesNewSubscriber(): void { + $mockWpdb = $this->createMockWpdb(); + + // No existing subscriber + $mockWpdb->shouldReceive('prepare') + ->once() + ->andReturn('SELECT * FROM wp_mayo_subscribers WHERE email = "test@example.com"'); + + $mockWpdb->shouldReceive('get_row') + ->once() + ->andReturn(null); + + // Insert new subscriber + $mockWpdb->shouldReceive('insert') + ->once() + ->andReturn(1); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::subscribe('test@example.com'); + + $this->assertIsArray($result); + $this->assertTrue($result['success']); + $this->assertEquals('confirmation_sent', $result['code']); + } + + /** + * Test subscribe with preferences creates new subscriber with preferences + */ + public function testSubscribeWithPreferencesCreatesSubscriberWithPreferences(): void { + $mockWpdb = $this->createMockWpdb(); + + $mockWpdb->shouldReceive('prepare') + ->once() + ->andReturn('SQL'); + + $mockWpdb->shouldReceive('get_row') + ->once() + ->andReturn(null); + + $mockWpdb->shouldReceive('insert') + ->once() + ->withArgs(function($table, $data) { + return isset($data['preferences']) && $data['preferences'] !== null; + }) + ->andReturn(1); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $preferences = ['categories' => [1, 2], 'tags' => [], 'service_bodies' => []]; + $result = TestableSubscriber::subscribe('test@example.com', $preferences); + + $this->assertTrue($result['success']); + } + + /** + * Test subscribe returns error when email already active + */ + public function testSubscribeReturnsErrorWhenEmailAlreadyActive(): void { + $mockWpdb = $this->createMockWpdb(); + + $existingSubscriber = (object)[ + 'id' => 1, + 'email' => 'test@example.com', + 'status' => 'active', + 'token' => 'existingtoken123' + ]; + + $mockWpdb->shouldReceive('prepare')->once()->andReturn('SQL'); + $mockWpdb->shouldReceive('get_row')->once()->andReturn($existingSubscriber); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::subscribe('test@example.com'); + + $this->assertFalse($result['success']); + $this->assertEquals('already_subscribed', $result['code']); + } + + /** + * Test subscribe resends confirmation for pending subscriber + */ + public function testSubscribeResendsConfirmationForPending(): void { + $mockWpdb = $this->createMockWpdb(); + + $existingSubscriber = (object)[ + 'id' => 1, + 'email' => 'test@example.com', + 'status' => 'pending', + 'token' => 'existingtoken123' + ]; + + $mockWpdb->shouldReceive('prepare')->once()->andReturn('SQL'); + $mockWpdb->shouldReceive('get_row')->once()->andReturn($existingSubscriber); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::subscribe('test@example.com'); + + $this->assertTrue($result['success']); + $this->assertEquals('confirmation_resent', $result['code']); + } + + /** + * Test subscribe re-subscribes unsubscribed user + */ + public function testSubscribeResubscribesUnsubscribedUser(): void { + $mockWpdb = $this->createMockWpdb(); + + $existingSubscriber = (object)[ + 'id' => 1, + 'email' => 'test@example.com', + 'status' => 'unsubscribed', + 'token' => 'existingtoken123' + ]; + + $mockWpdb->shouldReceive('prepare')->once()->andReturn('SQL'); + $mockWpdb->shouldReceive('get_row')->once()->andReturn($existingSubscriber); + $mockWpdb->shouldReceive('update')->once()->andReturn(1); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::subscribe('test@example.com'); + + $this->assertTrue($result['success']); + $this->assertEquals('resubscribed', $result['code']); + } + + /** + * Test subscribe returns error on database failure + */ + public function testSubscribeReturnsErrorOnDatabaseFailure(): void { + $mockWpdb = $this->createMockWpdb(); + + $mockWpdb->shouldReceive('prepare')->once()->andReturn('SQL'); + $mockWpdb->shouldReceive('get_row')->once()->andReturn(null); + $mockWpdb->shouldReceive('insert')->once()->andReturn(false); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::subscribe('test@example.com'); + + $this->assertFalse($result['success']); + $this->assertEquals('database_error', $result['code']); + } + + /** + * Test get_by_token returns subscriber + */ + public function testGetByTokenReturnsSubscriber(): void { + $mockWpdb = $this->createMockWpdb(); + + $subscriber = (object)[ + 'id' => 1, + 'email' => 'test@example.com', + 'status' => 'active', + 'token' => 'abc123def456' + ]; + + $mockWpdb->shouldReceive('prepare')->once()->andReturn('SQL'); + $mockWpdb->shouldReceive('get_row')->once()->andReturn($subscriber); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::get_by_token('abc123def456'); + + $this->assertNotNull($result); + $this->assertEquals('test@example.com', $result->email); + } + + /** + * Test get_by_token returns null for invalid token + */ + public function testGetByTokenReturnsNullForInvalidToken(): void { + $mockWpdb = $this->createMockWpdb(); + + $mockWpdb->shouldReceive('prepare')->once()->andReturn('SQL'); + $mockWpdb->shouldReceive('get_row')->once()->andReturn(null); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::get_by_token('invalidtoken'); + + $this->assertNull($result); + } + + /** + * Test confirm activates subscriber + */ + public function testConfirmActivatesSubscriber(): void { + $mockWpdb = $this->createMockWpdb(); + + $subscriber = (object)[ + 'id' => 1, + 'email' => 'test@example.com', + 'status' => 'pending', + 'token' => 'validtoken123' + ]; + + $mockWpdb->shouldReceive('prepare')->once()->andReturn('SQL'); + $mockWpdb->shouldReceive('get_row')->once()->andReturn($subscriber); + $mockWpdb->shouldReceive('update')->once()->andReturn(1); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::confirm('validtoken123'); + + $this->assertTrue($result['success']); + $this->assertEquals('confirmed', $result['code']); + } + + /** + * Test confirm returns error for invalid token + */ + public function testConfirmReturnsErrorForInvalidToken(): void { + $mockWpdb = $this->createMockWpdb(); + + $mockWpdb->shouldReceive('prepare')->once()->andReturn('SQL'); + $mockWpdb->shouldReceive('get_row')->once()->andReturn(null); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::confirm('invalidtoken'); + + $this->assertFalse($result['success']); + $this->assertEquals('invalid_token', $result['code']); + } + + /** + * Test confirm returns already confirmed for active subscriber + */ + public function testConfirmReturnsAlreadyConfirmedForActive(): void { + $mockWpdb = $this->createMockWpdb(); + + $subscriber = (object)[ + 'id' => 1, + 'email' => 'test@example.com', + 'status' => 'active', + 'token' => 'validtoken123' + ]; + + $mockWpdb->shouldReceive('prepare')->once()->andReturn('SQL'); + $mockWpdb->shouldReceive('get_row')->once()->andReturn($subscriber); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::confirm('validtoken123'); + + $this->assertTrue($result['success']); + $this->assertEquals('already_confirmed', $result['code']); + } + + /** + * Test unsubscribe marks subscriber as unsubscribed + */ + public function testUnsubscribeMarksSubscriberAsUnsubscribed(): void { + $mockWpdb = $this->createMockWpdb(); + + $subscriber = (object)[ + 'id' => 1, + 'email' => 'test@example.com', + 'status' => 'active', + 'token' => 'validtoken123' + ]; + + $mockWpdb->shouldReceive('prepare')->once()->andReturn('SQL'); + $mockWpdb->shouldReceive('get_row')->once()->andReturn($subscriber); + $mockWpdb->shouldReceive('update')->once()->andReturn(1); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::unsubscribe('validtoken123'); + + $this->assertTrue($result['success']); + $this->assertEquals('unsubscribed', $result['code']); + } + + /** + * Test unsubscribe returns error for invalid token + */ + public function testUnsubscribeReturnsErrorForInvalidToken(): void { + $mockWpdb = $this->createMockWpdb(); + + $mockWpdb->shouldReceive('prepare')->once()->andReturn('SQL'); + $mockWpdb->shouldReceive('get_row')->once()->andReturn(null); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::unsubscribe('invalidtoken'); + + $this->assertFalse($result['success']); + $this->assertEquals('invalid_token', $result['code']); + } + + /** + * Test unsubscribe returns already_unsubscribed for unsubscribed user + */ + public function testUnsubscribeReturnsAlreadyUnsubscribed(): void { + $mockWpdb = $this->createMockWpdb(); + + $subscriber = (object)[ + 'id' => 1, + 'email' => 'test@example.com', + 'status' => 'unsubscribed', + 'token' => 'validtoken123' + ]; + + $mockWpdb->shouldReceive('prepare')->once()->andReturn('SQL'); + $mockWpdb->shouldReceive('get_row')->once()->andReturn($subscriber); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::unsubscribe('validtoken123'); + + $this->assertTrue($result['success']); + $this->assertEquals('already_unsubscribed', $result['code']); + } + + /** + * Test get_active_subscribers returns only active subscribers + */ + public function testGetActiveSubscribersReturnsOnlyActive(): void { + $mockWpdb = $this->createMockWpdb(); + + $subscribers = [ + (object)['id' => 1, 'email' => 'active1@example.com', 'status' => 'active'], + (object)['id' => 2, 'email' => 'active2@example.com', 'status' => 'active'] + ]; + + $mockWpdb->shouldReceive('get_results')->once()->andReturn($subscribers); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::get_active_subscribers(); + + $this->assertIsArray($result); + $this->assertCount(2, $result); + } + + /** + * Test get_all_subscribers returns all subscribers + */ + public function testGetAllSubscribersReturnsAll(): void { + $mockWpdb = $this->createMockWpdb(); + + $subscribers = [ + (object)['id' => 1, 'email' => 'a@example.com', 'status' => 'active'], + (object)['id' => 2, 'email' => 'b@example.com', 'status' => 'pending'], + (object)['id' => 3, 'email' => 'c@example.com', 'status' => 'unsubscribed'] + ]; + + $mockWpdb->shouldReceive('get_results')->once()->andReturn($subscribers); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::get_all_subscribers(); + + $this->assertIsArray($result); + $this->assertCount(3, $result); + } + + /** + * Test update_status updates subscriber status + */ + public function testUpdateStatusUpdatesSubscriberStatus(): void { + $mockWpdb = $this->createMockWpdb(); + + $mockWpdb->shouldReceive('update') + ->once() + ->withArgs(function($table, $data, $where) { + return $data['status'] === 'active'; + }) + ->andReturn(1); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::update_status(1, 'active'); + + $this->assertTrue($result); + } + + /** + * Test update_status sets confirmed_at when activating + */ + public function testUpdateStatusSetsConfirmedAtWhenActivating(): void { + $mockWpdb = $this->createMockWpdb(); + + $mockWpdb->shouldReceive('update') + ->once() + ->withArgs(function($table, $data, $where) { + return isset($data['confirmed_at']); + }) + ->andReturn(1); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::update_status(1, 'active'); + + $this->assertTrue($result); + } + + /** + * Test update_preferences updates subscriber preferences + */ + public function testUpdatePreferencesUpdatesSubscriberPreferences(): void { + $mockWpdb = $this->createMockWpdb(); + + $mockWpdb->shouldReceive('update') + ->once() + ->andReturn(1); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::update_preferences('token123', ['categories' => [1]]); + + $this->assertTrue($result); + } + + /** + * Test update_preferences returns false on failure + */ + public function testUpdatePreferencesReturnsFalseOnFailure(): void { + $mockWpdb = $this->createMockWpdb(); + + $mockWpdb->shouldReceive('update') + ->once() + ->andReturn(false); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::update_preferences('token123', ['categories' => [1]]); + + $this->assertFalse($result); + } + + /** + * Test update_preferences_by_id updates subscriber preferences by ID + */ + public function testUpdatePreferencesByIdUpdatesPreferences(): void { + $mockWpdb = $this->createMockWpdb(); + + $mockWpdb->shouldReceive('update') + ->once() + ->andReturn(1); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::update_preferences_by_id(1, ['categories' => [1, 2]]); + + $this->assertTrue($result); + } + + /** + * Test delete removes subscriber + */ + public function testDeleteRemovesSubscriber(): void { + $mockWpdb = $this->createMockWpdb(); + + $mockWpdb->shouldReceive('delete') + ->once() + ->andReturn(1); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::delete(1); + + $this->assertTrue($result); + } + + /** + * Test delete returns false when subscriber not found + */ + public function testDeleteReturnsFalseWhenNotFound(): void { + $mockWpdb = $this->createMockWpdb(); + + $mockWpdb->shouldReceive('delete') + ->once() + ->andReturn(false); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::delete(999); + + $this->assertFalse($result); + } + + /** + * Test get_matching returns subscribers matching criteria + */ + public function testGetMatchingReturnsMatchingSubscribers(): void { + $mockWpdb = $this->createMockWpdb(); + + $subscribers = [ + (object)[ + 'id' => 1, + 'email' => 'match@example.com', + 'status' => 'active', + 'preferences' => json_encode(['categories' => [1], 'tags' => [], 'service_bodies' => []]) + ] + ]; + + $mockWpdb->shouldReceive('get_results')->once()->andReturn($subscribers); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::get_matching([ + 'categories' => [1], + 'tags' => [], + 'service_body' => '' + ]); + + $this->assertIsArray($result); + $this->assertCount(1, $result); + } + + /** + * Test count_matching returns count of matching subscribers + */ + public function testCountMatchingReturnsCount(): void { + $mockWpdb = $this->createMockWpdb(); + + $subscribers = [ + (object)[ + 'id' => 1, + 'email' => 'match1@example.com', + 'status' => 'active', + 'preferences' => null + ], + (object)[ + 'id' => 2, + 'email' => 'match2@example.com', + 'status' => 'active', + 'preferences' => null + ] + ]; + + $mockWpdb->shouldReceive('get_results')->once()->andReturn($subscribers); + + TestableSubscriber::$mockWpdb = $mockWpdb; + + $result = TestableSubscriber::count_matching([ + 'categories' => [1], + 'tags' => [], + 'service_body' => '' + ]); + + $this->assertEquals(2, $result); + } + + /** + * Test get_wpdb returns wpdb instance + * Note: create_table can't be fully tested without WordPress test environment + * because it requires require_once for upgrade.php + */ + public function testGetWpdbIntegration(): void { + // The base class get_wpdb() is tested implicitly by other tests + // This test verifies the TestableSubscriber mock pattern works + $mockWpdb = $this->createMockWpdb(); + TestableSubscriber::$mockWpdb = $mockWpdb; + + // Verify the mock is returned + $this->assertEquals('wp_mayo_subscribers', TestableSubscriber::get_table_name()); + } + /** * Helper to get private method */ diff --git a/tests/bootstrap.php b/tests/bootstrap.php index baa3b27..5486997 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -171,6 +171,10 @@ public function set_body($body) { $this->body = $body; } + public function get_json_params() { + return $this->body_params; + } + #[\ReturnTypeWillChange] public function offsetExists($offset): bool { $params = $this->get_params();