diff --git a/includes/class-accepted-actions.php b/includes/class-accepted-actions.php index b1b79618..d2ff4129 100644 --- a/includes/class-accepted-actions.php +++ b/includes/class-accepted-actions.php @@ -45,6 +45,7 @@ class Accepted_Actions { 'network_incoming_post_deleted' => 'Network_Incoming_Post_Deleted', 'newspack_network_distributor_migrate_incoming_posts' => 'Distributor_Migrate_Incoming_Posts', 'network_hub_name_updated' => 'Hub_Name_Updated', + 'newspack_network_product_updated' => 'Product_Updated', ]; /** @@ -71,5 +72,6 @@ class Accepted_Actions { 'network_incoming_post_deleted', 'newspack_network_distributor_migrate_incoming_posts', 'network_hub_name_updated', + 'newspack_network_product_updated', ]; } diff --git a/includes/class-initializer.php b/includes/class-initializer.php index aaa0bca5..044b9270 100644 --- a/includes/class-initializer.php +++ b/includes/class-initializer.php @@ -60,11 +60,14 @@ public static function init() { CLI\Integrity_Check::init(); Woocommerce\Events::init(); + Woocommerce\Product_Admin::init(); Woocommerce_Subscriptions\My_Account::init(); Woocommerce_Memberships\Admin::init(); Woocommerce_Memberships\Events::init(); Woocommerce_Memberships\Subscriptions_Integration::init(); Woocommerce_Memberships\Limit_Purchase::init(); + Content_Gate\Access::init(); + Content_Gate\Limit_Purchase::init(); register_activation_hook( NEWSPACK_NETWORK_PLUGIN_FILE, [ __CLASS__, 'activation_hook' ] ); } diff --git a/includes/cli/backfillers/class-product-updated.php b/includes/cli/backfillers/class-product-updated.php new file mode 100644 index 00000000..614c00c7 --- /dev/null +++ b/includes/cli/backfillers/class-product-updated.php @@ -0,0 +1,79 @@ +get_id() ); + } + + /** + * Gets the events to be processed. + * + * @return \Newspack_Network\Incoming_Events\Abstract_Incoming_Event[] $events An array of events. + */ + public function get_events() { + if ( ! function_exists( 'wc_get_product' ) ) { + return []; + } + + $products = get_posts( + [ + 'post_type' => 'product', + 'post_status' => 'any', + 'numberposts' => -1, + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + [ + 'key' => Product_Admin::NETWORK_ID_META_KEY, + 'compare' => '!=', + 'value' => '', + ], + ], + 'date_query' => [ + 'column' => 'post_modified_gmt', + 'after' => $this->start, + 'before' => $this->end, + 'inclusive' => true, + ], + ] + ); + + $this->maybe_initialize_progress_bar( 'Processing products', count( $products ) ); + + $events = []; + WP_CLI::line( '' ); + WP_CLI::line( sprintf( 'Found %s product(s) with Network IDs eligible for sync.', count( $products ) ) ); + WP_CLI::line( '' ); + + foreach ( $products as $product ) { + $product_data = Woocommerce_Events::product_updated( $product->ID ); + if ( ! $product_data ) { + continue; + } + $timestamp = strtotime( $product->post_modified_gmt ); + $events[] = new \Newspack_Network\Incoming_Events\Product_Updated( get_bloginfo( 'url' ), $product_data, $timestamp ); + } + + return $events; + } +} diff --git a/includes/content-gate/class-access.php b/includes/content-gate/class-access.php new file mode 100644 index 00000000..a3c4d9fe --- /dev/null +++ b/includes/content-gate/class-access.php @@ -0,0 +1,164 @@ + $subscriptions ) { + $site_products = $network_products[ $site ] ?? []; + foreach ( $subscriptions as $subscription ) { + foreach ( $subscription['products'] as $product ) { + // Cast to string to handle int/string key mismatch from JSON round-tripping. + $product_id = (string) $product['id']; + // Look up this product's Network ID from synced data. + $remote_network_id = $site_products[ $product_id ]['network_id'] ?? ''; + if ( ! empty( $remote_network_id ) && in_array( $remote_network_id, $network_ids, true ) ) { + return true; + } + } + } + } + + return $has_subscription; + } + + /** + * Get Network IDs for the given local product IDs. + * + * @param array $product_ids Local product IDs. + * @return array Array of Network IDs (non-empty values only). + */ + public static function get_network_ids_for_products( $product_ids ) { + $network_ids = []; + foreach ( $product_ids as $product_id ) { + $network_id = Product_Admin::get_network_id( $product_id ); + if ( ! empty( $network_id ) ) { + $network_ids[] = $network_id; + } + } + return array_unique( $network_ids ); + } + + /** + * Check if a user has an active network subscription for a given Network ID. + * + * @param int $user_id The user ID. + * @param string $network_id The product Network ID to match. + * @return array|false Array with 'site' and 'subscription' keys if found, false otherwise. + */ + public static function user_has_active_network_subscription_for_network_id( $user_id, $network_id ) { + $network_subscriptions = self::get_user_network_active_subscriptions( $user_id ); + if ( empty( $network_subscriptions ) ) { + return false; + } + + $network_products = get_option( Product_Updated::OPTION_NAME, [] ); + + foreach ( $network_subscriptions as $site => $subscriptions ) { + $site_products = $network_products[ $site ] ?? []; + foreach ( $subscriptions as $subscription ) { + foreach ( $subscription['products'] as $product ) { + // Cast to string to handle int/string key mismatch from JSON round-tripping. + $product_id = (string) $product['id']; + $remote_network_id = $site_products[ $product_id ]['network_id'] ?? ''; + if ( ! empty( $remote_network_id ) && $remote_network_id === $network_id ) { + return [ + 'site' => $site, + 'subscription' => $subscription, + ]; + } + } + } + } + + return false; + } + + /** + * Gets all active subscriptions for a user across all network sites. + * + * @param int $user_id The user ID. + * @return array An array with the site as key and an array of subscriptions as value. + */ + public static function get_user_network_active_subscriptions( $user_id ) { + $meta = get_user_meta( $user_id, Subscription_Changed::USER_SUBSCRIPTIONS_META_KEY, true ); + if ( ! $meta ) { + return []; + } + + $returned_subs = []; + + foreach ( $meta as $site => $subscriptions ) { + $returned_subs[ $site ] = array_filter( + $subscriptions, + function ( $sub ) { + return in_array( $sub['status'], [ 'active', 'pending-cancel' ], true ); + } + ); + if ( empty( $returned_subs[ $site ] ) ) { + unset( $returned_subs[ $site ] ); + } + } + + return $returned_subs; + } +} diff --git a/includes/content-gate/class-limit-purchase.php b/includes/content-gate/class-limit-purchase.php new file mode 100644 index 00000000..e769426f --- /dev/null +++ b/includes/content-gate/class-limit-purchase.php @@ -0,0 +1,140 @@ +is_type( [ 'subscription', 'subscription_variation', 'variable-subscription' ] ) ) { + return; + } + + $network_id = Product_Admin::get_network_id( $product->get_id() ); + if ( empty( $network_id ) ) { + return; + } + + return Access::user_has_active_network_subscription_for_network_id( $user_id, $network_id ); + } + + /** + * Filters the error message shown when a product can't be added to the cart. + * + * @param string $message Message. + * @param \WC_Product $product_data Product data. + * @return string + */ + public static function cart_product_cannot_be_purchased_message( $message, \WC_Product $product_data ) { + $network_subscription = self::get_network_equivalent_subscription( $product_data ); + if ( $network_subscription ) { + $message = sprintf( + /* translators: %s: Site URL */ + __( "You can't buy this subscription because you already have it active on %s", 'newspack-network' ), + $network_subscription['site'] + ); + } + return $message; + } + + /** + * Get user from email. + * + * @return false|int User ID if found by email address, false otherwise. + */ + private static function get_user_id_from_email() { + $billing_email = filter_input( INPUT_POST, 'billing_email', FILTER_SANITIZE_EMAIL ); + if ( $billing_email ) { + $customer = \get_user_by( 'email', $billing_email ); + if ( $customer ) { + return $customer->ID; + } + } + return false; + } + + /** + * Validate network subscription for logged out readers. + * + * @param array $data Checkout data. + * @param \WP_Error $errors Checkout errors. + */ + public static function validate_network_subscription( $data, $errors ) { + if ( is_user_logged_in() || ! function_exists( 'WC' ) ) { + return; + } + $id_from_email = self::get_user_id_from_email(); + if ( $id_from_email ) { + $cart_items = WC()->cart->get_cart(); + foreach ( $cart_items as $cart_item ) { + $product = $cart_item['data']; + $network_active_subscription = self::get_network_equivalent_subscription( $product, $id_from_email ); + if ( $network_active_subscription ) { + $error_message = __( 'Oops! You already have a subscription on another site in this network that grants you access to this site as well. Please log in using the same email address.', 'newspack-network' ); + $errors->add( 'network_subscription', $error_message ); + break; + } + } + } + } +} diff --git a/includes/hub/stores/event-log-items/class-product-updated.php b/includes/hub/stores/event-log-items/class-product-updated.php new file mode 100644 index 00000000..c93f31df --- /dev/null +++ b/includes/hub/stores/event-log-items/class-product-updated.php @@ -0,0 +1,33 @@ +get_node_id() ) ? get_bloginfo( 'url' ) : $this->get_node_url(); + $data = $this->get_data(); + return sprintf( + /* translators: 1: Product name 2: Network ID 3: site url */ + __( 'Product "%1$s" (Network ID: %2$s) updated on %3$s', 'newspack-network' ), + $data->name ?? __( 'Unknown', 'newspack-network' ), + empty( $data->network_id ) ? __( 'none', 'newspack-network' ) : $data->network_id, + $url + ); + } +} diff --git a/includes/incoming-events/class-product-updated.php b/includes/incoming-events/class-product-updated.php new file mode 100644 index 00000000..eac29959 --- /dev/null +++ b/includes/incoming-events/class-product-updated.php @@ -0,0 +1,118 @@ +update_option(); + } + + /** + * Process event in Node. + * + * @return void + */ + public function process_in_node() { + $this->update_option(); + } + + /** + * Updates the option with the product data. + * + * @return void + */ + public function update_option() { + Debugger::log( 'Processing product_updated' ); + + $current_value = get_option( self::OPTION_NAME, [] ); + + if ( ! is_array( $current_value ) ) { + $current_value = []; + } + + if ( ! isset( $current_value[ $this->get_site() ] ) ) { + $current_value[ $this->get_site() ] = []; + } + + $current_value[ $this->get_site() ][ $this->get_id() ] = [ + 'id' => $this->get_id(), + 'name' => $this->get_name(), + 'slug' => $this->get_slug(), + 'network_id' => $this->get_network_id(), + ]; + + // Also store entries for variations so that variation IDs resolve to the parent's Network ID. + $variation_ids = $this->get_variation_ids(); + foreach ( $variation_ids as $variation_id ) { + $current_value[ $this->get_site() ][ $variation_id ] = [ + 'id' => $variation_id, + 'network_id' => $this->get_network_id(), + ]; + } + + update_option( self::OPTION_NAME, $current_value, false ); + } + + /** + * Returns the id property. + * + * @return ?int + */ + public function get_id() { + return $this->data->id ?? null; + } + + /** + * Returns the name property. + * + * @return ?string + */ + public function get_name() { + return $this->data->name ?? null; + } + + /** + * Returns the slug property. + * + * @return ?string + */ + public function get_slug() { + return $this->data->slug ?? null; + } + + /** + * Returns the network_id property. + * + * @return ?string + */ + public function get_network_id() { + return $this->data->network_id ?? null; + } + + /** + * Returns the variation IDs. + * + * @return array + */ + public function get_variation_ids() { + return $this->data->variation_ids ?? []; + } +} diff --git a/includes/woocommerce/class-events.php b/includes/woocommerce/class-events.php index 72434f36..d6243112 100644 --- a/includes/woocommerce/class-events.php +++ b/includes/woocommerce/class-events.php @@ -9,6 +9,7 @@ use Newspack\Data_Events; use Newspack_Network\Woocommerce_Memberships\Admin as Memberships_Admin; +use Newspack_Network\Woocommerce\Product_Admin; /** * Class to register additional listeners to the Newspack Data Events API @@ -36,6 +37,38 @@ public static function register_listeners() { Data_Events::register_listener( 'woocommerce_order_status_changed', 'newspack_node_order_changed', [ __CLASS__, 'item_changed' ] ); Data_Events::register_listener( 'woocommerce_subscription_status_changed', 'newspack_node_subscription_changed', [ __CLASS__, 'subscription_changed' ] ); + Data_Events::register_listener( 'newspack_network_save_product', 'newspack_network_product_updated', [ __CLASS__, 'product_updated' ] ); + } + + /** + * Triggers a data event when a product's Network ID is updated. + * + * @param int $product_id The product post ID. + * @return array|void + */ + public static function product_updated( $product_id ) { + if ( ! function_exists( 'wc_get_product' ) ) { + return; + } + $product = wc_get_product( $product_id ); + if ( ! $product ) { + return; + } + $network_id = get_post_meta( $product->get_id(), Product_Admin::NETWORK_ID_META_KEY, true ); + + $result = [ + 'id' => $product->get_id(), + 'network_id' => $network_id, + 'name' => $product->get_name(), + 'slug' => $product->get_slug(), + ]; + + // Include variation IDs so they are also mapped to this Network ID. + if ( $product->is_type( 'variable-subscription' ) ) { + $result['variation_ids'] = $product->get_children(); + } + + return $result; } /** diff --git a/includes/woocommerce/class-product-admin.php b/includes/woocommerce/class-product-admin.php new file mode 100644 index 00000000..e082e9a2 --- /dev/null +++ b/includes/woocommerce/class-product-admin.php @@ -0,0 +1,128 @@ +get_parent_id() ) { + return get_post_meta( $product->get_parent_id(), self::NETWORK_ID_META_KEY, true ); + } + } + + return ''; + } + + /** + * Initializer. + */ + public static function init() { + add_action( 'add_meta_boxes', [ __CLASS__, 'add_meta_box' ] ); + add_action( 'save_post_product', [ __CLASS__, 'save_meta_box' ] ); + } + + /** + * Adds a meta box to the product edit screen. + */ + public static function add_meta_box() { + if ( ! function_exists( 'wc_get_product' ) ) { + return; + } + global $post; + $product = wc_get_product( $post ); + if ( ! $product || ! $product->is_type( [ 'subscription', 'variable-subscription' ] ) ) { + return; + } + add_meta_box( + 'newspack-network-product-meta-box', + __( 'Newspack Network', 'newspack-network' ), + [ __CLASS__, 'render_meta_box' ], + 'product', + 'side' + ); + } + + /** + * Renders the meta box. + * + * @param \WP_Post $post The post object. + */ + public static function render_meta_box( $post ) { + $network_id = get_post_meta( $post->ID, self::NETWORK_ID_META_KEY, true ); + wp_nonce_field( 'newspack_network_save_product', 'newspack_network_save_product_nonce' ); + ?> + + +

+ is_type( [ 'subscription', 'variable-subscription' ] ) ) { + return; + } + + $network_id = sanitize_text_field( wp_unslash( $_POST['newspack_network_product_id'] ?? '' ) ); + + update_post_meta( $post_id, self::NETWORK_ID_META_KEY, $network_id ); + + /** + * Triggers an action when a product's network id is saved. + * + * @param int $post_id The product post ID. + */ + do_action( 'newspack_network_save_product', $post_id ); + } +} diff --git a/tests/unit-tests/test-content-gate-access.php b/tests/unit-tests/test-content-gate-access.php new file mode 100644 index 00000000..75fae9c9 --- /dev/null +++ b/tests/unit-tests/test-content-gate-access.php @@ -0,0 +1,399 @@ + [ + 100 => [ + 'id' => 100, + 'name' => 'Premium Monthly', + 'slug' => 'premium-monthly', + 'network_id' => 'premium', + ], + 101 => [ + 'id' => 101, + 'name' => 'Basic Plan', + 'slug' => 'basic-plan', + 'network_id' => 'basic', + ], + 102 => [ + 'id' => 102, + 'name' => 'Local Only', + 'slug' => 'local-only', + 'network_id' => '', + ], + ], + 'http://site2' => [ + 200 => [ + 'id' => 200, + 'name' => 'Premium Yearly', + 'slug' => 'premium-yearly', + 'network_id' => 'premium', + ], + ], + ]; + update_option( Product_Updated::OPTION_NAME, $products, false ); + + // Create local products with Network ID meta. + self::$local_product_premium = wp_insert_post( + [ + 'post_type' => 'product', + 'post_status' => 'publish', + 'post_title' => 'Local Premium', + ] + ); + update_post_meta( self::$local_product_premium, Product_Admin::NETWORK_ID_META_KEY, 'premium' ); + + self::$local_product_basic = wp_insert_post( + [ + 'post_type' => 'product', + 'post_status' => 'publish', + 'post_title' => 'Local Basic', + ] + ); + update_post_meta( self::$local_product_basic, Product_Admin::NETWORK_ID_META_KEY, 'basic' ); + + self::$local_product_no_network = wp_insert_post( + [ + 'post_type' => 'product', + 'post_status' => 'publish', + 'post_title' => 'No Network ID', + ] + ); + + // Create test users with network subscription meta. + self::$user_with_active_sub = wp_insert_user( + [ + 'user_login' => 'cg_user_active_sub', + 'user_pass' => '123', + 'role' => 'subscriber', + ] + ); + add_user_meta( + self::$user_with_active_sub, + Subscription_Changed::USER_SUBSCRIPTIONS_META_KEY, + [ + 'http://site1' => [ + 500 => [ + 'id' => 500, + 'status' => 'active', + 'products' => [ + 100 => [ + 'id' => 100, + 'name' => 'Premium Monthly', + ], + ], + ], + ], + ] + ); + + self::$user_with_pending_cancel_sub = wp_insert_user( + [ + 'user_login' => 'cg_user_pending_sub', + 'user_pass' => '123', + 'role' => 'subscriber', + ] + ); + add_user_meta( + self::$user_with_pending_cancel_sub, + Subscription_Changed::USER_SUBSCRIPTIONS_META_KEY, + [ + 'http://site2' => [ + 501 => [ + 'id' => 501, + 'status' => 'pending-cancel', + 'products' => [ + 200 => [ + 'id' => 200, + 'name' => 'Premium Yearly', + ], + ], + ], + ], + ] + ); + + self::$user_with_cancelled_sub = wp_insert_user( + [ + 'user_login' => 'cg_user_cancelled_sub', + 'user_pass' => '123', + 'role' => 'subscriber', + ] + ); + add_user_meta( + self::$user_with_cancelled_sub, + Subscription_Changed::USER_SUBSCRIPTIONS_META_KEY, + [ + 'http://site2' => [ + 502 => [ + 'id' => 502, + 'status' => 'cancelled', + 'products' => [ + 200 => [ + 'id' => 200, + 'name' => 'Premium Yearly', + ], + ], + ], + ], + ] + ); + + self::$user_without_subs = wp_insert_user( + [ + 'user_login' => 'cg_user_no_subs', + 'user_pass' => '123', + 'role' => 'subscriber', + ] + ); + } + + /** + * When $has_subscription is already true, the filter should short-circuit and return true. + */ + public function test_check_network_subscriptions_already_granted() { + $result = Access::check_network_subscriptions( true, self::$user_without_subs, [ self::$local_product_premium ] ); + $this->assertTrue( $result ); + } + + /** + * When product_ids is empty, the filter should return the original value. + */ + public function test_check_network_subscriptions_empty_product_ids() { + $result = Access::check_network_subscriptions( false, self::$user_with_active_sub, [] ); + $this->assertFalse( $result ); + } + + /** + * When the local product has no Network ID, the filter should return the original value. + */ + public function test_check_network_subscriptions_no_network_id_on_product() { + $result = Access::check_network_subscriptions( false, self::$user_with_active_sub, [ self::$local_product_no_network ] ); + $this->assertFalse( $result ); + } + + /** + * When the user has no network subscriptions, the filter should return the original value. + */ + public function test_check_network_subscriptions_user_without_subs() { + $result = Access::check_network_subscriptions( false, self::$user_without_subs, [ self::$local_product_premium ] ); + $this->assertFalse( $result ); + } + + /** + * When the user has an active subscription on another site for a product with a matching + * Network ID, access should be granted. + */ + public function test_check_network_subscriptions_matching_network_id() { + $result = Access::check_network_subscriptions( false, self::$user_with_active_sub, [ self::$local_product_premium ] ); + $this->assertTrue( $result ); + } + + /** + * Pending-cancel subscriptions should still grant access. + */ + public function test_check_network_subscriptions_pending_cancel_grants_access() { + $result = Access::check_network_subscriptions( false, self::$user_with_pending_cancel_sub, [ self::$local_product_premium ] ); + $this->assertTrue( $result ); + } + + /** + * Cancelled subscriptions should not grant access. + */ + public function test_check_network_subscriptions_cancelled_does_not_grant_access() { + $result = Access::check_network_subscriptions( false, self::$user_with_cancelled_sub, [ self::$local_product_premium ] ); + $this->assertFalse( $result ); + } + + /** + * When the user's subscription is for a different Network ID, access should not be granted. + */ + public function test_check_network_subscriptions_non_matching_network_id() { + $result = Access::check_network_subscriptions( false, self::$user_with_active_sub, [ self::$local_product_basic ] ); + $this->assertFalse( $result ); + } + + /** + * When the synced product data is missing (option not set), access should not be granted. + */ + public function test_check_network_subscriptions_missing_synced_product_data() { + $original = get_option( Product_Updated::OPTION_NAME ); + delete_option( Product_Updated::OPTION_NAME ); + + $result = Access::check_network_subscriptions( false, self::$user_with_active_sub, [ self::$local_product_premium ] ); + $this->assertFalse( $result ); + + update_option( Product_Updated::OPTION_NAME, $original, false ); + } + + /** + * Data provider for test_user_has_active_network_subscription_for_network_id. + * + * @return array + */ + public function network_subscription_for_network_id_data() { + return [ + 'active sub, matching network ID' => [ + 'user_with_active_sub', + 'premium', + 500, + 'http://site1', + ], + 'active sub, non-matching network ID' => [ + 'user_with_active_sub', + 'basic', + false, + ], + 'active sub, nonexistent network ID' => [ + 'user_with_active_sub', + 'nonexistent', + false, + ], + 'pending-cancel sub, matching network ID' => [ + 'user_with_pending_cancel_sub', + 'premium', + 501, + 'http://site2', + ], + 'cancelled sub, matching network ID' => [ + 'user_with_cancelled_sub', + 'premium', + false, + ], + 'no subs, matching network ID' => [ + 'user_without_subs', + 'premium', + false, + ], + ]; + } + + /** + * Test user_has_active_network_subscription_for_network_id. + * + * @param string $user_property Static property name for the user ID. + * @param string $network_id Product Network ID to look up. + * @param int|bool $expected_id Expected subscription ID, or false. + * @param string $expected_site Expected site URL (only when $expected_id is not false). + * @dataProvider network_subscription_for_network_id_data + */ + public function test_user_has_active_network_subscription_for_network_id( $user_property, $network_id, $expected_id, $expected_site = '' ) { + $result = Access::user_has_active_network_subscription_for_network_id( self::$$user_property, $network_id ); + if ( $expected_id ) { + $this->assertIsArray( $result ); + $this->assertArrayHasKey( 'site', $result ); + $this->assertArrayHasKey( 'subscription', $result ); + $this->assertEquals( $expected_id, $result['subscription']['id'] ); + $this->assertEquals( $expected_site, $result['site'] ); + } else { + $this->assertFalse( $result ); + } + } + + /** + * Test get_network_ids_for_products returns correct Network IDs. + */ + public function test_get_network_ids_for_products() { + $network_ids = Access::get_network_ids_for_products( [ self::$local_product_premium, self::$local_product_basic ] ); + $this->assertCount( 2, $network_ids ); + $this->assertContains( 'premium', $network_ids ); + $this->assertContains( 'basic', $network_ids ); + } + + /** + * Test get_network_ids_for_products skips products without Network IDs. + */ + public function test_get_network_ids_for_products_skips_empty() { + $network_ids = Access::get_network_ids_for_products( [ self::$local_product_no_network ] ); + $this->assertEmpty( $network_ids ); + } + + /** + * Test get_network_ids_for_products deduplicates. + */ + public function test_get_network_ids_for_products_deduplicates() { + // Create another product with the same Network ID. + $another_premium = wp_insert_post( + [ + 'post_type' => 'product', + 'post_status' => 'publish', + 'post_title' => 'Another Premium', + ] + ); + update_post_meta( $another_premium, Product_Admin::NETWORK_ID_META_KEY, 'premium' ); + + $network_ids = Access::get_network_ids_for_products( [ self::$local_product_premium, $another_premium ] ); + $this->assertCount( 1, $network_ids ); + $this->assertContains( 'premium', $network_ids ); + + wp_delete_post( $another_premium, true ); + } +}