aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorPiotr Miazga <pmiazga@wikimedia.org>2025-03-06 14:48:12 +0100
committerPiotr Miazga <pmiazga@wikimedia.org>2025-03-21 10:50:14 +0100
commitf1e88be974500910c79ad0dd9a2cf4a454d24b4d (patch)
tree681a41dd81136fb3609b071d2ccce8ad5a436af7 /tests
parent35bbe148a98cb4f119524cbbf52dd0e96ec0f4dc (diff)
downloadmediawikicore-f1e88be974500910c79ad0dd9a2cf4a454d24b4d.tar.gz
mediawikicore-f1e88be974500910c79ad0dd9a2cf4a454d24b4d.zip
notifications: Introduce Notification Middleware and NotificationEnvelope
To allow ourselves esier processing/modifying Notifications lets introduce an idea of NotificationsEnvelope which represents a Notification being sent and list of recipients. The middleware approach will allow us to modify the Notification behaviour by letting extensions to inject/modify the Notifications. Each Middleware will retrieve a list of Envelopes MediaWiki wants to send. Middlewares should iterate over envelopes and decide if those want to add/remove/replace Notifications and/or Recipients. Bug: T387996 Change-Id: Ib3ee35c75b2f4dcfdc516b9259a852dc73c4a778
Diffstat (limited to 'tests')
-rw-r--r--tests/phpunit/integration/includes/Notification/MiddlewareChainTest.php74
-rw-r--r--tests/phpunit/unit/includes/Notification/MiddlewareChainTest.php170
-rw-r--r--tests/phpunit/unit/includes/Notification/NotificationBatchTest.php94
-rw-r--r--tests/phpunit/unit/includes/Notification/NotificationServiceTest.php35
-rw-r--r--tests/phpunit/unit/includes/Notification/SuppressNotificationByTypeMiddlewareTest.php40
5 files changed, 413 insertions, 0 deletions
diff --git a/tests/phpunit/integration/includes/Notification/MiddlewareChainTest.php b/tests/phpunit/integration/includes/Notification/MiddlewareChainTest.php
new file mode 100644
index 000000000000..4fda5639b16b
--- /dev/null
+++ b/tests/phpunit/integration/includes/Notification/MiddlewareChainTest.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Tests\Integration\Notification;
+
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Notification\MiddlewareException;
+use MediaWiki\Notification\Notification;
+use MediaWiki\Notification\NotificationMiddlewareInterface;
+use MediaWiki\Notification\RecipientSet;
+use MediaWiki\Registration\ExtensionRegistry;
+use MediaWiki\User\UserIdentity;
+use MediaWikiIntegrationTestCase;
+use Wikimedia\ScopedCallback;
+
+/**
+ * @group Mail
+ * @covers \MediaWiki\Notification\MiddlewareChain
+ */
+class MiddlewareChainTest extends MediaWikiIntegrationTestCase {
+
+ /**
+ * Test case when Middleware calls NotificationService::notify() to inject new notification
+ * This can cause endless loops where Middleware triggers a notification, that triggers the
+ * middleware again.
+ *
+ * @return void
+ */
+ public function testMiddlewareCannotTriggerNotificationService() {
+ $this->expectException( MiddlewareException::class );
+
+ $scope = ExtensionRegistry::getInstance()->setAttributeForTest(
+ 'NotificationMiddleware', [
+ [
+ "factory" => static function () {
+ return new class implements NotificationMiddlewareInterface {
+ public function handle( $batch, callable $next ): void {
+ MediaWikiServices::getInstance()
+ ->getNotificationService()
+ ->notify(
+ new Notification( "bad" ), new RecipientSet( [] )
+ );
+ $next();
+ }
+ };
+ },
+ ],
+ ]
+ );
+ $sut = MediaWikiServices::getInstance()->getNotificationService();
+ $user = $this->createMock( UserIdentity::class );
+ $sut->notify(
+ new Notification( "good" ), new RecipientSet( [ $user ] )
+ );
+ ScopedCallback::consume( $scope );
+ }
+}
diff --git a/tests/phpunit/unit/includes/Notification/MiddlewareChainTest.php b/tests/phpunit/unit/includes/Notification/MiddlewareChainTest.php
new file mode 100644
index 000000000000..c133727d84cb
--- /dev/null
+++ b/tests/phpunit/unit/includes/Notification/MiddlewareChainTest.php
@@ -0,0 +1,170 @@
+<?php
+
+namespace MediaWiki\Tests\Notification;
+
+use MediaWiki\Notification\MiddlewareChain;
+use MediaWiki\Notification\Notification;
+use MediaWiki\Notification\NotificationEnvelope;
+use MediaWiki\Notification\NotificationMiddlewareInterface;
+use MediaWiki\Notification\NotificationsBatch;
+use MediaWiki\Notification\RecipientSet;
+use MediaWiki\User\UserIdentity;
+use MediaWikiUnitTestCase;
+
+/**
+ * @covers \MediaWiki\Notification\MiddlewareChain
+ */
+class MiddlewareChainTest extends MediaWikiUnitTestCase {
+
+ private function getSut( array $middlewares ): MiddlewareChain {
+ $specs = [];
+ foreach ( $middlewares as $middleware ) {
+ $specs[] = [ 'factory' => static fn () => $middleware ];
+ }
+ return new MiddlewareChain( $this->createSimpleObjectFactory(), $specs );
+ }
+
+ public function testEmptyMiddlewareChainReturnsOriginalBatch() {
+ $userIdentity = $this->createMock( UserIdentity::class );
+ $notificationToSend = new Notification( 'test', [] );
+ $recipients = new RecipientSet( [ $userIdentity ] );
+
+ $envelopes = new NotificationsBatch(
+ new NotificationEnvelope( $notificationToSend, $recipients )
+ );
+
+ $sut = $this->getSut( [] );
+ $this->assertSame( $envelopes, $sut->process( $envelopes ) );
+ }
+
+ public function testExecutesInOrderAndModifiesBatch() {
+ $userIdentity = $this->createMock( UserIdentity::class );
+ $notificationToSend = new Notification( 'test', [] );
+ $recipients = new RecipientSet( [ $userIdentity ] );
+
+ $noOpMiddleware = $this->createMock( NotificationMiddlewareInterface::class );
+ $noOpMiddleware->expects( $this->exactly( 2 ) )
+ ->method( 'handle' )
+ ->willReturnCallback( function ( NotificationsBatch $batch, callable $next ) {
+ $this->assertCount( 1, $batch );
+ $next();
+ } );
+ $emptyMiddleware = $this->createMock( NotificationMiddlewareInterface::class );
+ $emptyMiddleware->expects( $this->once() )
+ ->method( 'handle' )
+ ->willReturnCallback( static function ( NotificationsBatch $batch, callable $next ) {
+ foreach ( $batch as $envelope ) {
+ $batch->remove( $envelope );
+ }
+ $next();
+ } );
+ $makeSureBatchIsEmptyMiddleware = $this->createMock( NotificationMiddlewareInterface::class );
+ $makeSureBatchIsEmptyMiddleware->expects( $this->once() )
+ ->method( 'handle' )
+ ->willReturnCallback( function ( NotificationsBatch $batch, callable $next ) {
+ $this->assertCount( 0, $batch );
+ $next();
+ } );
+
+ $middlewareChain = $this->getSut( [
+ $noOpMiddleware, $noOpMiddleware, $emptyMiddleware, $makeSureBatchIsEmptyMiddleware,
+ ] );
+
+ $result = $middlewareChain->process( new NotificationsBatch(
+ new NotificationEnvelope( $notificationToSend, $recipients )
+ ) );
+ $this->assertCount( 0, $result );
+ }
+
+ public function testMiddlewareDoesntCallNext() {
+ $userIdentity = $this->createMock( UserIdentity::class );
+ $keptNotification = new Notification( 'test', [] );
+ $recipients = new RecipientSet( [ $userIdentity ] );
+
+ $suppressWelcomeMiddleware = $this->createMock( NotificationMiddlewareInterface::class );
+ $suppressWelcomeMiddleware->expects( $this->once() )
+ ->method( 'handle' )
+ ->willReturnCallback( static function ( NotificationsBatch $batch, callable $next ) {
+ // no op, but also don't call next
+ } );
+
+ $middlewareChain = $this->getSut( [
+ $suppressWelcomeMiddleware,
+ ] );
+
+ $batch = $middlewareChain->process(
+ new NotificationsBatch(
+ new NotificationEnvelope( $keptNotification, $recipients )
+ )
+ );
+ $this->assertCount( 1, $batch );
+ $envelopes = iterator_to_array( $batch );
+ $this->assertSame( $keptNotification, $envelopes[0]->getNotification() );
+ }
+
+ public function testSupressNotification() {
+ $userIdentity = $this->createMock( UserIdentity::class );
+ $keptNotification = new Notification( 'test', [] );
+ $removedNotification = new Notification( 'welcome', [] );
+ $recipients = new RecipientSet( [ $userIdentity ] );
+
+ $suppressWelcomeMiddleware = $this->createMock( NotificationMiddlewareInterface::class );
+ $suppressWelcomeMiddleware->expects( $this->once() )
+ ->method( 'handle' )
+ ->willReturnCallback( static function ( NotificationsBatch $batch, callable $next ) {
+ foreach ( $batch as $envelope ) {
+ if ( $envelope->getNotification()->getType() === 'welcome' ) {
+ $batch->remove( $envelope );
+ }
+ }
+ $next();
+ } );
+
+ $middlewareChain = $this->getSut( [
+ $suppressWelcomeMiddleware,
+ ] );
+
+ $batch = $middlewareChain->process( new NotificationsBatch(
+ new NotificationEnvelope( $keptNotification, $recipients ),
+ new NotificationEnvelope( $removedNotification, $recipients )
+ ) );
+ $this->assertCount( 1, $batch );
+ foreach ( $batch as $envelope ) {
+ $this->assertSame( $keptNotification, $envelope->getNotification() );
+ }
+ }
+
+ /**
+ * Test case when Middleware calls NotificationService::notify() to inject new notification
+ * This can cause endless loops where Middleware triggers a notification, that triggers the
+ * middleware again.
+ *
+ * @return void
+ */
+ public function testBadMiddlewareTriesToSendNotificationInsteadOfInjectingToBatch() {
+ $userIdentity = $this->createMock( UserIdentity::class );
+ $keptNotification = new Notification( 'test', [] );
+ $recipients = new RecipientSet( [ $userIdentity ] );
+
+ $suppressWelcomeMiddleware = $this->createMock( NotificationMiddlewareInterface::class );
+ $suppressWelcomeMiddleware->expects( $this->once() )
+ ->method( 'handle' )
+ ->willReturnCallback( static function ( NotificationsBatch $batch, callable $next ) {
+ // no op, but also don't call next
+ } );
+
+ $middlewareChain = $this->getSut( [
+ $suppressWelcomeMiddleware,
+ ] );
+
+ $batch = $middlewareChain->process(
+ new NotificationsBatch(
+ new NotificationEnvelope( $keptNotification, $recipients )
+ )
+ );
+ $this->assertCount( 1, $batch );
+ $envelopes = iterator_to_array( $batch );
+ $this->assertSame( $keptNotification, $envelopes[0]->getNotification() );
+ }
+
+}
diff --git a/tests/phpunit/unit/includes/Notification/NotificationBatchTest.php b/tests/phpunit/unit/includes/Notification/NotificationBatchTest.php
new file mode 100644
index 000000000000..4cda67741215
--- /dev/null
+++ b/tests/phpunit/unit/includes/Notification/NotificationBatchTest.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace MediaWiki\Tests\Notification;
+
+use MediaWiki\Notification\Notification;
+use MediaWiki\Notification\NotificationEnvelope;
+use MediaWiki\Notification\NotificationsBatch;
+use MediaWiki\Notification\RecipientSet;
+use MediaWikiUnitTestCase;
+use stdClass;
+
+/**
+ * @covers \MediaWiki\Notification\NotificationsBatch
+ * @covers \MediaWiki\Notification\NotificationEnvelope
+ */
+class NotificationBatchTest extends MediaWikiUnitTestCase {
+
+ public function testAdds() {
+ $first = new NotificationEnvelope( new Notification( 'first' ), new RecipientSet( [] ) );
+ $second = new NotificationEnvelope( new Notification( 'second' ), new RecipientSet( [] ) );
+
+ $batch = new NotificationsBatch( $first );
+ $batch->add( $second );
+
+ $this->assertCount( 2, $batch );
+ $envelopesArray = iterator_to_array( $batch );
+ $this->assertSame( $first, $envelopesArray[0] );
+ $this->assertSame( $second, $envelopesArray[1] );
+ }
+
+ public function testRemoveWhenExists() {
+ $first = new NotificationEnvelope( new Notification( 'first' ), new RecipientSet( [] ) );
+ $second = new NotificationEnvelope( new Notification( 'second' ), new RecipientSet( [] ) );
+ $third = new NotificationEnvelope( new Notification( 'third' ), new RecipientSet( [] ) );
+ $last = new NotificationEnvelope( new Notification( 'fourth' ), new RecipientSet( [] ) );
+
+ $batch = new NotificationsBatch( $first, $second, $third, $last );
+ $this->assertCount( 4, $batch );
+
+ $batch->remove( $second );
+ $this->assertCount( 3, $batch );
+ $envelopesArray = iterator_to_array( $batch );
+ $this->assertSame( $first, $envelopesArray[0] );
+ $this->assertSame( $third, $envelopesArray[1] );
+ $this->assertSame( $last, $envelopesArray[2] );
+
+ $batch->remove( $first );
+ $this->assertCount( 2, $batch );
+ $envelopesArray = iterator_to_array( $batch );
+ $this->assertSame( $third, $envelopesArray[0] );
+ $this->assertSame( $last, $envelopesArray[1] );
+
+ $batch->remove( $last );
+ $this->assertCount( 1, $batch );
+ $envelopesArray = iterator_to_array( $batch );
+ $this->assertSame( $third, $envelopesArray[0] );
+ }
+
+ public function testRemovesWhenNotExists() {
+ $first = new NotificationEnvelope( new Notification( 'first' ), new RecipientSet( [] ) );
+ $second = new NotificationEnvelope( new Notification( 'second' ), new RecipientSet( [] ) );
+ $other = new NotificationEnvelope( new Notification( 'third' ), new RecipientSet( [] ) );
+
+ $batch = new NotificationsBatch( $first, $second );
+ $batch->remove( $other );
+
+ $this->assertCount( 2, $batch );
+ $envelopesArray = iterator_to_array( $batch );
+ $this->assertSame( $first, $envelopesArray[0] );
+ $this->assertSame( $second, $envelopesArray[1] );
+ }
+
+ public function testFilters() {
+ $first = new NotificationEnvelope( new Notification( 'first' ), new RecipientSet( [] ) );
+ $second = new NotificationEnvelope( new Notification( 'second' ), new RecipientSet( [] ) );
+
+ $filterMock = $this->getMockBuilder( stdClass::class )
+ ->addMethods( [ '__invoke' ] )
+ ->getMock();
+
+ $filterMock->expects( $this->exactly( 2 ) )
+ ->method( '__invoke' )
+ ->willReturnCallback( static function ( NotificationEnvelope $envelope ) use ( $first ) {
+ return $envelope->equals( $first );
+ } );
+
+ $batch = new NotificationsBatch( $first, $second );
+ $batch->filter( $filterMock );
+ $this->assertCount( 1, $batch );
+ $envelopesArray = iterator_to_array( $batch );
+ $this->assertSame( $first, $envelopesArray[0] );
+ }
+
+}
diff --git a/tests/phpunit/unit/includes/Notification/NotificationServiceTest.php b/tests/phpunit/unit/includes/Notification/NotificationServiceTest.php
index 242a3ac6f515..e62a1be23e41 100644
--- a/tests/phpunit/unit/includes/Notification/NotificationServiceTest.php
+++ b/tests/phpunit/unit/includes/Notification/NotificationServiceTest.php
@@ -2,6 +2,7 @@
namespace MediaWiki\Tests\Notification;
+use MediaWiki\Notification\MiddlewareChain;
use MediaWiki\Notification\Notification;
use MediaWiki\Notification\NotificationHandler;
use MediaWiki\Notification\NotificationService;
@@ -16,6 +17,36 @@ use RuntimeException;
*/
class NotificationServiceTest extends MediaWikiUnitTestCase {
+ protected function getEmptyMiddleware() {
+ $middleware = $this->createMock( MiddlewareChain::class );
+ $middleware->expects( $this->any() )
+ ->method( 'process' )
+ ->willReturnArgument( 0 );
+ return $middleware;
+ }
+
+ public function testTriggersMiddleware() {
+ $notification = new Notification( 'test.middleware' );
+ $recipients = new RecipientSet( [] );
+ $middleware = $this->createMock( MiddlewareChain::class );
+ $middleware->expects( $this->once() )
+ ->method( 'process' )
+ ->willReturnCallback( function ( $batch ) {
+ $envelopes = iterator_to_array( $batch );
+ $this->assertCount( 1, $envelopes );
+ $this->assertSame( 'test.middleware', $envelopes[0]->getNotification()->getType() );
+ return $batch;
+ } );
+
+ $svc = new NotificationService(
+ new NullLogger(),
+ $this->createSimpleObjectFactory(),
+ $middleware,
+ []
+ );
+ $svc->notify( $notification, $recipients );
+ }
+
public function testBasic() {
$recipients = new RecipientSet( [] );
@@ -34,6 +65,7 @@ class NotificationServiceTest extends MediaWikiUnitTestCase {
$svc = new NotificationService(
new NullLogger(),
$this->createSimpleObjectFactory(),
+ $this->getEmptyMiddleware(),
[
[ 'types' => [ 'A' ], 'factory' => static fn () => $handlerA ],
[ 'types' => [ '*' ], 'factory' => static fn () => $handlerB ],
@@ -56,6 +88,7 @@ class NotificationServiceTest extends MediaWikiUnitTestCase {
$svc = new NotificationService(
new NullLogger(),
$this->createSimpleObjectFactory(),
+ $this->getEmptyMiddleware(),
[
[ 'types' => [ 'A/*' ], 'factory' => static fn () => $handler ],
]
@@ -74,6 +107,7 @@ class NotificationServiceTest extends MediaWikiUnitTestCase {
$svc = new NotificationService(
new NullLogger(),
$this->createSimpleObjectFactory(),
+ $this->getEmptyMiddleware(),
[
[ 'types' => [ '*' ], 'factory' => static fn () => $handler ],
[ 'types' => [ '*' ], 'factory' => static fn () => $handler ],
@@ -93,6 +127,7 @@ class NotificationServiceTest extends MediaWikiUnitTestCase {
$svc = new NotificationService(
$mockLogger,
$this->createSimpleObjectFactory(),
+ $this->getEmptyMiddleware(),
[
[ 'types' => [ 'B' ], 'factory' => static fn () => $handler ],
]
diff --git a/tests/phpunit/unit/includes/Notification/SuppressNotificationByTypeMiddlewareTest.php b/tests/phpunit/unit/includes/Notification/SuppressNotificationByTypeMiddlewareTest.php
new file mode 100644
index 000000000000..ce56c776b088
--- /dev/null
+++ b/tests/phpunit/unit/includes/Notification/SuppressNotificationByTypeMiddlewareTest.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace MediaWiki\Tests\Notification;
+
+use MediaWiki\Notification\Middleware\SuppressNotificationByTypeMiddleware;
+use MediaWiki\Notification\Notification;
+use MediaWiki\Notification\NotificationEnvelope;
+use MediaWiki\Notification\NotificationsBatch;
+use MediaWiki\Notification\RecipientSet;
+use MediaWikiUnitTestCase;
+
+/**
+ * @covers \MediaWiki\Notification\NotificationService
+ */
+class SuppressNotificationByTypeMiddlewareTest extends MediaWikiUnitTestCase {
+
+ public function testRemovesNotificationTest() {
+ $typeToRemove = 'test';
+ $sut = new SuppressNotificationByTypeMiddleware( $typeToRemove );
+ $notificationA = new Notification( 'first' );
+ $notificationB = new Notification( $typeToRemove );
+ $notificationC = new Notification( 'last' );
+ $recipients = new RecipientSet( [] );
+ $test = $this;
+
+ $batch = new NotificationsBatch(
+ new NotificationEnvelope( $notificationA, $recipients ),
+ new NotificationEnvelope( $notificationB, $recipients ),
+ new NotificationEnvelope( $notificationC, $recipients )
+ );
+
+ $sut->handle( $batch, static function () use ( $test, $batch ) {
+ $test->assertCount( 2, $batch );
+ $envelopes = iterator_to_array( $batch );
+ $test->assertSame( 'first', $envelopes[0]->getNotification()->getType() );
+ $test->assertSame( 'last', $envelopes[1]->getNotification()->getType() );
+ } );
+ $this->assertCount( 2, $batch );
+ }
+}