diff options
author | Piotr Miazga <pmiazga@wikimedia.org> | 2025-03-06 14:48:12 +0100 |
---|---|---|
committer | Piotr Miazga <pmiazga@wikimedia.org> | 2025-03-21 10:50:14 +0100 |
commit | f1e88be974500910c79ad0dd9a2cf4a454d24b4d (patch) | |
tree | 681a41dd81136fb3609b071d2ccce8ad5a436af7 /tests | |
parent | 35bbe148a98cb4f119524cbbf52dd0e96ec0f4dc (diff) | |
download | mediawikicore-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')
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 ); + } +} |