From 462141f3813f2dbd2afdedb41fd4feabc90d3387 Mon Sep 17 00:00:00 2001 From: raftaar1191 Date: Mon, 3 Aug 2026 17:41:07 +0530 Subject: [PATCH] feat(providers)!: extract built-in integrations to add-on, add register_providers global filter (v3.0.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING. Removes BuddyBossProfileTypeProvider (bb_profile_type, since v1.4.0) and MemberPressMembershipProvider (mepr_membership, since v1.5.0) along with their v1.6.0 opt-in filters and the tests/bootstrap.php stubs that supported them. Both providers — plus eight new integrations (LearnDash, LifterLMS, Paid Memberships Pro, Restrict Content Pro, WooCommerce Memberships, s2Member, Wishlist Member, Memberium) — now ship in the AcrossAI User Access Pro add-on plugin at github.com/acrossai-co/user-access-pro, using fresh uap_* IDs. Adds a new global filter in AccessControlManager::load_providers(): apply_filters( 'wpb_access_control_register_providers', \$providers, \$table_slug ) It fires before the existing per-consumer filter with the running providers array and the consumer's table slug. Add-on plugins hook this once and their providers appear in every consumer of the library — no per-consumer bootstrap code needed. Consumers retain final say via their own per-instance filter. README documents the extension point in a new "Writing a third-party provider add-on" section, and CHANGELOG covers the removals, additions, and migration steps for any consumer that had enabled the old providers via the v1.6.0 gates. Test suite drops from 295 to 147 (10 provider suites removed, 1 new global- filter test added). All green. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 38 ++ README.md | 105 +++-- src/AccessControlManager.php | 29 +- src/BuddyBossProfileTypeProvider.php | 221 ----------- src/MemberPressMembershipProvider.php | 242 ------------ tests/Unit/AccessControlManagerTest.php | 92 ++++- .../Unit/BuddyBossProfileTypeProviderTest.php | 371 ------------------ .../MemberPressMembershipProviderTest.php | 340 ---------------- tests/bootstrap.php | 33 -- 9 files changed, 207 insertions(+), 1264 deletions(-) delete mode 100644 src/BuddyBossProfileTypeProvider.php delete mode 100644 src/MemberPressMembershipProvider.php delete mode 100644 tests/Unit/BuddyBossProfileTypeProviderTest.php delete mode 100644 tests/Unit/MemberPressMembershipProviderTest.php 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; - } - } -}