Commit c72cecb0347 for woocommerce
commit c72cecb03478d6d58396c04b64afffa753886ea7
Author: Hannah Tinkler <hannah.tinkler@gmail.com>
Date: Wed Mar 25 15:13:08 2026 +0000
Adds supporting methods for the (forthcoming) notification processor. (#63748)
* Add meta dedup, get_tokens_for_roles, and notification abstractions.
- Add abstract has_meta/write_meta to Notification base class, with
implementations on NewOrderNotification and NewReviewNotification.
- Add from_array() static factory using NOTIFICATION_CLASSES registry.
- Add PushTokensDataStore::get_tokens_for_roles() for role-based token
queries with meta cache priming.
- Remove response check from InternalNotificationDispatcher (non-blocking
request makes response unobservable; safety net covers failures).
- Add StubOrderNotification/StubReviewNotification test helpers.
* Narrows catch args, updates tests.
* Resolves linting errors.
* Updates meta checks to use exists methods.
* Updates function calls to use legacy wrapper.
* Improve test stubs and dispatcher test assertions.
- Stub notifications now track meta writes in memory so has_meta
reflects write_meta calls, matching real notification behavior.
- Assert timeout and Content-Type in dispatcher request shape test.
* Updates some code comments for clarity.
* Move NOTIFICATION_CLASSES to Notification and derive type from map.
- Move NOTIFICATION_CLASSES from PushNotifications to Notification as a type-to-class map.
- Derive notification type in the constructor via is_a() lookup, removing the need for subclasses to implement get_type().
- Replace from_array() iteration with direct map lookup.
* Remove get_type() in favour of protected $type property.
- Make $type protected so subclasses access it directly.
- Remove public get_type() method from Notification.
- Replace external get_type() call in dispatcher log with $notification::class.
* Add const TYPE and abstract get_type() to Notification.
- Add const TYPE on each notification subclass as source of truth for the type string.
- Add abstract get_type() on Notification, subclasses return self::TYPE.
- Remove protected $type property and constructor map lookup.
- Simplify constructor to just resource_id validation.
* Actions PR feedback re. get_type usage and tests.
diff --git a/plugins/woocommerce/src/Internal/PushNotifications/DataStores/PushTokensDataStore.php b/plugins/woocommerce/src/Internal/PushNotifications/DataStores/PushTokensDataStore.php
index 85b38b48736..7489f849aed 100644
--- a/plugins/woocommerce/src/Internal/PushNotifications/DataStores/PushTokensDataStore.php
+++ b/plugins/woocommerce/src/Internal/PushNotifications/DataStores/PushTokensDataStore.php
@@ -302,6 +302,73 @@ class PushTokensDataStore {
return null;
}
+ /**
+ * Returns all push tokens belonging to users with the given roles.
+ *
+ * @param string[] $roles The roles to query tokens for.
+ * @return PushToken[]
+ *
+ * @since 10.7.0
+ */
+ public function get_tokens_for_roles( array $roles ): array {
+ if ( empty( $roles ) ) {
+ return array();
+ }
+
+ $user_ids = get_users(
+ array(
+ 'role__in' => $roles,
+ 'fields' => 'ID',
+ )
+ );
+
+ if ( empty( $user_ids ) ) {
+ return array();
+ }
+
+ $query = new WP_Query(
+ array(
+ 'post_type' => PushToken::POST_TYPE,
+ 'post_status' => 'private',
+ 'author__in' => $user_ids,
+ 'posts_per_page' => -1,
+ 'fields' => 'ids',
+ )
+ );
+
+ /**
+ * Typehint for PHPStan, specifies these are IDs and not instances of
+ * WP_Post.
+ *
+ * @var int[] $post_ids
+ */
+ $post_ids = $query->posts;
+
+ if ( empty( $post_ids ) ) {
+ return array();
+ }
+
+ update_meta_cache( 'post', $post_ids );
+
+ $tokens = array();
+
+ foreach ( $post_ids as $post_id ) {
+ try {
+ $tokens[] = $this->read( (int) $post_id );
+ } catch ( WC_Data_Exception $e ) {
+ wc_get_logger()->warning(
+ 'Skipping malformed push token during role-based query.',
+ array(
+ 'token_id' => $post_id,
+ 'error' => $e->getMessage(),
+ )
+ );
+ }
+ }
+
+ return $tokens;
+ }
+
/**
* Returns an associative array of post meta as key => value pairs for the
* keys defined in SUPPORTED_META; missing keys return null. Use
diff --git a/plugins/woocommerce/src/Internal/PushNotifications/Dispatchers/InternalNotificationDispatcher.php b/plugins/woocommerce/src/Internal/PushNotifications/Dispatchers/InternalNotificationDispatcher.php
index 4642c5434d6..5cec21c20f9 100644
--- a/plugins/woocommerce/src/Internal/PushNotifications/Dispatchers/InternalNotificationDispatcher.php
+++ b/plugins/woocommerce/src/Internal/PushNotifications/Dispatchers/InternalNotificationDispatcher.php
@@ -65,7 +65,12 @@ class InternalNotificationDispatcher {
wp_salt( 'auth' )
);
- $response = wp_remote_post(
+ /**
+ * The request is non-blocking so the response is not handled anywhere.
+ * If the request fails, the ActionScheduler safety net will pick up
+ * unsent notifications after 60 seconds.
+ */
+ wp_remote_post(
rest_url( self::SEND_ENDPOINT ),
array(
'blocking' => false,
@@ -77,12 +82,5 @@ class InternalNotificationDispatcher {
'body' => $body,
)
);
-
- if ( is_wp_error( $response ) ) {
- wc_get_logger()->error(
- sprintf( 'Loopback dispatch failed: %s', $response->get_error_message() ),
- array( 'source' => PushNotifications::FEATURE_NAME )
- );
- }
}
}
diff --git a/plugins/woocommerce/src/Internal/PushNotifications/Notifications/NewOrderNotification.php b/plugins/woocommerce/src/Internal/PushNotifications/Notifications/NewOrderNotification.php
index 850b381f61e..dc98e28f320 100644
--- a/plugins/woocommerce/src/Internal/PushNotifications/Notifications/NewOrderNotification.php
+++ b/plugins/woocommerce/src/Internal/PushNotifications/Notifications/NewOrderNotification.php
@@ -15,8 +15,7 @@ defined( 'ABSPATH' ) || exit;
*/
class NewOrderNotification extends Notification {
/**
- * The notification type identifier, this should match the subtype or type
- * (if there isn't a subtype) values attributed to notes in WordPress.com.
+ * The notification type identifier for new orders.
*/
const TYPE = 'store_order';
@@ -31,14 +30,10 @@ class NewOrderNotification extends Notification {
const EMOJI_LIST = array( '🎉', '🎊', '🥳', '👏', '🙌' );
/**
- * Creates a new order notification.
- *
- * @param int $order_id The order ID.
- *
- * @since 10.7.0
+ * {@inheritDoc}
*/
- public function __construct( int $order_id ) {
- parent::__construct( self::TYPE, $order_id );
+ public function get_type(): string {
+ return self::TYPE;
}
/**
@@ -58,7 +53,7 @@ class NewOrderNotification extends Notification {
}
return array(
- 'type' => self::TYPE,
+ 'type' => $this->get_type(),
'icon' => self::ICON,
// This represents the time the notification was triggered, so we can monitor age of notification at delivery.
'timestamp' => gmdate( 'c' ),
@@ -87,4 +82,28 @@ class NewOrderNotification extends Notification {
),
);
}
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param string $key The meta key.
+ */
+ public function has_meta( string $key ): bool {
+ $order = WC()->call_function( 'wc_get_order', $this->get_resource_id() );
+ return $order instanceof WC_Order && $order->meta_exists( $key );
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param string $key The meta key.
+ */
+ public function write_meta( string $key ): void {
+ $order = WC()->call_function( 'wc_get_order', $this->get_resource_id() );
+
+ if ( $order instanceof WC_Order ) {
+ $order->update_meta_data( $key, (string) time() );
+ $order->save_meta_data();
+ }
+ }
}
diff --git a/plugins/woocommerce/src/Internal/PushNotifications/Notifications/NewReviewNotification.php b/plugins/woocommerce/src/Internal/PushNotifications/Notifications/NewReviewNotification.php
index 511c0c19762..0b94b3e65cc 100644
--- a/plugins/woocommerce/src/Internal/PushNotifications/Notifications/NewReviewNotification.php
+++ b/plugins/woocommerce/src/Internal/PushNotifications/Notifications/NewReviewNotification.php
@@ -15,19 +15,15 @@ defined( 'ABSPATH' ) || exit;
*/
class NewReviewNotification extends Notification {
/**
- * The notification type identifier.
+ * The notification type identifier for new reviews.
*/
const TYPE = 'store_review';
/**
- * Creates a new review notification.
- *
- * @param int $comment_id The comment ID.
- *
- * @since 10.7.0
+ * {@inheritDoc}
*/
- public function __construct( int $comment_id ) {
- parent::__construct( self::TYPE, $comment_id );
+ public function get_type(): string {
+ return self::TYPE;
}
/**
@@ -47,7 +43,7 @@ class NewReviewNotification extends Notification {
}
return array(
- 'type' => self::TYPE,
+ 'type' => $this->get_type(),
// This represents the time the notification was triggered, so we can monitor age of notification at delivery.
'timestamp' => gmdate( 'c' ),
'resource_id' => $this->get_resource_id(),
@@ -72,4 +68,22 @@ class NewReviewNotification extends Notification {
),
);
}
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param string $key The meta key.
+ */
+ public function has_meta( string $key ): bool {
+ return WC()->call_function( 'metadata_exists', 'comment', $this->get_resource_id(), $key );
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param string $key The meta key.
+ */
+ public function write_meta( string $key ): void {
+ WC()->call_function( 'update_comment_meta', $this->get_resource_id(), $key, (string) time() );
+ }
}
diff --git a/plugins/woocommerce/src/Internal/PushNotifications/Notifications/Notification.php b/plugins/woocommerce/src/Internal/PushNotifications/Notifications/Notification.php
index 655b431d1dc..b8082737e10 100644
--- a/plugins/woocommerce/src/Internal/PushNotifications/Notifications/Notification.php
+++ b/plugins/woocommerce/src/Internal/PushNotifications/Notifications/Notification.php
@@ -18,11 +18,14 @@ defined( 'ABSPATH' ) || exit;
*/
abstract class Notification {
/**
- * The notification type.
+ * Map of notification type identifiers to their corresponding subclass.
*
- * @var string
+ * @var array<string, class-string<Notification>>
*/
- private string $type;
+ const NOTIFICATION_CLASSES = array(
+ 'store_order' => NewOrderNotification::class,
+ 'store_review' => NewReviewNotification::class,
+ );
/**
* The ID of the resource this notification is about (e.g. order ID, comment
@@ -35,26 +38,31 @@ abstract class Notification {
/**
* Creates a new Notification instance.
*
- * @param string $type The notification type.
- * @param int $resource_id The resource ID.
+ * @param int $resource_id The resource ID.
*
- * @throws InvalidArgumentException If any argument is invalid.
+ * @throws InvalidArgumentException If the resource ID is invalid.
*
* @since 10.7.0
*/
- public function __construct( string $type, int $resource_id ) {
- if ( '' === trim( $type ) ) {
- throw new InvalidArgumentException( 'Notification type must not be empty.' );
- }
-
+ public function __construct( int $resource_id ) {
if ( $resource_id <= 0 ) {
throw new InvalidArgumentException( 'Notification resource_id must be positive.' );
}
- $this->type = trim( $type );
$this->resource_id = $resource_id;
}
+ /**
+ * Returns the notification type identifier, this should match the subtype
+ * or type (if there isn't a subtype) values attributed to notes in
+ * WordPress.com.
+ *
+ * @return string
+ *
+ * @since 10.7.0
+ */
+ abstract public function get_type(): string;
+
/**
* Returns the WPCOM-ready payload for this notification.
*
@@ -66,6 +74,26 @@ abstract class Notification {
*/
abstract public function to_payload(): ?array;
+ /**
+ * Checks whether a meta key exists for this notification's resource.
+ *
+ * @param string $key The meta key.
+ * @return bool
+ *
+ * @since 10.7.0
+ */
+ abstract public function has_meta( string $key ): bool;
+
+ /**
+ * Writes a meta key with a timestamp to this notification's resource.
+ *
+ * @param string $key The meta key.
+ * @return void
+ *
+ * @since 10.7.0
+ */
+ abstract public function write_meta( string $key ): void;
+
/**
* Returns the notification data as an array.
*
@@ -75,32 +103,45 @@ abstract class Notification {
*/
public function to_array(): array {
return array(
- 'type' => $this->type,
+ 'type' => $this->get_type(),
'resource_id' => $this->resource_id,
);
}
/**
- * Returns a unique identifier for this notification, used for
- * deduplication.
+ * Reconstructs a Notification subclass from a serialized array.
*
- * @return string
+ * @param array{type: string, resource_id: int} $data The notification data.
+ * @return self
+ *
+ * @throws InvalidArgumentException If the type is unknown.
*
* @since 10.7.0
*/
- public function get_identifier(): string {
- return sprintf( '%s_%s_%s', get_current_blog_id(), $this->type, $this->resource_id );
+ public static function from_array( array $data ): self {
+ $type = $data['type'] ?? '';
+ $resource_id = (int) ( $data['resource_id'] ?? 0 );
+
+ $class = self::NOTIFICATION_CLASSES[ $type ] ?? null;
+
+ if ( ! $class ) {
+ // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
+ throw new InvalidArgumentException( sprintf( 'Unknown notification type: %s', $type ) );
+ }
+
+ return new $class( $resource_id );
}
/**
- * Gets the notification type.
+ * Returns a unique identifier for this notification, used for
+ * deduplication.
*
* @return string
*
* @since 10.7.0
*/
- public function get_type(): string {
- return $this->type;
+ public function get_identifier(): string {
+ return sprintf( '%s_%s_%s', get_current_blog_id(), $this->get_type(), $this->resource_id );
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/DataStores/PushTokensDataStoreTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/DataStores/PushTokensDataStoreTest.php
index 18eabd82d4c..9d226cd894e 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/DataStores/PushTokensDataStoreTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/DataStores/PushTokensDataStoreTest.php
@@ -703,6 +703,156 @@ class PushTokensDataStoreTest extends WC_Unit_Test_Case {
$this->assertEquals( array( 'app_version' => '2.0' ), $updated_token->get_metadata() );
}
+ /**
+ * @testdox Should return tokens for users with matching roles.
+ */
+ public function test_get_tokens_for_roles_returns_tokens_for_matching_users(): void {
+ $admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
+ $data_store = new PushTokensDataStore();
+
+ $data_store->create(
+ array(
+ 'user_id' => $admin_id,
+ 'token' => 'admin_token_' . wp_rand(),
+ 'platform' => PushToken::PLATFORM_APPLE,
+ 'device_uuid' => 'admin-device-' . wp_rand(),
+ 'origin' => PushToken::ORIGIN_WOOCOMMERCE_IOS,
+ 'device_locale' => 'en_US',
+ 'metadata' => array( 'app_version' => '1.0' ),
+ )
+ );
+
+ $tokens = $data_store->get_tokens_for_roles( array( 'administrator' ) );
+
+ $this->assertCount( 1, $tokens );
+ $this->assertInstanceOf( PushToken::class, $tokens[0] );
+ $this->assertSame( $admin_id, $tokens[0]->get_user_id() );
+ }
+
+ /**
+ * @testdox Should return empty array when no users have the specified role.
+ */
+ public function test_get_tokens_for_roles_returns_empty_when_no_users_have_role(): void {
+ $data_store = new PushTokensDataStore();
+
+ $tokens = $data_store->get_tokens_for_roles( array( 'shop_manager' ) );
+
+ $this->assertSame( array(), $tokens );
+ }
+
+ /**
+ * @testdox Should return empty array when users have the role but no tokens.
+ */
+ public function test_get_tokens_for_roles_returns_empty_when_users_have_no_tokens(): void {
+ $this->factory->user->create( array( 'role' => 'administrator' ) );
+ $data_store = new PushTokensDataStore();
+
+ $tokens = $data_store->get_tokens_for_roles( array( 'administrator' ) );
+
+ $this->assertSame( array(), $tokens );
+ }
+
+ /**
+ * @testdox Should skip malformed tokens and return only valid ones.
+ */
+ public function test_get_tokens_for_roles_skips_malformed_tokens(): void {
+ $admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
+ $data_store = new PushTokensDataStore();
+
+ wp_insert_post(
+ array(
+ 'post_author' => $admin_id,
+ 'post_type' => PushToken::POST_TYPE,
+ 'post_status' => 'private',
+ 'meta_input' => array(
+ 'platform' => PushToken::PLATFORM_APPLE,
+ 'token' => 'partial_token',
+ ),
+ )
+ );
+
+ $data_store->create(
+ array(
+ 'user_id' => $admin_id,
+ 'token' => 'valid_token',
+ 'platform' => PushToken::PLATFORM_APPLE,
+ 'device_uuid' => 'valid-device',
+ 'origin' => PushToken::ORIGIN_WOOCOMMERCE_IOS,
+ 'device_locale' => 'en_US',
+ 'metadata' => array( 'app_version' => '1.0' ),
+ )
+ );
+
+ $tokens = $data_store->get_tokens_for_roles( array( 'administrator' ) );
+
+ $this->assertCount( 1, $tokens );
+ $this->assertSame( 'valid-device', $tokens[0]->get_device_uuid() );
+ }
+
+ /**
+ * @testdox Should return tokens from multiple users with different matching roles.
+ */
+ public function test_get_tokens_for_roles_returns_tokens_from_multiple_roles(): void {
+ $admin_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
+ $manager_id = $this->factory->user->create( array( 'role' => 'shop_manager' ) );
+ $data_store = new PushTokensDataStore();
+
+ $data_store->create(
+ array(
+ 'user_id' => $admin_id,
+ 'token' => 'admin_token',
+ 'platform' => PushToken::PLATFORM_APPLE,
+ 'device_uuid' => 'admin-device',
+ 'origin' => PushToken::ORIGIN_WOOCOMMERCE_IOS,
+ 'device_locale' => 'en_US',
+ 'metadata' => array( 'app_version' => '1.0' ),
+ )
+ );
+
+ $data_store->create(
+ array(
+ 'user_id' => $manager_id,
+ 'token' => 'manager_token',
+ 'platform' => PushToken::PLATFORM_APPLE,
+ 'device_uuid' => 'manager-device',
+ 'origin' => PushToken::ORIGIN_WOOCOMMERCE_IOS,
+ 'device_locale' => 'en_US',
+ 'metadata' => array( 'app_version' => '1.0' ),
+ )
+ );
+
+ $tokens = $data_store->get_tokens_for_roles( array( 'administrator', 'shop_manager' ) );
+ $device_ids = array_map( fn ( PushToken $t ) => $t->get_device_uuid(), $tokens );
+
+ $this->assertCount( 2, $tokens );
+ $this->assertContains( 'admin-device', $device_ids );
+ $this->assertContains( 'manager-device', $device_ids );
+ }
+
+ /**
+ * @testdox Should not return tokens for users without the specified role.
+ */
+ public function test_get_tokens_for_roles_excludes_users_without_role(): void {
+ $subscriber_id = $this->factory->user->create( array( 'role' => 'subscriber' ) );
+ $data_store = new PushTokensDataStore();
+
+ $data_store->create(
+ array(
+ 'user_id' => $subscriber_id,
+ 'token' => 'subscriber_token_' . wp_rand(),
+ 'platform' => PushToken::PLATFORM_APPLE,
+ 'device_uuid' => 'subscriber-device-' . wp_rand(),
+ 'origin' => PushToken::ORIGIN_WOOCOMMERCE_IOS,
+ 'device_locale' => 'en_US',
+ 'metadata' => array( 'app_version' => '1.0' ),
+ )
+ );
+
+ $tokens = $data_store->get_tokens_for_roles( array( 'administrator' ) );
+
+ $this->assertSame( array(), $tokens );
+ }
+
/**
* Creates a test push token and saves it to the database.
*
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Dispatchers/InternalNotificationDispatcherTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Dispatchers/InternalNotificationDispatcherTest.php
index 3ed44a3e841..71d989a37e6 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Dispatchers/InternalNotificationDispatcherTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Dispatchers/InternalNotificationDispatcherTest.php
@@ -5,8 +5,9 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Internal\PushNotifications\Dispatchers;
use Automattic\WooCommerce\Internal\PushNotifications\Dispatchers\InternalNotificationDispatcher;
-use Automattic\WooCommerce\Internal\PushNotifications\Notifications\Notification;
use Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken;
+use Automattic\WooCommerce\Tests\Internal\PushNotifications\Stubs\StubOrderNotification;
+use Automattic\WooCommerce\Tests\Internal\PushNotifications\Stubs\StubReviewNotification;
use WC_Unit_Test_Case;
/**
@@ -82,7 +83,7 @@ class InternalNotificationDispatcherTest extends WC_Unit_Test_Case {
* @testdox Should fire a non-blocking POST to the send endpoint URL.
*/
public function test_dispatch_fires_non_blocking_post_to_send_endpoint(): void {
- $notifications = array( $this->create_notification( 'store_order', 1 ) );
+ $notifications = array( new StubOrderNotification( 1 ) );
$this->sut->dispatch( $notifications );
@@ -95,13 +96,23 @@ class InternalNotificationDispatcherTest extends WC_Unit_Test_Case {
$this->captured_request['blocking'],
'Request should be non-blocking'
);
+ $this->assertSame(
+ 1,
+ $this->captured_request['timeout'],
+ 'Request timeout should be 1 second'
+ );
+ $this->assertSame(
+ 'application/json',
+ $this->captured_request['headers']['Content-Type'],
+ 'Request Content-Type should be application/json'
+ );
}
/**
* @testdox Should include a valid JWT with correct claims and body hash.
*/
public function test_dispatch_includes_valid_jwt_with_correct_claims(): void {
- $notifications = array( $this->create_notification( 'store_order', 1 ) );
+ $notifications = array( new StubOrderNotification( 1 ) );
$this->sut->dispatch( $notifications );
@@ -130,8 +141,8 @@ class InternalNotificationDispatcherTest extends WC_Unit_Test_Case {
*/
public function test_dispatch_body_contains_encoded_notifications(): void {
$notifications = array(
- $this->create_notification( 'store_order', 10 ),
- $this->create_notification( 'store_review', 20 ),
+ new StubOrderNotification( 10 ),
+ new StubReviewNotification( 20 ),
);
$this->sut->dispatch( $notifications );
@@ -154,22 +165,4 @@ class InternalNotificationDispatcherTest extends WC_Unit_Test_Case {
$this->assertNull( $this->captured_url, 'No HTTP request should be made for empty notifications' );
}
-
- /**
- * Creates a concrete Notification instance for testing.
- *
- * @param string $type The notification type.
- * @param int $resource_id The resource ID.
- * @return Notification
- */
- private function create_notification( string $type, int $resource_id ): Notification {
- return new class( $type, $resource_id ) extends Notification {
- /**
- * {@inheritDoc}
- */
- public function to_payload(): ?array {
- return array( 'test' => true );
- }
- };
- }
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Dispatchers/WpcomNotificationDispatcherTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Dispatchers/WpcomNotificationDispatcherTest.php
index c68540e60b6..c77519fb94c 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Dispatchers/WpcomNotificationDispatcherTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Dispatchers/WpcomNotificationDispatcherTest.php
@@ -6,7 +6,7 @@ namespace Automattic\WooCommerce\Tests\Internal\PushNotifications\Dispatchers;
use Automattic\WooCommerce\Internal\PushNotifications\Dispatchers\WpcomNotificationDispatcher;
use Automattic\WooCommerce\Internal\PushNotifications\Entities\PushToken;
-use Automattic\WooCommerce\Internal\PushNotifications\Notifications\Notification;
+use Automattic\WooCommerce\Internal\PushNotifications\Notifications\NewOrderNotification;
use WC_Unit_Test_Case;
use WP_Error;
@@ -277,31 +277,19 @@ class WpcomNotificationDispatcherTest extends WC_Unit_Test_Case {
}
/**
- * Creates a concrete Notification instance for testing.
+ * Creates a mock Notification instance for testing.
*
* @param array|null $payload The payload to return from to_payload().
- * @return Notification
+ * @return NewOrderNotification
*/
- private function create_notification( ?array $payload = array( 'test' => true ) ): Notification {
- return new class( $payload ) extends Notification {
- /** @var array|null */
- private ?array $test_payload;
-
- /**
- * @param array|null $payload The payload to return.
- */
- public function __construct( ?array $payload ) {
- parent::__construct( 'store_order', 1 );
- $this->test_payload = $payload;
- }
-
- /**
- * {@inheritDoc}
- */
- public function to_payload(): ?array {
- return $this->test_payload;
- }
- };
+ private function create_notification( ?array $payload = array( 'test' => true ) ): NewOrderNotification {
+ $mock = $this->getMockBuilder( NewOrderNotification::class )
+ ->setConstructorArgs( array( 1 ) )
+ ->onlyMethods( array( 'to_payload', 'has_meta', 'write_meta' ) )
+ ->getMock();
+ $mock->method( 'to_payload' )->willReturn( $payload );
+
+ return $mock;
}
/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Notifications/NotificationTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Notifications/NotificationTest.php
index 2ceb08d0871..d5601a83e97 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Notifications/NotificationTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Notifications/NotificationTest.php
@@ -4,8 +4,11 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Internal\PushNotifications\Notifications;
+use Automattic\WooCommerce\Internal\PushNotifications\Notifications\NewOrderNotification;
+use Automattic\WooCommerce\Internal\PushNotifications\Notifications\NewReviewNotification;
use Automattic\WooCommerce\Internal\PushNotifications\Notifications\Notification;
-
+use Automattic\WooCommerce\Tests\Internal\PushNotifications\Stubs\StubOrderNotification;
+use Automattic\WooCommerce\Tests\Internal\PushNotifications\Stubs\StubReviewNotification;
use InvalidArgumentException;
use WC_Unit_Test_Case;
@@ -13,58 +16,20 @@ use WC_Unit_Test_Case;
* Tests for the Notification class.
*/
class NotificationTest extends WC_Unit_Test_Case {
- /**
- * @testdox Should store and return the notification type.
- */
- public function test_get_type(): void {
- $notification = $this->create_notification( 'store_order', 42 );
-
- $this->assertSame( 'store_order', $notification->get_type() );
- }
-
- /**
- * @testdox Should store and return the resource ID.
- */
- public function test_get_resource_id(): void {
- $notification = $this->create_notification( 'store_order', 42 );
-
- $this->assertSame( 42, $notification->get_resource_id() );
- }
-
/**
* @testdox Should return an identifier combining blog ID, type, and resource ID.
*/
public function test_get_identifier(): void {
- $notification = $this->create_notification( 'store_order', 42 );
+ $notification = new StubOrderNotification( 42 );
$this->assertSame( get_current_blog_id() . '_store_order_42', $notification->get_identifier() );
}
- /**
- * @testdox Should return different identifiers for different resource IDs.
- */
- public function test_get_identifier_differs_by_resource_id(): void {
- $order = $this->create_notification( 'store_order', 42 );
- $review = $this->create_notification( 'store_order', 43 );
-
- $this->assertNotSame( $order->get_identifier(), $review->get_identifier() );
- }
-
- /**
- * @testdox Should return different identifiers for different types with the same resource ID.
- */
- public function test_get_identifier_differs_by_type(): void {
- $order = $this->create_notification( 'store_order', 42 );
- $review = $this->create_notification( 'store_review', 42 );
-
- $this->assertNotSame( $order->get_identifier(), $review->get_identifier() );
- }
-
/**
* @testdox Should return notification data as an array.
*/
public function test_to_array(): void {
- $notification = $this->create_notification( 'store_review', 99 );
+ $notification = new StubReviewNotification( 99 );
$result = $notification->to_array();
@@ -75,49 +40,62 @@ class NotificationTest extends WC_Unit_Test_Case {
}
/**
- * @testdox Should throw when type is empty.
+ * @testdox Should throw when resource_id is $resource_id.
+ * @testWith [0]
+ * [-1]
+ *
+ * @param int $resource_id The invalid resource ID.
*/
- public function test_throws_for_empty_type(): void {
+ public function test_throws_for_non_positive_resource_id( int $resource_id ): void {
$this->expectException( InvalidArgumentException::class );
- $this->create_notification( '', 1 );
+ new StubOrderNotification( $resource_id );
}
/**
- * @testdox Should throw when resource_id is zero.
+ * @testdox from_array should create correct notification for $type type.
+ * @testWith ["store_order", "Automattic\\WooCommerce\\Internal\\PushNotifications\\Notifications\\NewOrderNotification"]
+ * ["store_review", "Automattic\\WooCommerce\\Internal\\PushNotifications\\Notifications\\NewReviewNotification"]
+ *
+ * @param string $type The notification type.
+ * @param string $expected_class The expected class name.
*/
- public function test_throws_for_zero_resource_id(): void {
- $this->expectException( InvalidArgumentException::class );
-
- $this->create_notification( 'store_order', 0 );
+ public function test_from_array_creates_notification( string $type, string $expected_class ): void {
+ $notification = Notification::from_array(
+ array(
+ 'type' => $type,
+ 'resource_id' => 42,
+ )
+ );
+
+ $this->assertInstanceOf( $expected_class, $notification );
+ $this->assertSame( 42, $notification->get_resource_id() );
}
/**
- * @testdox Should throw when resource_id is negative.
+ * @testdox from_array should throw for an unknown notification type.
*/
- public function test_throws_for_negative_resource_id(): void {
+ public function test_from_array_throws_for_unknown_type(): void {
$this->expectException( InvalidArgumentException::class );
- $this->create_notification( 'store_order', -1 );
+ Notification::from_array(
+ array(
+ 'type' => 'unknown_type',
+ 'resource_id' => 1,
+ )
+ );
}
/**
- * Creates a concrete Notification instance for testing.
- *
- * @param string $type The notification type.
- * @param int $resource_id The resource ID.
- * @return Notification
+ * @testdox Should throw when type is missing from array data.
*/
- private function create_notification( string $type, int $resource_id ): Notification {
- return new class( $type, $resource_id ) extends Notification {
- /**
- * Returns a test payload.
- *
- * @return array|null
- */
- public function to_payload(): ?array {
- return array( 'test' => true );
- }
- };
+ public function test_from_array_throws_for_missing_type(): void {
+ $this->expectException( InvalidArgumentException::class );
+
+ Notification::from_array(
+ array(
+ 'resource_id' => 1,
+ )
+ );
}
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Services/PendingNotificationStoreTest.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Services/PendingNotificationStoreTest.php
index 0e871cf9b20..9e3893071f5 100644
--- a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Services/PendingNotificationStoreTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Services/PendingNotificationStoreTest.php
@@ -5,8 +5,9 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Tests\Internal\PushNotifications\Services;
use Automattic\WooCommerce\Internal\PushNotifications\Dispatchers\InternalNotificationDispatcher;
-use Automattic\WooCommerce\Internal\PushNotifications\Notifications\Notification;
use Automattic\WooCommerce\Internal\PushNotifications\Services\PendingNotificationStore;
+use Automattic\WooCommerce\Tests\Internal\PushNotifications\Stubs\StubOrderNotification;
+use Automattic\WooCommerce\Tests\Internal\PushNotifications\Stubs\StubReviewNotification;
use WC_Unit_Test_Case;
/**
@@ -46,9 +47,7 @@ class PendingNotificationStoreTest extends WC_Unit_Test_Case {
* @testdox Should add a notification to the store.
*/
public function test_add_stores_notification(): void {
- $notification = $this->create_notification( 'store_order', 42 );
-
- $this->store->add( $notification );
+ $this->store->add( new StubOrderNotification( 42 ) );
$this->assertSame( 1, $this->store->count() );
}
@@ -57,11 +56,8 @@ class PendingNotificationStoreTest extends WC_Unit_Test_Case {
* @testdox Should deduplicate notifications with the same type and resource ID.
*/
public function test_add_deduplicates_same_type_and_resource(): void {
- $first = $this->create_notification( 'store_order', 42 );
- $second = $this->create_notification( 'store_order', 42 );
-
- $this->store->add( $first );
- $this->store->add( $second );
+ $this->store->add( new StubOrderNotification( 42 ) );
+ $this->store->add( new StubOrderNotification( 42 ) );
$this->assertSame( 1, $this->store->count() );
}
@@ -70,11 +66,8 @@ class PendingNotificationStoreTest extends WC_Unit_Test_Case {
* @testdox Should store notifications with different types separately.
*/
public function test_add_allows_different_types_for_same_resource(): void {
- $order = $this->create_notification( 'store_order', 42 );
- $review = $this->create_notification( 'store_review', 42 );
-
- $this->store->add( $order );
- $this->store->add( $review );
+ $this->store->add( new StubOrderNotification( 42 ) );
+ $this->store->add( new StubReviewNotification( 42 ) );
$this->assertSame( 2, $this->store->count() );
}
@@ -83,11 +76,8 @@ class PendingNotificationStoreTest extends WC_Unit_Test_Case {
* @testdox Should store notifications with different resource IDs separately.
*/
public function test_add_allows_same_type_for_different_resources(): void {
- $order_1 = $this->create_notification( 'store_order', 42 );
- $order_2 = $this->create_notification( 'store_order', 43 );
-
- $this->store->add( $order_1 );
- $this->store->add( $order_2 );
+ $this->store->add( new StubOrderNotification( 42 ) );
+ $this->store->add( new StubOrderNotification( 43 ) );
$this->assertSame( 2, $this->store->count() );
}
@@ -100,7 +90,7 @@ class PendingNotificationStoreTest extends WC_Unit_Test_Case {
$store = new PendingNotificationStore();
$store->init( $dispatcher );
- $store->add( $this->create_notification( 'store_order', 42 ) );
+ $store->add( new StubOrderNotification( 42 ) );
$this->assertSame( 0, $store->count() );
}
@@ -109,9 +99,9 @@ class PendingNotificationStoreTest extends WC_Unit_Test_Case {
* @testdox Should register shutdown hook only once regardless of how many notifications are added.
*/
public function test_add_registers_shutdown_hook_once(): void {
- $this->store->add( $this->create_notification( 'store_order', 1 ) );
- $this->store->add( $this->create_notification( 'store_order', 2 ) );
- $this->store->add( $this->create_notification( 'store_order', 3 ) );
+ $this->store->add( new StubOrderNotification( 1 ) );
+ $this->store->add( new StubOrderNotification( 2 ) );
+ $this->store->add( new StubOrderNotification( 3 ) );
$hook_count = 0;
@@ -134,7 +124,7 @@ class PendingNotificationStoreTest extends WC_Unit_Test_Case {
* @testdox Should clear pending notifications after dispatch.
*/
public function test_dispatch_all_clears_store(): void {
- $this->store->add( $this->create_notification( 'store_order', 1 ) );
+ $this->store->add( new StubOrderNotification( 1 ) );
$this->store->dispatch_all();
@@ -145,8 +135,8 @@ class PendingNotificationStoreTest extends WC_Unit_Test_Case {
* @testdox Should return all pending notifications via get_all.
*/
public function test_get_all_returns_pending_notifications(): void {
- $this->store->add( $this->create_notification( 'store_order', 1 ) );
- $this->store->add( $this->create_notification( 'store_review', 2 ) );
+ $this->store->add( new StubOrderNotification( 1 ) );
+ $this->store->add( new StubReviewNotification( 2 ) );
$all = $this->store->get_all();
@@ -154,24 +144,4 @@ class PendingNotificationStoreTest extends WC_Unit_Test_Case {
$this->assertSame( 1, $all[0]->get_resource_id() );
$this->assertSame( 2, $all[1]->get_resource_id() );
}
-
- /**
- * Creates a concrete Notification instance for testing.
- *
- * @param string $type The notification type.
- * @param int $resource_id The resource ID.
- * @return Notification
- */
- private function create_notification( string $type, int $resource_id ): Notification {
- return new class( $type, $resource_id ) extends Notification {
- /**
- * Returns a test payload.
- *
- * @return array|null
- */
- public function to_payload(): ?array {
- return array( 'test' => true );
- }
- };
- }
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Stubs/StubOrderNotification.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Stubs/StubOrderNotification.php
new file mode 100644
index 00000000000..8f3a9a94b92
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Stubs/StubOrderNotification.php
@@ -0,0 +1,40 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\PushNotifications\Stubs;
+
+use Automattic\WooCommerce\Internal\PushNotifications\Notifications\NewOrderNotification;
+
+/**
+ * Stub notification with type 'store_order' for testing.
+ */
+class StubOrderNotification extends NewOrderNotification {
+ /** @var array<string, bool> */
+ private array $meta = array();
+
+ /**
+ * {@inheritDoc}
+ */
+ public function to_payload(): ?array {
+ return array( 'test' => true );
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param string $key The meta key.
+ */
+ public function has_meta( string $key ): bool {
+ return isset( $this->meta[ $key ] );
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param string $key The meta key.
+ */
+ public function write_meta( string $key ): void {
+ $this->meta[ $key ] = true;
+ }
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Stubs/StubReviewNotification.php b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Stubs/StubReviewNotification.php
new file mode 100644
index 00000000000..26c2460980d
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/PushNotifications/Stubs/StubReviewNotification.php
@@ -0,0 +1,40 @@
+<?php
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\PushNotifications\Stubs;
+
+use Automattic\WooCommerce\Internal\PushNotifications\Notifications\NewReviewNotification;
+
+/**
+ * Stub notification with type 'store_review' for testing.
+ */
+class StubReviewNotification extends NewReviewNotification {
+ /** @var array<string, bool> */
+ private array $meta = array();
+
+ /**
+ * {@inheritDoc}
+ */
+ public function to_payload(): ?array {
+ return array( 'test' => true );
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param string $key The meta key.
+ */
+ public function has_meta( string $key ): bool {
+ return isset( $this->meta[ $key ] );
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param string $key The meta key.
+ */
+ public function write_meta( string $key ): void {
+ $this->meta[ $key ] = true;
+ }
+}