RateLimiter: improve test coverage
This adds two tests: 1) ensure that &can-bypass can be used to impose limits on users who are otherwise exempt from limits. 2) ensure that among multiple limits derived from group membership, the most permissive one is used. Change-Id: Iaf9e5d9cc15cbbd3ba117662dd89885ff517580d
This commit is contained in:
parent
8de63ae485
commit
584287926c
2 changed files with 161 additions and 3 deletions
|
|
@ -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' );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace Wikimedia\WRStats;
|
||||
|
||||
use HashBagOStuff;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \Wikimedia\WRStats\BagOStuffStatsStore
|
||||
*/
|
||||
class BagOStuffStatsStoreTest extends TestCase {
|
||||
|
||||
/**
|
||||
* @var HashBagOStuff
|
||||
*/
|
||||
private $cache;
|
||||
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
private $mockTime = 1000000.0;
|
||||
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->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 );
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue