Merge "Fix sessionfailure i18n message during authentication"
[lhc/web/wiklou.git] / tests / phpunit / includes / libs / rdbms / database / DatabaseTest.php
1 <?php
2
3 use Wikimedia\Rdbms\IDatabase;
4 use Wikimedia\Rdbms\LBFactorySingle;
5 use Wikimedia\Rdbms\TransactionProfiler;
6 use Wikimedia\TestingAccessWrapper;
7
8 class DatabaseTest extends PHPUnit_Framework_TestCase {
9
10 use MediaWikiCoversValidator;
11
12 protected function setUp() {
13 $this->db = new DatabaseTestHelper( __CLASS__ . '::' . $this->getName() );
14 }
15
16 public static function provideAddQuotes() {
17 return [
18 [ null, 'NULL' ],
19 [ 1234, "'1234'" ],
20 [ 1234.5678, "'1234.5678'" ],
21 [ 'string', "'string'" ],
22 [ 'string\'s cause trouble', "'string\'s cause trouble'" ],
23 ];
24 }
25
26 /**
27 * @dataProvider provideAddQuotes
28 * @covers Wikimedia\Rdbms\Database::addQuotes
29 */
30 public function testAddQuotes( $input, $expected ) {
31 $this->assertEquals( $expected, $this->db->addQuotes( $input ) );
32 }
33
34 public static function provideTableName() {
35 // Formatting is mostly ignored since addIdentifierQuotes is abstract.
36 // For testing of addIdentifierQuotes, see actual Database subclas tests.
37 return [
38 'local' => [
39 'tablename',
40 'tablename',
41 'quoted',
42 ],
43 'local-raw' => [
44 'tablename',
45 'tablename',
46 'raw',
47 ],
48 'shared' => [
49 'sharedb.tablename',
50 'tablename',
51 'quoted',
52 [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => '' ],
53 ],
54 'shared-raw' => [
55 'sharedb.tablename',
56 'tablename',
57 'raw',
58 [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => '' ],
59 ],
60 'shared-prefix' => [
61 'sharedb.sh_tablename',
62 'tablename',
63 'quoted',
64 [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => 'sh_' ],
65 ],
66 'shared-prefix-raw' => [
67 'sharedb.sh_tablename',
68 'tablename',
69 'raw',
70 [ 'dbname' => 'sharedb', 'schema' => null, 'prefix' => 'sh_' ],
71 ],
72 'foreign' => [
73 'databasename.tablename',
74 'databasename.tablename',
75 'quoted',
76 ],
77 'foreign-raw' => [
78 'databasename.tablename',
79 'databasename.tablename',
80 'raw',
81 ],
82 ];
83 }
84
85 /**
86 * @dataProvider provideTableName
87 * @covers Wikimedia\Rdbms\Database::tableName
88 */
89 public function testTableName( $expected, $table, $format, array $alias = null ) {
90 if ( $alias ) {
91 $this->db->setTableAliases( [ $table => $alias ] );
92 }
93 $this->assertEquals(
94 $expected,
95 $this->db->tableName( $table, $format ?: 'quoted' )
96 );
97 }
98
99 public function provideTableNamesWithIndexClauseOrJOIN() {
100 return [
101 'one-element array' => [
102 [ 'table' ], [], 'table '
103 ],
104 'comma join' => [
105 [ 'table1', 'table2' ], [], 'table1,table2 '
106 ],
107 'real join' => [
108 [ 'table1', 'table2' ],
109 [ 'table2' => [ 'LEFT JOIN', 't1_id = t2_id' ] ],
110 'table1 LEFT JOIN table2 ON ((t1_id = t2_id))'
111 ],
112 'real join with multiple conditionals' => [
113 [ 'table1', 'table2' ],
114 [ 'table2' => [ 'LEFT JOIN', [ 't1_id = t2_id', 't2_x = \'X\'' ] ] ],
115 'table1 LEFT JOIN table2 ON ((t1_id = t2_id) AND (t2_x = \'X\'))'
116 ],
117 'join with parenthesized group' => [
118 [ 'table1', 'n' => [ 'table2', 'table3' ] ],
119 [
120 'table3' => [ 'JOIN', 't2_id = t3_id' ],
121 'n' => [ 'LEFT JOIN', 't1_id = t2_id' ],
122 ],
123 'table1 LEFT JOIN (table2 JOIN table3 ON ((t2_id = t3_id))) ON ((t1_id = t2_id))'
124 ],
125 'join with degenerate parenthesized group' => [
126 [ 'table1', 'n' => [ 't2' => 'table2' ] ],
127 [
128 'n' => [ 'LEFT JOIN', 't1_id = t2_id' ],
129 ],
130 'table1 LEFT JOIN table2 t2 ON ((t1_id = t2_id))'
131 ],
132 ];
133 }
134
135 /**
136 * @dataProvider provideTableNamesWithIndexClauseOrJOIN
137 * @covers Wikimedia\Rdbms\Database::tableNamesWithIndexClauseOrJOIN
138 */
139 public function testTableNamesWithIndexClauseOrJOIN( $tables, $join_conds, $expect ) {
140 $clause = TestingAccessWrapper::newFromObject( $this->db )
141 ->tableNamesWithIndexClauseOrJOIN( $tables, [], [], $join_conds );
142 $this->assertSame( $expect, $clause );
143 }
144
145 /**
146 * @covers Wikimedia\Rdbms\Database::onTransactionIdle
147 * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
148 */
149 public function testTransactionIdle() {
150 $db = $this->db;
151
152 $db->setFlag( DBO_TRX );
153 $called = false;
154 $flagSet = null;
155 $db->onTransactionIdle(
156 function () use ( $db, &$flagSet, &$called ) {
157 $called = true;
158 $flagSet = $db->getFlag( DBO_TRX );
159 },
160 __METHOD__
161 );
162 $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
163 $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
164 $this->assertTrue( $called, 'Callback reached' );
165
166 $db->clearFlag( DBO_TRX );
167 $flagSet = null;
168 $db->onTransactionIdle(
169 function () use ( $db, &$flagSet ) {
170 $flagSet = $db->getFlag( DBO_TRX );
171 },
172 __METHOD__
173 );
174 $this->assertFalse( $flagSet, 'DBO_TRX off in callback' );
175 $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
176
177 $db->clearFlag( DBO_TRX );
178 $db->onTransactionIdle(
179 function () use ( $db ) {
180 $db->setFlag( DBO_TRX );
181 },
182 __METHOD__
183 );
184 $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
185 }
186
187 /**
188 * @covers Wikimedia\Rdbms\Database::onTransactionPreCommitOrIdle
189 * @covers Wikimedia\Rdbms\Database::runOnTransactionPreCommitCallbacks
190 */
191 public function testTransactionPreCommitOrIdle() {
192 $db = $this->getMockDB( [ 'isOpen' ] );
193 $db->method( 'isOpen' )->willReturn( true );
194 $db->clearFlag( DBO_TRX );
195
196 $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX is not set' );
197
198 $called = false;
199 $db->onTransactionPreCommitOrIdle(
200 function () use ( &$called ) {
201 $called = true;
202 },
203 __METHOD__
204 );
205 $this->assertTrue( $called, 'Called when idle' );
206
207 $db->begin( __METHOD__ );
208 $called = false;
209 $db->onTransactionPreCommitOrIdle(
210 function () use ( &$called ) {
211 $called = true;
212 },
213 __METHOD__
214 );
215 $this->assertFalse( $called, 'Not called when transaction is active' );
216 $db->commit( __METHOD__ );
217 $this->assertTrue( $called, 'Called when transaction is committed' );
218 }
219
220 /**
221 * @covers Wikimedia\Rdbms\Database::onTransactionPreCommitOrIdle
222 * @covers Wikimedia\Rdbms\Database::runOnTransactionPreCommitCallbacks
223 */
224 public function testTransactionPreCommitOrIdle_TRX() {
225 $db = $this->getMockDB( [ 'isOpen' ] );
226 $db->method( 'isOpen' )->willReturn( true );
227 $db->setFlag( DBO_TRX );
228
229 $lbFactory = LBFactorySingle::newFromConnection( $db );
230 // Ask for the connectin so that LB sets internal state
231 // about this connection being the master connection
232 $lb = $lbFactory->getMainLB();
233 $conn = $lb->openConnection( $lb->getWriterIndex() );
234 $this->assertSame( $db, $conn, 'Same DB instance' );
235 $this->assertTrue( $db->getFlag( DBO_TRX ), 'DBO_TRX is set' );
236
237 $called = false;
238 $db->onTransactionPreCommitOrIdle(
239 function () use ( &$called ) {
240 $called = true;
241 }
242 );
243 $this->assertFalse( $called, 'Not called when idle if DBO_TRX is set' );
244
245 $lbFactory->beginMasterChanges( __METHOD__ );
246 $this->assertFalse( $called, 'Not called when lb-transaction is active' );
247
248 $lbFactory->commitMasterChanges( __METHOD__ );
249 $this->assertTrue( $called, 'Called when lb-transaction is committed' );
250 }
251
252 /**
253 * @covers Wikimedia\Rdbms\Database::onTransactionResolution
254 * @covers Wikimedia\Rdbms\Database::runOnTransactionIdleCallbacks
255 */
256 public function testTransactionResolution() {
257 $db = $this->db;
258
259 $db->clearFlag( DBO_TRX );
260 $db->begin( __METHOD__ );
261 $called = false;
262 $db->onTransactionResolution( function () use ( $db, &$called ) {
263 $called = true;
264 $db->setFlag( DBO_TRX );
265 } );
266 $db->commit( __METHOD__ );
267 $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
268 $this->assertTrue( $called, 'Callback reached' );
269
270 $db->clearFlag( DBO_TRX );
271 $db->begin( __METHOD__ );
272 $called = false;
273 $db->onTransactionResolution( function () use ( $db, &$called ) {
274 $called = true;
275 $db->setFlag( DBO_TRX );
276 } );
277 $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
278 $this->assertFalse( $db->getFlag( DBO_TRX ), 'DBO_TRX restored to default' );
279 $this->assertTrue( $called, 'Callback reached' );
280 }
281
282 /**
283 * @covers Wikimedia\Rdbms\Database::setTransactionListener
284 */
285 public function testTransactionListener() {
286 $db = $this->db;
287
288 $db->setTransactionListener( 'ping', function () use ( $db, &$called ) {
289 $called = true;
290 } );
291
292 $called = false;
293 $db->begin( __METHOD__ );
294 $db->commit( __METHOD__ );
295 $this->assertTrue( $called, 'Callback reached' );
296
297 $called = false;
298 $db->begin( __METHOD__ );
299 $db->commit( __METHOD__ );
300 $this->assertTrue( $called, 'Callback still reached' );
301
302 $called = false;
303 $db->begin( __METHOD__ );
304 $db->rollback( __METHOD__ );
305 $this->assertTrue( $called, 'Callback reached' );
306
307 $db->setTransactionListener( 'ping', null );
308 $called = false;
309 $db->begin( __METHOD__ );
310 $db->commit( __METHOD__ );
311 $this->assertFalse( $called, 'Callback not reached' );
312 }
313
314 /**
315 * Use this mock instead of DatabaseTestHelper for cases where
316 * DatabaseTestHelper is too inflexibile due to mocking too much
317 * or being too restrictive about fname matching (e.g. for tests
318 * that assert behaviour when the name is a mismatch, we need to
319 * catch the error here instead of there).
320 *
321 * @return Database
322 */
323 private function getMockDB( $methods = [] ) {
324 static $abstractMethods = [
325 'fetchAffectedRowCount',
326 'closeConnection',
327 'dataSeek',
328 'doQuery',
329 'fetchObject', 'fetchRow',
330 'fieldInfo', 'fieldName',
331 'getSoftwareLink', 'getServerVersion',
332 'getType',
333 'indexInfo',
334 'insertId',
335 'lastError', 'lastErrno',
336 'numFields', 'numRows',
337 'open',
338 'strencode',
339 ];
340 $db = $this->getMockBuilder( Database::class )
341 ->disableOriginalConstructor()
342 ->setMethods( array_values( array_unique( array_merge(
343 $abstractMethods,
344 $methods
345 ) ) ) )
346 ->getMock();
347 $wdb = TestingAccessWrapper::newFromObject( $db );
348 $wdb->trxProfiler = new TransactionProfiler();
349 $wdb->connLogger = new \Psr\Log\NullLogger();
350 $wdb->queryLogger = new \Psr\Log\NullLogger();
351 return $db;
352 }
353
354 /**
355 * @covers Wikimedia\Rdbms\Database::flushSnapshot
356 */
357 public function testFlushSnapshot() {
358 $db = $this->getMockDB( [ 'isOpen' ] );
359 $db->method( 'isOpen' )->willReturn( true );
360
361 $db->flushSnapshot( __METHOD__ ); // ok
362 $db->flushSnapshot( __METHOD__ ); // ok
363
364 $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
365 $db->query( 'SELECT 1', __METHOD__ );
366 $this->assertTrue( (bool)$db->trxLevel(), "Transaction started." );
367 $db->flushSnapshot( __METHOD__ ); // ok
368 $db->restoreFlags( $db::RESTORE_PRIOR );
369
370 $this->assertFalse( (bool)$db->trxLevel(), "Transaction cleared." );
371 }
372
373 public function testGetScopedLock() {
374 $db = $this->getMockDB( [ 'isOpen' ] );
375 $db->method( 'isOpen' )->willReturn( true );
376
377 $db->setFlag( DBO_TRX );
378 try {
379 $this->badLockingMethodImplicit( $db );
380 } catch ( RunTimeException $e ) {
381 $this->assertTrue( $db->trxLevel() > 0, "Transaction not committed." );
382 }
383 $db->clearFlag( DBO_TRX );
384 $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
385 $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
386
387 try {
388 $this->badLockingMethodExplicit( $db );
389 } catch ( RunTimeException $e ) {
390 $this->assertTrue( $db->trxLevel() > 0, "Transaction not committed." );
391 }
392 $db->rollback( __METHOD__, IDatabase::FLUSHING_ALL_PEERS );
393 $this->assertTrue( $db->lockIsFree( 'meow', __METHOD__ ) );
394 }
395
396 private function badLockingMethodImplicit( IDatabase $db ) {
397 $lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
398 $db->query( "SELECT 1" ); // trigger DBO_TRX
399 throw new RunTimeException( "Uh oh!" );
400 }
401
402 private function badLockingMethodExplicit( IDatabase $db ) {
403 $lock = $db->getScopedLockAndFlush( 'meow', __METHOD__, 1 );
404 $db->begin( __METHOD__ );
405 throw new RunTimeException( "Uh oh!" );
406 }
407
408 /**
409 * @covers Wikimedia\Rdbms\Database::getFlag
410 * @covers Wikimedia\Rdbms\Database::setFlag
411 * @covers Wikimedia\Rdbms\Database::restoreFlags
412 */
413 public function testFlagSetting() {
414 $db = $this->db;
415 $origTrx = $db->getFlag( DBO_TRX );
416 $origSsl = $db->getFlag( DBO_SSL );
417
418 $origTrx
419 ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
420 : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
421 $this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
422
423 $origSsl
424 ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
425 : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
426 $this->assertEquals( !$origSsl, $db->getFlag( DBO_SSL ) );
427
428 $db->restoreFlags( $db::RESTORE_INITIAL );
429 $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
430 $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
431
432 $origTrx
433 ? $db->clearFlag( DBO_TRX, $db::REMEMBER_PRIOR )
434 : $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
435 $origSsl
436 ? $db->clearFlag( DBO_SSL, $db::REMEMBER_PRIOR )
437 : $db->setFlag( DBO_SSL, $db::REMEMBER_PRIOR );
438
439 $db->restoreFlags();
440 $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
441 $this->assertEquals( !$origTrx, $db->getFlag( DBO_TRX ) );
442
443 $db->restoreFlags();
444 $this->assertEquals( $origSsl, $db->getFlag( DBO_SSL ) );
445 $this->assertEquals( $origTrx, $db->getFlag( DBO_TRX ) );
446 }
447
448 /**
449 * @covers Wikimedia\Rdbms\Database::tablePrefix
450 * @covers Wikimedia\Rdbms\Database::dbSchema
451 */
452 public function testMutators() {
453 $old = $this->db->tablePrefix();
454 $this->assertInternalType( 'string', $old, 'Prefix is string' );
455 $this->assertEquals( $old, $this->db->tablePrefix(), "Prefix unchanged" );
456 $this->assertEquals( $old, $this->db->tablePrefix( 'xxx' ) );
457 $this->assertEquals( 'xxx', $this->db->tablePrefix(), "Prefix set" );
458 $this->db->tablePrefix( $old );
459 $this->assertNotEquals( 'xxx', $this->db->tablePrefix() );
460
461 $old = $this->db->dbSchema();
462 $this->assertInternalType( 'string', $old, 'Schema is string' );
463 $this->assertEquals( $old, $this->db->dbSchema(), "Schema unchanged" );
464 $this->assertEquals( $old, $this->db->dbSchema( 'xxx' ) );
465 $this->assertEquals( 'xxx', $this->db->dbSchema(), "Schema set" );
466 $this->db->dbSchema( $old );
467 $this->assertNotEquals( 'xxx', $this->db->dbSchema() );
468 }
469 }