diff --git a/tests/phpunit/integration/includes/Permissions/RateLimiterTest.php b/tests/phpunit/integration/includes/Permissions/RateLimiterTest.php index 154deae2a76..77057e3773c 100644 --- a/tests/phpunit/integration/includes/Permissions/RateLimiterTest.php +++ b/tests/phpunit/integration/includes/Permissions/RateLimiterTest.php @@ -42,6 +42,8 @@ class RateLimiterTest extends MediaWikiIntegrationTestCase { /** * @covers ::limit + * @covers ::__construct + * @covers ::getConditions * @covers \Wikimedia\WRStats\WRStatsFactory * @covers \Wikimedia\WRStats\BagOStuffStatsStore */ @@ -135,7 +137,8 @@ class RateLimiterTest extends MediaWikiIntegrationTestCase { } /** - * @covers \MediaWiki\Permissions\RateLimiter::limit + * @covers ::limit + * @covers ::getConditions */ public function testPingLimiterWithStaleCache() { $limits = [ @@ -255,7 +258,7 @@ class RateLimiterTest extends MediaWikiIntegrationTestCase { ); } - public function provideIsPingLimitable() { + public function provideIsExempt() { $user = new UserIdentityValue( 123, 'Foo' ); yield 'IP not excluded' @@ -273,7 +276,7 @@ class RateLimiterTest extends MediaWikiIntegrationTestCase { } /** - * @dataProvider provideIsPingLimitable + * @dataProvider provideIsExempt * @covers ::isExempt * * @param array $rateLimitExcludeIps @@ -363,4 +366,58 @@ class RateLimiterTest extends MediaWikiIntegrationTestCase { $this->assertTrue( $limiter->limit( $user3, 'edit' ) ); } + /** + * Test that '&can-bypass' can be used to impose limits on users + * who are otherwise exempt from limits. + * + * @covers ::limit + */ + public function testCanBypass() { + $limits = [ + 'edit' => [ + 'user' => [ 1, 60 ], + ], + 'delete' => [ + '&can-bypass' => false, + 'user' => [ 1, 60 ], + ], + ]; + + $user = new RateLimitSubject( + new UserIdentityValue( 7, 'Garth' ), + '127.0.0.1', + [ RateLimitSubject::EXEMPT => true ] + ); + + $limiter = $this->newRateLimiter( $limits, [] ); + $this->assertFalse( $limiter->limit( $user, 'edit' ) ); + $this->assertFalse( $limiter->limit( $user, 'delete' ) ); + + $this->assertFalse( $limiter->limit( $user, 'edit' ), 'bypass should be granted' ); + $this->assertTrue( $limiter->limit( $user, 'delete' ), 'bypass should be denied' ); + } + + /** + * Test that the most permissive limit is used when a limit is defined for + * multiple groups a user belongs to. + * + * @covers ::limit + */ + public function testGroupLimits() { + $limits = [ + 'edit' => [ + 'user' => [ 1, 60 ], + 'autoconfirmed' => [ 2, 60 ], + ], + ]; + + $user = $this->getTestUser( [ 'autoconfirmed' ] )->getUser(); + $user = new RateLimitSubject( $user, '127.0.0.1', [] ); + + $limiter = $this->newRateLimiter( $limits, [] ); + $this->assertFalse( $limiter->limit( $user, 'edit' ) ); + $this->assertFalse( $limiter->limit( $user, 'edit' ), 'limit for autoconfirmed used' ); + $this->assertTrue( $limiter->limit( $user, 'edit' ), 'limit for autoconfirmed exceeded' ); + } + } diff --git a/tests/phpunit/unit/includes/libs/WRStats/BagOStuffStatsStoreTest.php b/tests/phpunit/unit/includes/libs/WRStats/BagOStuffStatsStoreTest.php new file mode 100644 index 00000000000..8c454e13a6d --- /dev/null +++ b/tests/phpunit/unit/includes/libs/WRStats/BagOStuffStatsStoreTest.php @@ -0,0 +1,101 @@ +cache = new HashBagOStuff(); + $this->cache->setMockTime( $this->mockTime ); + } + + private function tickMockTime( $time ) { + $this->mockTime += $time; + $this->cache->setMockTime( $this->mockTime ); + } + + private function getStatsStore() { + return new BagOStuffStatsStore( $this->cache ); + } + + public function provideMakeKey() { + yield [ [ 'prefix' ], [ 'internals' ], new LocalEntityKey( [ 'key' ] ), 'local:prefix:internals:key' ]; + yield [ [ 'prefix' ], [ 'internals' ], new GlobalEntityKey( [ 'key' ] ), 'global:prefix:internals:key' ]; + yield [ [ 'p', 'q' ], [ 'i', 'j' ], new GlobalEntityKey( [ 'k', 'h' ] ), 'global:p:q:i:j:k:h' ]; + } + + /** + * @param array $prefix + * @param array $internals + * @param EntityKey $entity + * @param string $expected + * + * @dataProvider provideMakeKey + */ + public function testMakeKey( $prefix, $internals, $entity, $expected ) { + $store = $this->getStatsStore(); + $this->assertSame( + $expected, + $store->makeKey( + $prefix, + $internals, + $entity + ) + ); + } + + public function testIncrAndExpiry() { + $store = $this->getStatsStore(); + + $store->incr( [ 'a' => 1, 'b' => 2 ], 10 ); + + $this->tickMockTime( 2 ); + + $store->incr( [ 'b' => 1, 'c' => 1 ], 10 ); + + $values = $store->query( [ 'a', 'b', 'c' ] ); + $this->assertSame( 1, $values['a'] ); + $this->assertSame( 3, $values['b'] ); + $this->assertSame( 1, $values['c'] ); + + $this->tickMockTime( 9 ); + + // The TTL is counted from the time the value was first set, + // not the time it was last updated. So the entries + // for a and b should have expired now. + $values = $store->query( [ 'a', 'b', 'c' ] ); + $this->assertArrayNotHasKey( 'a', $values ); + $this->assertArrayNotHasKey( 'b', $values ); + $this->assertSame( 1, $values['c'] ); + } + + public function testDelete() { + $store = $this->getStatsStore(); + + $store->incr( [ 'a' => 1, 'b' => 2 ], 10 ); + + $store->delete( [ 'b' ] ); + + $values = $store->query( [ 'a', 'b' ] ); + $this->assertSame( 1, $values['a'] ); + $this->assertArrayNotHasKey( 'b', $values ); + } + +}