diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e30b1..b8a3876 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # Changelog +## 3.0.0 + +**BREAKING.** The two plugin-dependent providers shipped in v1.4.0 / v1.5.0 have been extracted into a separate WordPress add-on plugin — **AcrossAI User Access Pro** ([`acrossai/user-access-pro`](https://github.com/acrossai-co/user-access-pro)) — along with eight new integrations. The library now ships only the three WordPress-native providers (`wp_role`, `wp_user`, `wp_capability`) plus a new extension point for third-party add-ons. + +### Removed classes + +- `BuddyBossProfileTypeProvider` (`bb_profile_type`) — added in v1.4.0 +- `MemberPressMembershipProvider` (`mepr_membership`) — added in v1.5.0 + +The corresponding v1.6.0 opt-in filters (`wpb_access_control_bb_profile_type_enabled`, `wpb_access_control_mepr_membership_enabled`) are also gone. To get either integration back, install and activate **AcrossAI User Access Pro** — it registers replacements automatically. The new plugin uses fresh IDs prefixed `uap_` (`uap_bb_profile_type`, `uap_mepr`) so any pre-existing DB rules with the old IDs will not match. Since both providers defaulted to `is_available() === false` in v1.6.0 through v2.0.x, most sites had no live rules to migrate. + +### Added + +- **`wpb_access_control_register_providers` global filter.** New `apply_filters()` call in `AccessControlManager::load_providers()` — fires before the per-consumer filter with `( array $providers, string $table_slug )`. Add-on plugins hook this once and their providers appear in every consumer of the library. See the "Writing a third-party provider add-on" section in the README. + +### AcrossAI User Access Pro bundles 10 integrations + +Along with replacements for BuddyBoss and MemberPress, the new add-on ships eight integrations that were never in this library: + +- LearnDash Group (`uap_ld_group`) +- LifterLMS Membership (`uap_llms`) +- Paid Memberships Pro (`uap_pmpro`) +- Restrict Content Pro (`uap_rcp`) +- WooCommerce Memberships (`uap_wc`) +- s2Member Level (`uap_s2`) +- Wishlist Member Level (`uap_wlm`) +- Memberium Membership (`uap_memberium`) + +Once activated, every option lights up in the "Who can access" dropdown automatically whenever the underlying dependency plugin is present — no per-consumer bootstrap code required. + +### Migration + +If you had enabled either provider via a v1.6.0 opt-in filter: + +1. Install **AcrossAI User Access Pro** on the site. +2. Remove any `wpb_access_control_bb_profile_type_enabled` / `wpb_access_control_mepr_membership_enabled` filter code — the new plugin auto-enables its providers whenever the underlying dependency plugin is active. +3. If you have live rules stored with the old IDs, migrate them: `UPDATE {prefix}{consumer}_access_control SET access_control_key = 'uap_bb_profile_type' WHERE access_control_key = 'bb_profile_type'` and `... SET access_control_key = 'uap_mepr' WHERE access_control_key = 'mepr_membership'`. + ## 2.0.0 **BREAKING.** Each consumer plugin now owns its own database table, object-cache group, and REST route prefix. Previously every consumer wrote into the single shared `{prefix}wpb_access_control` table — two plugins on the same WordPress install collided on storage, cache, and routes. diff --git a/README.md b/README.md index 7a535a6..3149a27 100644 --- a/README.md +++ b/README.md @@ -338,13 +338,20 @@ The component has four states driven by a single **"Who can access"** dropdown: | **Everyone (no restriction)** | Nothing — all users can access | | **WordPress Role** | Checkboxes for each WordPress role | | **WordPress Capability** | Checkboxes for each WordPress capability (discovered across all roles) | -| **BuddyBoss Profile Type** | Checkboxes for each BuddyBoss profile type — only visible when BuddyBoss is active *and* the consumer opts in via `wpb_access_control_bb_profile_type_enabled` | -| **MemberPress Membership** | Checkboxes for each MemberPress membership — only visible when MemberPress is active *and* the consumer opts in via `wpb_access_control_mepr_membership_enabled` | | **Users** | Search-as-you-type field + selected-user tags | Custom providers registered via the filter also appear in the dropdown. If they expose `options`, checkboxes are rendered automatically. +> **Third-party integrations (BuddyBoss, MemberPress, LearnDash, LifterLMS, +> Paid Memberships Pro, Restrict Content Pro, WooCommerce Memberships, +> s2Member, Wishlist Member, Memberium)** shipped in the library through +> v2.0.x have moved to a separate add-on plugin: **AcrossAI User Access Pro** +> (`acrossai/user-access-pro`). Install and activate it and the corresponding +> option appears in the dropdown automatically whenever the underlying plugin +> is active — no `add_filter` wiring required. See "Writing a third-party +> provider add-on" below to build your own. + ### Enqueue the built assets The compiled assets live in `assets/build/`. The `.asset.php` file declares @@ -813,8 +820,12 @@ correct controls dynamically without hard-coding provider IDs. | `wp_role` | `WpRoleProvider` | Restricts by WordPress user role. Administrator is always bypassed. | | `wp_user` | `WpUserProvider` | Restricts to specific WordPress users by ID. | | `wp_capability` | `WpCapabilityProvider` | Restricts by one or more WordPress capability slugs. Users holding **any** of the selected capabilities pass. | -| `bb_profile_type` | `BuddyBossProfileTypeProvider` | Restricts by one or more BuddyBoss profile types (member types). **Opt-in**: requires `wpb_access_control_bb_profile_type_enabled` to return `true` *and* BuddyBoss Platform to be active. | -| `mepr_membership` | `MemberPressMembershipProvider` | Restricts by one or more MemberPress memberships. **Opt-in**: requires `wpb_access_control_mepr_membership_enabled` to return `true` *and* MemberPress to be active. | + +> Providers for BuddyBoss, MemberPress, LearnDash, LifterLMS, Paid +> Memberships Pro, Restrict Content Pro, WooCommerce Memberships, s2Member, +> Wishlist Member, and Memberium ship in the **AcrossAI User Access Pro** +> add-on plugin — not in this library. See "Writing a third-party provider +> add-on" below for how to add your own. ### `WpRoleProvider` filters @@ -856,56 +867,64 @@ returns true for **any** selected capability. | `wpb_access_control_wp_capability_options` | `(array $options): array` | Add or remove selectable capability options (e.g. to surface a custom cap that no role holds yet) | | `wpb_access_control_wp_capability_has_access` | `(bool $result, int $user_id, array $selected): bool` | Override the final capability-based decision | -### `BuddyBossProfileTypeProvider` +--- -Requires the [BuddyBoss Platform](https://www.buddyboss.com/platform/) plugin -to be active **and** the consumer plugin to opt in via the -`wpb_access_control_bb_profile_type_enabled` filter (default `false`). When -either condition is unmet the provider reports `is_available() === false`, -the React dropdown hides the option, and saved rules of type -`bb_profile_type` deny. +## Writing a third-party provider add-on -```php -// In your consumer plugin's bootstrap — enable the BuddyBoss provider. -add_filter( 'wpb_access_control_bb_profile_type_enabled', '__return_true' ); -``` +Anyone can ship a WordPress plugin that adds providers to every consumer of +`wpb-access-control` on the site, with no per-consumer bootstrap code. The +reference implementation is +[**AcrossAI User Access Pro**](https://github.com/acrossai-co/user-access-pro), +which packages 10 membership-plugin integrations this way. -Options are profile-type slugs (the same identifiers BuddyBoss exposes via -`bp_get_member_types()`). Both admin-created types (Profile Types CPT) and -code-registered types via `bp_register_member_type()` appear in the list. -Access is granted when the requesting user is assigned to **any** of the -selected profile types (via `bp_get_member_type()`). +The mechanism is one global filter registered by the library since v3.0.0: -| Filter | Signature | Description | -|--------|-----------|-------------| -| `wpb_access_control_bb_profile_type_enabled` | `(bool $enabled): bool` | **Opt-in gate.** Default `false`; must return `true` for the provider to fire | -| `wpb_access_control_bb_profile_type_options` | `(array $options): array` | Add or remove selectable profile-type options | -| `wpb_access_control_bb_profile_type_has_access` | `(bool $result, int $user_id, array $selected): bool` | Override the final profile-type-based decision | +``` +apply_filters( 'wpb_access_control_register_providers', $providers, $table_slug ) +``` -### `MemberPressMembershipProvider` +It fires **once per consumer instance** during `load_providers()`, before +the consumer-specific filter, and receives the running providers array plus +the consumer's `$table_slug`. Any provider you append here is visible to +that consumer's REST `/providers` endpoint and its React dropdown. -Requires the [MemberPress](https://memberpress.com/) plugin to be active -**and** the consumer plugin to opt in via the -`wpb_access_control_mepr_membership_enabled` filter (default `false`). When -either condition is unmet the provider reports `is_available() === false`, -the React dropdown hides the option, and saved rules of type -`mepr_membership` deny. +### Minimal add-on skeleton ```php -// In your consumer plugin's bootstrap — enable the MemberPress provider. -add_filter( 'wpb_access_control_mepr_membership_enabled', '__return_true' ); +table_slug + ); + + /** + * Filter the list of registered access-control providers for this + * consumer instance only. * * @since 1.0.0 * - * @param AbstractProvider[] $providers Default providers list. + * @param AbstractProvider[] $providers Providers after the global filter has run. */ $providers = (array) apply_filters( $this->providers_filter, $default_providers ); diff --git a/src/BuddyBossProfileTypeProvider.php b/src/BuddyBossProfileTypeProvider.php deleted file mode 100644 index f80c2ae..0000000 --- a/src/BuddyBossProfileTypeProvider.php +++ /dev/null @@ -1,221 +0,0 @@ - $type_object ]`. Human label lives in - * `$type_object->labels['singular_name']`. - * - `bp_get_member_type( int $user_id, false )` — return the array of - * profile-type slugs assigned to a user, or `false` when the user has - * none. Handles `$user_id = 0` gracefully. - * - * @package WPBoilerplate\AccessControl - * @since 1.4.0 - */ - -namespace WPBoilerplate\AccessControl; - -if ( ! defined( 'ABSPATH' ) ) { - exit; -} - -/** - * Provider that gates access by BuddyBoss profile type (member type). - * - * @since 1.4.0 - */ -class BuddyBossProfileTypeProvider extends AbstractProvider { - - /** - * {@inheritdoc} - * - * @since 1.4.0 - * - * @return string - */ - public function get_id(): string { - return 'bb_profile_type'; - } - - /** - * {@inheritdoc} - * - * @since 1.4.0 - * - * @return string - */ - public function get_label(): string { - return __( 'BuddyBoss Profile Type', 'wpb-access-control' ); - } - - /** - * Return whether the provider should fire for the current request. - * - * Two conditions must hold: - * 1. BuddyBoss Platform exposes its member-types API - * (`function_exists( 'bp_get_member_type'/'bp_get_member_types' )`). - * 2. The consumer plugin has opted in by hooking - * `wpb_access_control_bb_profile_type_enabled` to return `true`. - * - * The opt-in default is `false` so plugins that embed this library do - * not silently expose a BuddyBoss option in their UI just because - * BuddyBoss happens to be active on the site. - * - * The REST `/providers` endpoint forwards this flag to the React UI, - * which hides the dropdown option when false; `get_options()` and - * `user_has_access()` short-circuit on the same gate. - * - * @since 1.4.0 - * @since 1.6.0 Added the `wpb_access_control_bb_profile_type_enabled` opt-in filter. - * - * @return bool - */ - public function is_available(): bool { - if ( ! function_exists( 'bp_get_member_type' ) || ! function_exists( 'bp_get_member_types' ) ) { - return false; - } - - /** - * Filter whether the BuddyBoss Profile Type provider is enabled. - * - * Defaults to `false` — the consumer plugin must explicitly opt in - * by returning `true`. When `false`, the provider is hidden from - * the React dropdown, `get_options()` returns `[]`, and - * `user_has_access()` denies regardless of any stored rule. - * - * Example (in your plugin's bootstrap): - * - * add_filter( 'wpb_access_control_bb_profile_type_enabled', '__return_true' ); - * - * @since 1.6.0 - * - * @param bool $enabled Whether the provider should be active. Default false. - */ - return (bool) apply_filters( 'wpb_access_control_bb_profile_type_enabled', false ); - } - - /** - * Return every available BuddyBoss profile type as a selectable option. - * - * Returns an empty list when BuddyBoss is inactive so the UI surfaces an - * empty checkbox panel instead of stale slugs. Options are - * alphabetically sorted by label for a stable UI order. - * - * @since 1.4.0 - * - * @return array - */ - public function get_options(): array { - if ( ! $this->is_available() ) { - return array(); - } - - $types = \bp_get_member_types( array(), 'objects' ); - - $options = array(); - if ( is_array( $types ) ) { - foreach ( $types as $slug => $type ) { - $label = ''; - if ( is_object( $type ) && isset( $type->labels ) && is_array( $type->labels ) ) { - if ( ! empty( $type->labels['singular_name'] ) ) { - $label = (string) $type->labels['singular_name']; - } - } - if ( '' === $label ) { - $label = (string) $slug; - } - - $options[] = array( - 'id' => (string) $slug, - 'label' => $label, - ); - } - } - - usort( - $options, - static function ( array $a, array $b ): int { - return strcasecmp( $a['label'], $b['label'] ); - } - ); - - /** - * Filter the BuddyBoss profile-type options shown in the access control UI. - * - * Each entry is `[ 'id' => 'type_slug', 'label' => 'Human Name' ]`. Use this - * filter to surface programmatically-registered types that should remain - * hidden from the admin UI, or to inject a synthetic option. - * - * @since 1.4.0 - * - * @param array $options List of profile-type options. - */ - return (array) apply_filters( 'wpb_access_control_bb_profile_type_options', $options ); - } - - /** - * Return true when the user is assigned to at least one of the allowed profile types. - * - * Administrators bypass this check via AccessControlManager and will never - * reach this method. - * - * @since 1.4.0 - * - * @param int $user_id WordPress user ID. - * @param string[] $selected_options Profile-type slugs the admin has allowed. - * - * @return bool - */ - public function user_has_access( int $user_id, array $selected_options ): bool { - if ( ! $this->is_available() ) { - return false; - } - - if ( empty( $selected_options ) ) { - return false; - } - - $user_types = \bp_get_member_type( $user_id, false ); - - $result = false; - if ( is_array( $user_types ) && ! empty( $user_types ) ) { - $result = ! empty( array_intersect( $selected_options, $user_types ) ); - } - - /** - * Filter the final access decision for a BuddyBoss profile-type check. - * - * @since 1.4.0 - * - * @param bool $has_access Result before the filter. - * @param int $user_id User being checked. - * @param string[] $selected_options Allowed profile-type slugs. - */ - return (bool) apply_filters( 'wpb_access_control_bb_profile_type_has_access', $result, $user_id, $selected_options ); - } -} diff --git a/src/MemberPressMembershipProvider.php b/src/MemberPressMembershipProvider.php deleted file mode 100644 index 6369fc9..0000000 --- a/src/MemberPressMembershipProvider.php +++ /dev/null @@ -1,242 +0,0 @@ -'memberpressproduct', ...])` — list memberships. - * The CPT is registered by `MeprProductsCtrl::register_post_type()`; we use - * `get_posts()` directly instead of `MeprProduct::get_all()` so the call - * works even when MemberPress's autoloader hasn't loaded the model class. - * - `( new MeprUser( int $user_id ) )->active_product_subscriptions()` — - * returns the array of active membership post IDs the user holds; returns - * `[]` for `$user_id = 0` (unauthenticated). - * - * @package WPBoilerplate\AccessControl - * @since 1.5.0 - */ - -namespace WPBoilerplate\AccessControl; - -if ( ! defined( 'ABSPATH' ) ) { - exit; -} - -/** - * Provider that gates access by MemberPress membership. - * - * @since 1.5.0 - */ -class MemberPressMembershipProvider extends AbstractProvider { - - /** - * MemberPress CPT slug for memberships ("products"). - * - * @since 1.5.0 - * - * @var string - */ - private const POST_TYPE = 'memberpressproduct'; - - /** - * {@inheritdoc} - * - * @since 1.5.0 - * - * @return string - */ - public function get_id(): string { - return 'mepr_membership'; - } - - /** - * {@inheritdoc} - * - * @since 1.5.0 - * - * @return string - */ - public function get_label(): string { - return __( 'MemberPress Membership', 'wpb-access-control' ); - } - - /** - * Return whether the provider should fire for the current request. - * - * Two conditions must hold: - * 1. MemberPress is active (`defined( 'MEPR_VERSION' )` and - * `class_exists( 'MeprUser' )`). - * 2. The consumer plugin has opted in by hooking - * `wpb_access_control_mepr_membership_enabled` to return `true`. - * - * The opt-in default is `false` so plugins that embed this library do - * not silently expose a MemberPress option in their UI just because - * MemberPress happens to be active on the site. - * - * The REST `/providers` endpoint forwards this flag to the React UI, - * which hides the dropdown option when false; `get_options()` and - * `user_has_access()` short-circuit on the same gate. - * - * @since 1.5.0 - * @since 1.6.0 Added the `wpb_access_control_mepr_membership_enabled` opt-in filter. - * - * @return bool - */ - public function is_available(): bool { - if ( ! defined( 'MEPR_VERSION' ) || ! class_exists( 'MeprUser' ) ) { - return false; - } - - /** - * Filter whether the MemberPress Membership provider is enabled. - * - * Defaults to `false` — the consumer plugin must explicitly opt in - * by returning `true`. When `false`, the provider is hidden from - * the React dropdown, `get_options()` returns `[]`, and - * `user_has_access()` denies regardless of any stored rule. - * - * Example (in your plugin's bootstrap): - * - * add_filter( 'wpb_access_control_mepr_membership_enabled', '__return_true' ); - * - * @since 1.6.0 - * - * @param bool $enabled Whether the provider should be active. Default false. - */ - return (bool) apply_filters( 'wpb_access_control_mepr_membership_enabled', false ); - } - - /** - * Return every published MemberPress membership as a selectable option. - * - * Returns an empty list when MemberPress is inactive so the UI surfaces an - * empty checkbox panel instead of stale IDs. Memberships are ordered by - * title for a stable UI order. - * - * @since 1.5.0 - * - * @return array - */ - public function get_options(): array { - if ( ! $this->is_available() ) { - return array(); - } - - $memberships = \get_posts( - array( - 'post_type' => self::POST_TYPE, - 'post_status' => 'publish', - 'numberposts' => -1, - 'orderby' => 'title', - 'order' => 'ASC', - 'suppress_filters' => false, - ) - ); - - $options = array(); - if ( is_array( $memberships ) ) { - foreach ( $memberships as $membership ) { - if ( ! isset( $membership->ID ) ) { - continue; - } - - $title = isset( $membership->post_title ) ? (string) $membership->post_title : ''; - if ( '' === $title ) { - $title = sprintf( - /* translators: %d: membership post ID */ - __( 'Membership #%d', 'wpb-access-control' ), - (int) $membership->ID - ); - } - - $options[] = array( - 'id' => (string) (int) $membership->ID, - 'label' => $title, - ); - } - } - - /** - * Filter the MemberPress membership options shown in the access control UI. - * - * Each entry is `[ 'id' => 'membership_id', 'label' => 'Membership Title' ]`. - * Use this filter to hide or rename memberships in the admin UI. - * - * @since 1.5.0 - * - * @param array $options List of membership options. - */ - return (array) apply_filters( 'wpb_access_control_mepr_membership_options', $options ); - } - - /** - * Return true when the user has at least one of the allowed memberships. - * - * Administrators bypass this check via AccessControlManager and will never - * reach this method. - * - * @since 1.5.0 - * - * @param int $user_id WordPress user ID. - * @param string[] $selected_options Membership post IDs (as strings) the admin has allowed. - * - * @return bool - */ - public function user_has_access( int $user_id, array $selected_options ): bool { - if ( ! $this->is_available() ) { - return false; - } - - if ( empty( $selected_options ) ) { - return false; - } - - $result = false; - - // MeprUser::active_product_subscriptions() returns an array of int IDs - // (matching memberpressproduct post IDs) and `[]` for $user_id = 0. - $user = new \MeprUser( $user_id ); - $active_subscriptions = $user->active_product_subscriptions(); - - if ( is_array( $active_subscriptions ) && ! empty( $active_subscriptions ) ) { - // Normalize to strings for a strict intersect against the stored - // option list (RuleQuery stores option values as sanitised strings). - $active_ids = array_map( 'strval', $active_subscriptions ); - $result = ! empty( array_intersect( $selected_options, $active_ids ) ); - } - - /** - * Filter the final access decision for a MemberPress membership check. - * - * @since 1.5.0 - * - * @param bool $has_access Result before the filter. - * @param int $user_id User being checked. - * @param string[] $selected_options Allowed membership IDs as strings. - */ - return (bool) apply_filters( 'wpb_access_control_mepr_membership_has_access', $result, $user_id, $selected_options ); - } -} diff --git a/tests/Unit/AccessControlManagerTest.php b/tests/Unit/AccessControlManagerTest.php index bd5c16f..f21b9b9 100644 --- a/tests/Unit/AccessControlManagerTest.php +++ b/tests/Unit/AccessControlManagerTest.php @@ -153,16 +153,7 @@ public function test_load_providers_indexes_only_provider_instances_by_id(): voi ) ); - $reflection = new \ReflectionClass( AccessControlManager::class ); - $manager = $reflection->newInstanceWithoutConstructor(); - - $filter_property = $reflection->getProperty( 'providers_filter' ); - $filter_property->setAccessible( true ); - $filter_property->setValue( $manager, 'custom_provider_filter' ); - - $providers_property = $reflection->getProperty( 'providers' ); - $providers_property->setAccessible( true ); - $providers_property->setValue( $manager, array() ); + $manager = $this->make_bare_manager( 'custom_provider_filter', 'my_plugin' ); $manager->load_providers(); @@ -170,4 +161,85 @@ public function test_load_providers_indexes_only_provider_instances_by_id(): voi $this->assertSame( 'first', $manager->get_provider( 'first' )->get_id() ); $this->assertNull( $manager->get_provider( 'missing' ) ); } + + public function test_load_providers_applies_global_filter_before_consumer_filter(): void { + Functions\when( '__' )->returnArg(); + + $global = $this->provider( 'from_global', true ); + $consumer = $this->provider( 'from_consumer', true ); + + // The global filter fires first with the initial defaults + the + // consumer's table slug. It appends `from_global`. + Filters\expectApplied( 'wpb_access_control_register_providers' ) + ->once() + ->with( \Mockery::type( 'array' ), 'my_plugin' ) + ->andReturnUsing( + static function ( array $providers ) use ( $global ) { + $providers[] = $global; + return $providers; + } + ); + + // The consumer-specific filter fires next and receives whatever the + // global filter returned. It appends `from_consumer`. + Filters\expectApplied( 'my_plugin_providers' ) + ->once() + ->with( + \Mockery::on( + static function ( array $providers ) use ( $global ): bool { + // Must include the provider added by the global filter. + foreach ( $providers as $p ) { + if ( $p === $global ) { + return true; + } + } + return false; + } + ) + ) + ->andReturnUsing( + static function ( array $providers ) use ( $consumer ) { + $providers[] = $consumer; + return $providers; + } + ); + + $manager = $this->make_bare_manager( 'my_plugin_providers', 'my_plugin' ); + $manager->load_providers(); + + $ids = array_keys( $manager->get_providers() ); + // Global-added provider must appear before consumer-added one — proves + // the global filter ran first and its result flowed into the consumer filter. + $global_pos = array_search( 'from_global', $ids, true ); + $consumer_pos = array_search( 'from_consumer', $ids, true ); + + $this->assertNotFalse( $global_pos, 'Global-registered provider must be indexed' ); + $this->assertNotFalse( $consumer_pos, 'Consumer-registered provider must be indexed' ); + $this->assertLessThan( $consumer_pos, $global_pos, 'Global filter must fire before consumer filter' ); + } + + /** + * Build an AccessControlManager instance without invoking the constructor + * (which would spin up a real RuleQuery + BerlinDB table). Only the + * `providers_filter`, `table_slug`, and empty `providers` state are set, + * which is enough to exercise load_providers(). + */ + private function make_bare_manager( string $providers_filter, string $table_slug ): AccessControlManager { + $reflection = new \ReflectionClass( AccessControlManager::class ); + $manager = $reflection->newInstanceWithoutConstructor(); + + foreach ( + array( + 'providers_filter' => $providers_filter, + 'table_slug' => $table_slug, + 'providers' => array(), + ) as $property => $value + ) { + $prop = $reflection->getProperty( $property ); + $prop->setAccessible( true ); + $prop->setValue( $manager, $value ); + } + + return $manager; + } } diff --git a/tests/Unit/BuddyBossProfileTypeProviderTest.php b/tests/Unit/BuddyBossProfileTypeProviderTest.php deleted file mode 100644 index e2ad398..0000000 --- a/tests/Unit/BuddyBossProfileTypeProviderTest.php +++ /dev/null @@ -1,371 +0,0 @@ -returnArg(); - // Reset opt-in to true for every test; specific tests can flip it - // before invoking the provider to verify the default-false gate. - self::$opt_in_default = true; - Filters\expectApplied( 'wpb_access_control_bb_profile_type_enabled' ) - ->zeroOrMoreTimes() - ->andReturnUsing( static function () { - return self::$opt_in_default; - } ); - } - - protected function tearDown(): void { - Monkey\tearDown(); - parent::tearDown(); - } - - private function provider(): BuddyBossProfileTypeProvider { - return new BuddyBossProfileTypeProvider(); - } - - /** - * Return a provider whose is_available() returns the given value. - * - * Avoids monkey-patching PHP's function_exists() so we can verify the - * guard behaviour in get_options() / user_has_access() deterministically. - * - * @param bool $available - * - * @return BuddyBossProfileTypeProvider - */ - private function provider_with_availability( bool $available ): BuddyBossProfileTypeProvider { - return new class( $available ) extends BuddyBossProfileTypeProvider { - private bool $available; - public function __construct( bool $available ) { - $this->available = $available; - } - public function is_available(): bool { - return $this->available; - } - }; - } - - private function type_object( string $singular_name ): object { - $obj = new \stdClass(); - $obj->labels = array( 'singular_name' => $singular_name ); - return $obj; - } - - // ------------------------------------------------------------------------- - // Identity / metadata - // ------------------------------------------------------------------------- - - public function test_get_id_returns_bb_profile_type(): void { - $this->assertSame( 'bb_profile_type', $this->provider()->get_id() ); - } - - public function test_get_label_returns_buddyboss_profile_type_string(): void { - $this->assertSame( 'BuddyBoss Profile Type', $this->provider()->get_label() ); - } - - // ------------------------------------------------------------------------- - // is_available() - // ------------------------------------------------------------------------- - - public function test_is_available_returns_true_when_functions_exist_and_filter_opts_in(): void { - // setUp() opts the provider in (filter → true); both BuddyBoss - // function stubs are defined here to satisfy the plugin-active check. - Functions\when( 'bp_get_member_type' )->justReturn( false ); - Functions\when( 'bp_get_member_types' )->justReturn( array() ); - - $this->assertTrue( $this->provider()->is_available() ); - } - - public function test_is_available_returns_false_by_default_when_opt_in_filter_not_hooked(): void { - // Production default: filter returns false → provider is inert. - self::$opt_in_default = false; - Functions\when( 'bp_get_member_type' )->justReturn( false ); - Functions\when( 'bp_get_member_types' )->justReturn( array() ); - - $this->assertFalse( $this->provider()->is_available() ); - } - - // ------------------------------------------------------------------------- - // get_options() — availability guard - // ------------------------------------------------------------------------- - - public function test_get_options_returns_empty_array_when_buddyboss_is_inactive(): void { - // Filter must not be applied when BuddyBoss is missing — caller - // shouldn't be able to inject options into a disabled provider. - Filters\expectApplied( 'wpb_access_control_bb_profile_type_options' )->never(); - - $this->assertSame( array(), $this->provider_with_availability( false )->get_options() ); - } - - // ------------------------------------------------------------------------- - // get_options() — happy path & shape - // ------------------------------------------------------------------------- - - public function test_get_options_uses_singular_name_label_from_type_object(): void { - Functions\when( 'bp_get_member_type' )->justReturn( false ); - Functions\when( 'bp_get_member_types' )->justReturn( - array( - 'customer' => $this->type_object( 'Customer' ), - ) - ); - Filters\expectApplied( 'wpb_access_control_bb_profile_type_options' )->once()->andReturnFirstArg(); - - $this->assertSame( - array( array( 'id' => 'customer', 'label' => 'Customer' ) ), - $this->provider()->get_options() - ); - } - - public function test_get_options_falls_back_to_slug_when_singular_name_missing(): void { - Functions\when( 'bp_get_member_type' )->justReturn( false ); - $broken_a = new \stdClass(); - $broken_a->labels = array(); - $broken_b = new \stdClass(); // no labels property at all - Functions\when( 'bp_get_member_types' )->justReturn( - array( - 'vendor' => $broken_a, - 'partner' => $broken_b, - ) - ); - Filters\expectApplied( 'wpb_access_control_bb_profile_type_options' )->andReturnFirstArg(); - - $options = $this->provider()->get_options(); - - $ids = array_column( $options, 'id' ); - $labels = array_column( $options, 'label' ); - - // Both fall back to the slug; sort order is by label (case-insensitive) - // → 'partner' < 'vendor'. - $this->assertSame( array( 'partner', 'vendor' ), $ids ); - $this->assertSame( array( 'partner', 'vendor' ), $labels ); - } - - public function test_get_options_falls_back_to_slug_when_singular_name_is_empty_string(): void { - Functions\when( 'bp_get_member_type' )->justReturn( false ); - $type = new \stdClass(); - $type->labels = array( 'singular_name' => '' ); - Functions\when( 'bp_get_member_types' )->justReturn( array( 'vendor' => $type ) ); - Filters\expectApplied( 'wpb_access_control_bb_profile_type_options' )->andReturnFirstArg(); - - $this->assertSame( - array( array( 'id' => 'vendor', 'label' => 'vendor' ) ), - $this->provider()->get_options() - ); - } - - public function test_get_options_sorts_alphabetically_case_insensitive_by_label(): void { - Functions\when( 'bp_get_member_type' )->justReturn( false ); - Functions\when( 'bp_get_member_types' )->justReturn( - array( - 'zeta' => $this->type_object( 'zeta' ), - 'alpha' => $this->type_object( 'Alpha' ), - 'mu' => $this->type_object( 'mu' ), - ) - ); - Filters\expectApplied( 'wpb_access_control_bb_profile_type_options' )->andReturnFirstArg(); - - $labels = array_column( $this->provider()->get_options(), 'label' ); - - $this->assertSame( array( 'Alpha', 'mu', 'zeta' ), $labels ); - } - - public function test_get_options_returns_empty_when_bp_get_member_types_returns_non_array(): void { - Functions\when( 'bp_get_member_type' )->justReturn( false ); - Functions\when( 'bp_get_member_types' )->justReturn( null ); - Filters\expectApplied( 'wpb_access_control_bb_profile_type_options' )->once()->andReturn( array() ); - - $this->assertSame( array(), $this->provider()->get_options() ); - } - - public function test_get_options_filter_can_replace_the_entire_list(): void { - Functions\when( 'bp_get_member_type' )->justReturn( false ); - Functions\when( 'bp_get_member_types' )->justReturn( - array( 'customer' => $this->type_object( 'Customer' ) ) - ); - Filters\expectApplied( 'wpb_access_control_bb_profile_type_options' ) - ->once() - ->andReturn( array( array( 'id' => 'override', 'label' => 'Override' ) ) ); - - $this->assertSame( - array( array( 'id' => 'override', 'label' => 'Override' ) ), - $this->provider()->get_options() - ); - } - - public function test_get_options_casts_non_array_filter_return_back_to_array(): void { - Functions\when( 'bp_get_member_type' )->justReturn( false ); - Functions\when( 'bp_get_member_types' )->justReturn( array() ); - Filters\expectApplied( 'wpb_access_control_bb_profile_type_options' )->andReturn( null ); - - $this->assertSame( array(), $this->provider()->get_options() ); - } - - // ------------------------------------------------------------------------- - // user_has_access() — availability guard - // ------------------------------------------------------------------------- - - public function test_user_has_access_returns_false_when_buddyboss_is_inactive(): void { - // bp_get_member_type must NOT be called when the plugin is missing. - Filters\expectApplied( 'wpb_access_control_bb_profile_type_has_access' )->never(); - - $this->assertFalse( - $this->provider_with_availability( false )->user_has_access( 7, array( 'customer' ) ) - ); - } - - // ------------------------------------------------------------------------- - // user_has_access() — empty options short-circuit - // ------------------------------------------------------------------------- - - public function test_user_has_access_returns_false_when_selected_options_empty(): void { - Functions\when( 'bp_get_member_type' )->justReturn( false ); - Functions\when( 'bp_get_member_types' )->justReturn( array() ); - Filters\expectApplied( 'wpb_access_control_bb_profile_type_has_access' )->never(); - - $this->assertFalse( $this->provider()->user_has_access( 7, array() ) ); - } - - // ------------------------------------------------------------------------- - // user_has_access() — no user types (BuddyBoss returns false) - // ------------------------------------------------------------------------- - - public function test_user_has_access_returns_false_when_user_has_no_profile_types(): void { - Functions\when( 'bp_get_member_types' )->justReturn( array() ); - Functions\expect( 'bp_get_member_type' )->once()->with( 7, false )->andReturn( false ); - Filters\expectApplied( 'wpb_access_control_bb_profile_type_has_access' ) - ->once() - ->with( false, 7, array( 'customer' ) ) - ->andReturn( false ); - - $this->assertFalse( $this->provider()->user_has_access( 7, array( 'customer' ) ) ); - } - - public function test_user_has_access_returns_false_when_user_types_is_empty_array(): void { - Functions\when( 'bp_get_member_types' )->justReturn( array() ); - Functions\expect( 'bp_get_member_type' )->once()->with( 7, false )->andReturn( array() ); - Filters\expectApplied( 'wpb_access_control_bb_profile_type_has_access' ) - ->once() - ->with( false, 7, array( 'customer' ) ) - ->andReturn( false ); - - $this->assertFalse( $this->provider()->user_has_access( 7, array( 'customer' ) ) ); - } - - // ------------------------------------------------------------------------- - // user_has_access() — intersect match (true cases) - // ------------------------------------------------------------------------- - - public function test_user_has_access_returns_true_when_user_type_matches_single_selected(): void { - Functions\when( 'bp_get_member_types' )->justReturn( array() ); - Functions\expect( 'bp_get_member_type' )->once()->with( 7, false )->andReturn( array( 'customer' ) ); - Filters\expectApplied( 'wpb_access_control_bb_profile_type_has_access' ) - ->once() - ->with( true, 7, array( 'customer' ) ) - ->andReturn( true ); - - $this->assertTrue( $this->provider()->user_has_access( 7, array( 'customer' ) ) ); - } - - public function test_user_has_access_returns_true_when_one_of_multiple_user_types_matches(): void { - Functions\when( 'bp_get_member_types' )->justReturn( array() ); - Functions\expect( 'bp_get_member_type' )->once()->with( 7, false )->andReturn( array( 'subscriber', 'vendor' ) ); - Filters\expectApplied( 'wpb_access_control_bb_profile_type_has_access' ) - ->once() - ->with( true, 7, array( 'customer', 'vendor' ) ) - ->andReturn( true ); - - $this->assertTrue( $this->provider()->user_has_access( 7, array( 'customer', 'vendor' ) ) ); - } - - // ------------------------------------------------------------------------- - // user_has_access() — no intersection (filter-driven cases) - // ------------------------------------------------------------------------- - - public function test_user_has_access_returns_false_when_no_user_type_matches_and_filter_keeps_false(): void { - Functions\when( 'bp_get_member_types' )->justReturn( array() ); - Functions\expect( 'bp_get_member_type' )->once()->with( 7, false )->andReturn( array( 'subscriber' ) ); - Filters\expectApplied( 'wpb_access_control_bb_profile_type_has_access' ) - ->once() - ->with( false, 7, array( 'customer', 'vendor' ) ) - ->andReturn( false ); - - $this->assertFalse( $this->provider()->user_has_access( 7, array( 'customer', 'vendor' ) ) ); - } - - public function test_user_has_access_filter_can_override_false_to_true(): void { - Functions\when( 'bp_get_member_types' )->justReturn( array() ); - Functions\expect( 'bp_get_member_type' )->once()->with( 7, false )->andReturn( false ); - Filters\expectApplied( 'wpb_access_control_bb_profile_type_has_access' ) - ->once() - ->with( false, 7, array( 'customer' ) ) - ->andReturn( true ); - - $this->assertTrue( $this->provider()->user_has_access( 7, array( 'customer' ) ) ); - } - - public function test_user_has_access_filter_truthy_value_is_cast_to_bool(): void { - Functions\when( 'bp_get_member_types' )->justReturn( array() ); - Functions\expect( 'bp_get_member_type' )->once()->andReturn( false ); - Filters\expectApplied( 'wpb_access_control_bb_profile_type_has_access' ) - ->once() - ->andReturn( 1 ); - - $this->assertTrue( $this->provider()->user_has_access( 1, array( 'customer' ) ) ); - } - - public function test_user_has_access_filter_falsy_value_is_cast_to_bool(): void { - Functions\when( 'bp_get_member_types' )->justReturn( array() ); - Functions\expect( 'bp_get_member_type' )->once()->andReturn( array( 'customer' ) ); - Filters\expectApplied( 'wpb_access_control_bb_profile_type_has_access' ) - ->once() - ->andReturn( '' ); - - $this->assertFalse( $this->provider()->user_has_access( 1, array( 'customer' ) ) ); - } - - public function test_user_has_access_uses_strict_intersect_comparison(): void { - Functions\when( 'bp_get_member_types' )->justReturn( array() ); - Functions\expect( 'bp_get_member_type' )->once()->with( 7, false )->andReturn( array( 'Customer' ) ); - Filters\expectApplied( 'wpb_access_control_bb_profile_type_has_access' ) - ->once() - ->andReturn( false ); - - // Profile-type slugs are case-sensitive — 'Customer' should not match 'customer'. - $this->assertFalse( $this->provider()->user_has_access( 7, array( 'customer' ) ) ); - } -} diff --git a/tests/Unit/MemberPressMembershipProviderTest.php b/tests/Unit/MemberPressMembershipProviderTest.php deleted file mode 100644 index 3854a16..0000000 --- a/tests/Unit/MemberPressMembershipProviderTest.php +++ /dev/null @@ -1,340 +0,0 @@ -returnArg(); - // Reset opt-in to true for every test; specific tests can flip it - // before invoking the provider to verify the default-false gate. - self::$opt_in_default = true; - Filters\expectApplied( 'wpb_access_control_mepr_membership_enabled' ) - ->zeroOrMoreTimes() - ->andReturnUsing( static function () { - return self::$opt_in_default; - } ); - \MeprUser::$next_subscriptions = array(); - } - - protected function tearDown(): void { - \MeprUser::$next_subscriptions = array(); - Monkey\tearDown(); - parent::tearDown(); - } - - private function provider(): MemberPressMembershipProvider { - return new MemberPressMembershipProvider(); - } - - /** - * Return a provider whose is_available() is forced to the given value. - * - * Avoids un-defining MEPR_VERSION / MeprUser (which PHP can't undo) when - * testing the unavailability short-circuits. - */ - private function provider_with_availability( bool $available ): MemberPressMembershipProvider { - return new class( $available ) extends MemberPressMembershipProvider { - private bool $available; - public function __construct( bool $available ) { - $this->available = $available; - } - public function is_available(): bool { - return $this->available; - } - }; - } - - private function post( int $id, string $title ): object { - $post = new \stdClass(); - $post->ID = $id; - $post->post_title = $title; - return $post; - } - - // ------------------------------------------------------------------------- - // Identity / metadata - // ------------------------------------------------------------------------- - - public function test_get_id_returns_mepr_membership(): void { - $this->assertSame( 'mepr_membership', $this->provider()->get_id() ); - } - - public function test_get_label_returns_memberpress_membership_string(): void { - $this->assertSame( 'MemberPress Membership', $this->provider()->get_label() ); - } - - // ------------------------------------------------------------------------- - // is_available() - // ------------------------------------------------------------------------- - - public function test_is_available_returns_true_when_plugin_present_and_filter_opts_in(): void { - // setUp() opts the provider in (filter → true); MEPR_VERSION and - // MeprUser are declared globally in tests/bootstrap.php. - $this->assertTrue( $this->provider()->is_available() ); - } - - public function test_is_available_returns_false_by_default_when_opt_in_filter_not_hooked(): void { - // Production default: filter returns false → provider is inert. - self::$opt_in_default = false; - - $this->assertFalse( $this->provider()->is_available() ); - } - - // ------------------------------------------------------------------------- - // get_options() — availability guard - // ------------------------------------------------------------------------- - - public function test_get_options_returns_empty_array_when_memberpress_is_inactive(): void { - // get_posts must NOT be called when the plugin is missing. - Functions\expect( 'get_posts' )->never(); - Filters\expectApplied( 'wpb_access_control_mepr_membership_options' )->never(); - - $this->assertSame( array(), $this->provider_with_availability( false )->get_options() ); - } - - // ------------------------------------------------------------------------- - // get_options() — happy path & shape - // ------------------------------------------------------------------------- - - public function test_get_options_maps_each_membership_post_to_id_label_pair(): void { - Functions\expect( 'get_posts' ) - ->once() - ->with( - \Mockery::on( - static function ( $args ) { - return is_array( $args ) - && 'memberpressproduct' === ( $args['post_type'] ?? null ) - && 'publish' === ( $args['post_status'] ?? null ); - } - ) - ) - ->andReturn( - array( - $this->post( 42, 'Gold' ), - $this->post( 100, 'Silver' ), - ) - ); - Filters\expectApplied( 'wpb_access_control_mepr_membership_options' )->once()->andReturnFirstArg(); - - $this->assertSame( - array( - array( 'id' => '42', 'label' => 'Gold' ), - array( 'id' => '100', 'label' => 'Silver' ), - ), - $this->provider()->get_options() - ); - } - - public function test_get_options_falls_back_to_synthetic_title_when_post_title_empty(): void { - Functions\expect( 'get_posts' )->once()->andReturn( - array( $this->post( 7, '' ) ) - ); - Filters\expectApplied( 'wpb_access_control_mepr_membership_options' )->andReturnFirstArg(); - - $options = $this->provider()->get_options(); - - $this->assertSame( '7', $options[0]['id'] ); - $this->assertSame( 'Membership #7', $options[0]['label'] ); - } - - public function test_get_options_skips_posts_missing_id_field(): void { - $broken = new \stdClass(); // no ID property - Functions\expect( 'get_posts' )->once()->andReturn( - array( $broken, $this->post( 9, 'Bronze' ) ) - ); - Filters\expectApplied( 'wpb_access_control_mepr_membership_options' )->andReturnFirstArg(); - - $this->assertSame( - array( array( 'id' => '9', 'label' => 'Bronze' ) ), - $this->provider()->get_options() - ); - } - - public function test_get_options_returns_empty_when_get_posts_returns_non_array(): void { - Functions\expect( 'get_posts' )->once()->andReturn( null ); - Filters\expectApplied( 'wpb_access_control_mepr_membership_options' )->once()->andReturn( array() ); - - $this->assertSame( array(), $this->provider()->get_options() ); - } - - public function test_get_options_filter_can_replace_the_entire_list(): void { - Functions\expect( 'get_posts' )->once()->andReturn( - array( $this->post( 1, 'Gold' ) ) - ); - Filters\expectApplied( 'wpb_access_control_mepr_membership_options' ) - ->once() - ->andReturn( array( array( 'id' => 'override', 'label' => 'Override' ) ) ); - - $this->assertSame( - array( array( 'id' => 'override', 'label' => 'Override' ) ), - $this->provider()->get_options() - ); - } - - public function test_get_options_casts_non_array_filter_return_back_to_array(): void { - Functions\expect( 'get_posts' )->once()->andReturn( array() ); - Filters\expectApplied( 'wpb_access_control_mepr_membership_options' )->andReturn( null ); - - $this->assertSame( array(), $this->provider()->get_options() ); - } - - // ------------------------------------------------------------------------- - // user_has_access() — availability guard - // ------------------------------------------------------------------------- - - public function test_user_has_access_returns_false_when_memberpress_is_inactive(): void { - // MeprUser must NOT be touched when the plugin is missing. - Filters\expectApplied( 'wpb_access_control_mepr_membership_has_access' )->never(); - \MeprUser::$next_subscriptions = array( 999 ); // sentinel: would match if called - - $this->assertFalse( - $this->provider_with_availability( false )->user_has_access( 7, array( '999' ) ) - ); - } - - // ------------------------------------------------------------------------- - // user_has_access() — empty options short-circuit - // ------------------------------------------------------------------------- - - public function test_user_has_access_returns_false_when_selected_options_empty(): void { - \MeprUser::$next_subscriptions = array( 42 ); - Filters\expectApplied( 'wpb_access_control_mepr_membership_has_access' )->never(); - - $this->assertFalse( $this->provider()->user_has_access( 7, array() ) ); - } - - // ------------------------------------------------------------------------- - // user_has_access() — no active subscriptions - // ------------------------------------------------------------------------- - - public function test_user_has_access_returns_false_when_user_has_no_active_subscriptions(): void { - \MeprUser::$next_subscriptions = array(); - Filters\expectApplied( 'wpb_access_control_mepr_membership_has_access' ) - ->once() - ->with( false, 7, array( '42' ) ) - ->andReturn( false ); - - $this->assertFalse( $this->provider()->user_has_access( 7, array( '42' ) ) ); - } - - // ------------------------------------------------------------------------- - // user_has_access() — intersect match (true cases) - // ------------------------------------------------------------------------- - - public function test_user_has_access_returns_true_when_active_subscription_matches_single_selected(): void { - \MeprUser::$next_subscriptions = array( 42 ); - Filters\expectApplied( 'wpb_access_control_mepr_membership_has_access' ) - ->once() - ->with( true, 7, array( '42' ) ) - ->andReturn( true ); - - $this->assertTrue( $this->provider()->user_has_access( 7, array( '42' ) ) ); - } - - public function test_user_has_access_returns_true_when_one_of_multiple_user_memberships_matches(): void { - \MeprUser::$next_subscriptions = array( 13, 42 ); - Filters\expectApplied( 'wpb_access_control_mepr_membership_has_access' ) - ->once() - ->with( true, 7, array( '42', '100' ) ) - ->andReturn( true ); - - $this->assertTrue( $this->provider()->user_has_access( 7, array( '42', '100' ) ) ); - } - - public function test_user_has_access_coerces_int_subscription_ids_to_strings_for_intersect(): void { - // MeprUser returns ints; stored options are strings — provider must - // coerce or the strict intersect will miss. - \MeprUser::$next_subscriptions = array( 42 ); - Filters\expectApplied( 'wpb_access_control_mepr_membership_has_access' ) - ->once() - ->with( true, 7, array( '42' ) ) - ->andReturn( true ); - - $this->assertTrue( $this->provider()->user_has_access( 7, array( '42' ) ) ); - } - - // ------------------------------------------------------------------------- - // user_has_access() — no intersection (filter-driven cases) - // ------------------------------------------------------------------------- - - public function test_user_has_access_returns_false_when_no_membership_matches_and_filter_keeps_false(): void { - \MeprUser::$next_subscriptions = array( 13 ); - Filters\expectApplied( 'wpb_access_control_mepr_membership_has_access' ) - ->once() - ->with( false, 7, array( '42', '100' ) ) - ->andReturn( false ); - - $this->assertFalse( $this->provider()->user_has_access( 7, array( '42', '100' ) ) ); - } - - public function test_user_has_access_filter_can_override_false_to_true(): void { - \MeprUser::$next_subscriptions = array(); - Filters\expectApplied( 'wpb_access_control_mepr_membership_has_access' ) - ->once() - ->with( false, 7, array( '42' ) ) - ->andReturn( true ); - - $this->assertTrue( $this->provider()->user_has_access( 7, array( '42' ) ) ); - } - - public function test_user_has_access_filter_truthy_value_is_cast_to_bool(): void { - \MeprUser::$next_subscriptions = array(); - Filters\expectApplied( 'wpb_access_control_mepr_membership_has_access' ) - ->once() - ->andReturn( 1 ); - - $this->assertTrue( $this->provider()->user_has_access( 1, array( '42' ) ) ); - } - - public function test_user_has_access_filter_falsy_value_is_cast_to_bool(): void { - \MeprUser::$next_subscriptions = array( 42 ); - Filters\expectApplied( 'wpb_access_control_mepr_membership_has_access' ) - ->once() - ->andReturn( '' ); - - $this->assertFalse( $this->provider()->user_has_access( 1, array( '42' ) ) ); - } -} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 7294be6..24b00d5 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -51,36 +51,3 @@ function sanitize_textarea_field( $str ) { return trim( $str ); } } - -// --------------------------------------------------------------------------- -// MemberPress test stubs — required by MemberPressMembershipProviderTest. -// -// PHP constants and class declarations persist for the lifetime of the -// process, so they belong here (loaded once) rather than inside any test -// file's namespace block. -// --------------------------------------------------------------------------- -if ( ! defined( 'MEPR_VERSION' ) ) { - define( 'MEPR_VERSION', '1.0.0-test' ); -} -if ( ! class_exists( 'MeprUser' ) ) { - /** - * Minimal stub of MemberPress's MeprUser model. - * - * Tests set `MeprUser::$next_subscriptions` before invoking - * `user_has_access()` to drive `active_product_subscriptions()`. - */ - class MeprUser { - /** @var array */ - public static $next_subscriptions = array(); - - public $ID = 0; - - public function __construct( $user_id = 0 ) { - $this->ID = (int) $user_id; - } - - public function active_product_subscriptions(): array { - return self::$next_subscriptions; - } - } -}