FileJournal tests
[lhc/web/wiklou.git] / tests / phpunit / includes / filebackend / filejournal / DBFileJournalIntegrationTest.php
1 <?php
2
3 use MediaWiki\MediaWikiServices;
4 use Wikimedia\Timestamp\ConvertibleTimestamp;
5
6 /**
7 * @coversDefaultClass DBFileJournal
8 * @covers ::__construct
9 * @covers ::getMasterDB
10 * @group Database
11 */
12 class DBFileJournalIntegrationTest extends MediaWikiIntegrationTestCase {
13 public function addDBDataOnce() {
14 global $IP;
15 $db = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_MASTER );
16 if ( $db->getType() !== 'mysql' ) {
17 return;
18 }
19 if ( !$db->tableExists( 'filejournal' ) ) {
20 $db->sourceFile( "$IP/maintenance/archives/patch-filejournal.sql" );
21 }
22 }
23
24 protected function setUp() {
25 parent::setUp();
26
27 $db = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_MASTER );
28 if ( $db->getType() !== 'mysql' ) {
29 $this->markTestSkipped( 'No filejournal schema available for this database type' );
30 }
31
32 $this->tablesUsed[] = 'filejournal';
33 }
34
35 private function getJournal( $options = [] ) {
36 return FileJournal::factory(
37 $options + [ 'class' => DBFileJournal::class, 'domain' => wfWikiID() ],
38 'local-backend' );
39 }
40
41 /**
42 * @covers ::doLogChangeBatch
43 */
44 public function testDoLogChangeBatch_exceptionDbConnect() {
45 $journal = $this->getJournal( [ 'domain' => 'no-such-domain' ] );
46
47 $this->assertEquals(
48 StatusValue::newFatal( 'filejournal-fail-dbconnect', 'local-backend' ),
49 $journal->logChangeBatch( [ [] ], 'batch' ) );
50 }
51
52 /**
53 * @covers ::doLogChangeBatch
54 */
55 public function testDoLogChangeBatch_exceptionDbQuery() {
56 MediaWikiServices::getInstance()->getConfiguredReadOnlyMode()->setReason( 'testing' );
57
58 $journal = $this->getJournal();
59
60 $this->assertEquals(
61 StatusValue::newFatal( 'filejournal-fail-dbquery', 'local-backend' ),
62 $journal->logChangeBatch(
63 [ [ 'op' => null, 'path' => '', 'newSha1' => false ] ], 'batch' ) );
64 }
65
66 /**
67 * @covers ::doLogChangeBatch
68 * @covers ::doGetCurrentPosition
69 */
70 public function testDoGetCurrentPosition() {
71 $journal = $this->getJournal();
72
73 $this->assertNull( $journal->getCurrentPosition() );
74
75 $journal->logChangeBatch(
76 [ [ 'op' => 'create', 'path' => '/path', 'newSha1' => false ] ], 'batch1' );
77
78 $this->assertSame( '1', $journal->getCurrentPosition() );
79
80 $journal->logChangeBatch(
81 [ [ 'op' => 'create', 'path' => '/path', 'newSha1' => false ] ], 'batch2' );
82
83 $this->assertSame( '2', $journal->getCurrentPosition() );
84 }
85
86 /**
87 * @covers ::doLogChangeBatch
88 * @covers ::doGetPositionAtTime
89 */
90 public function testDoGetPositionAtTime() {
91 $journal = $this->getJournal();
92
93 $now = time();
94
95 $this->assertFalse( $journal->getPositionAtTime( $now ) );
96
97 ConvertibleTimestamp::setFakeTime( $now - 86400 );
98
99 $journal->logChangeBatch(
100 [ [ 'op' => 'create', 'path' => '/path', 'newSha1' => false ] ], 'batch1' );
101
102 ConvertibleTimestamp::setFakeTime( $now - 3600 );
103
104 $journal->logChangeBatch(
105 [ [ 'op' => 'create', 'path' => '/path', 'newSha1' => false ] ], 'batch2' );
106
107 $this->assertFalse( $journal->getPositionAtTime( $now - 86401 ) );
108 $this->assertSame( '1', $journal->getPositionAtTime( $now - 86400 ) );
109 $this->assertSame( '1', $journal->getPositionAtTime( $now - 3601 ) );
110 $this->assertSame( '2', $journal->getPositionAtTime( $now - 3600 ) );
111 }
112
113 /**
114 * @param int $expectedStart First index expected to be returned (0-based)
115 * @param int|null $expectedCount Number of entries expected to be returned (null for all)
116 * @param string|null|false $expectedNext Expected value of $next, or false not to pass
117 * @param array $args If any third argument is present, $next will also be tested
118 * @dataProvider provideDoGetChangeEntries
119 * @covers ::doLogChangeBatch
120 * @covers ::doGetChangeEntries
121 */
122 public function testDoGetChangeEntries(
123 $expectedStart, $expectedCount, $expectedNext, array $args
124 ) {
125 $journal = $this->getJournal();
126
127 $i = 0;
128 $makeExpectedEntry = function ( $op, $path, $newSha1, $batch, $time ) use ( &$i ) {
129 $i++;
130 return [
131 'id' => (string)$i,
132 'batch_uuid' => $batch,
133 'backend' => 'local-backend',
134 'path' => $path,
135 'op' => $op ?? '',
136 'new_sha1' => $newSha1 !== false ? $newSha1 : '0',
137 'timestamp' => ConvertibleTimestamp::convert( TS_MW, $time ),
138 ];
139 };
140
141 $expectedEntries = [];
142
143 $now = time();
144
145 ConvertibleTimestamp::setFakeTime( $now - 3600 );
146 $changes = [
147 [ 'op' => 'create', 'path' => '/path1',
148 'newSha1' => base_convert( sha1( 'a' ), 16, 36 ) ],
149 [ 'op' => 'delete', 'path' => '/path2', 'newSha1' => false ],
150 [ 'op' => 'null', 'path' => '', 'newSha1' => false ],
151 ];
152 $this->assertEquals( StatusValue::newGood(),
153 $journal->logChangeBatch( $changes, 'batch1' ) );
154 foreach ( $changes as $change ) {
155 $expectedEntries[] = $makeExpectedEntry(
156 ...array_merge( array_values( $change ), [ 'batch1', $now - 3600 ] ) );
157 }
158
159 ConvertibleTimestamp::setFakeTime( $now - 60 );
160 $change = [ 'op' => 'update', 'path' => '/path1',
161 'newSha1' => base_convert( sha1( 'b' ), 16, 36 ) ];
162 $this->assertEquals(
163 StatusValue::newGood(), $journal->logChangeBatch( [ $change ], 'batch2' ) );
164 $expectedEntries[] = $makeExpectedEntry(
165 ...array_merge( array_values( $change ), [ 'batch2', $now - 60 ] ) );
166
167 if ( $expectedNext === false ) {
168 $this->assertSame(
169 array_slice( $expectedEntries, $expectedStart, $expectedCount ),
170 $journal->getChangeEntries( ...$args )
171 );
172 } else {
173 $next = false;
174 $this->assertSame(
175 array_slice( $expectedEntries, $expectedStart, $expectedCount ),
176 $journal->getChangeEntries( $args[0], $args[1], $next )
177 );
178 $this->assertSame( $expectedNext, $next );
179 }
180 }
181
182 public static function provideDoGetChangeEntries() {
183 return [
184 'No args' => [ 0, 4, false, [] ],
185 'null' => [ 0, 4, false, [ null ] ],
186 '1' => [ 0, 4, false, [ 1 ] ],
187 '2' => [ 1, 3, false, [ 2 ] ],
188 '4' => [ 3, 1, false, [ 4 ] ],
189 '5' => [ 0, 0, false, [ 5 ] ],
190 'null, 0' => [ 0, 4, null, [ null, 0 ] ],
191 '1, 0' => [ 0, 4, null, [ 1, 0 ] ],
192 '2, 0' => [ 1, 3, null, [ 2, 0 ] ],
193 '4, 0' => [ 3, 1, null, [ 4, 0 ] ],
194 '5, 0' => [ 0, 0, null, [ 5, 0 ] ],
195 '1, 1' => [ 0, 1, '2', [ 1, 1 ] ],
196 '1, 2' => [ 0, 2, '3', [ 1, 2 ] ],
197 '1, 4' => [ 0, 4, null, [ 1, 4 ] ],
198 '1, 5' => [ 0, 4, null, [ 1, 5 ] ],
199 '2, 2' => [ 1, 2, '4', [ 2, 2 ] ],
200 '1, 2 with no $next' => [ 0, 2, false, [ 1, 2 ] ],
201 ];
202 }
203
204 /**
205 * @covers ::doPurgeOldLogs
206 */
207 public function testDoPurgeOldLogs_noop() {
208 // If we tried to access the database, it would throw, because the domain doesn't exist
209 $journal = $this->getJournal( [ 'domain' => 'no-such-domain' ] );
210 $this->assertEquals( StatusValue::newGood(), $journal->purgeOldLogs() );
211 }
212
213 /**
214 * @covers ::doPurgeOldLogs
215 * @covers ::doLogChangeBatch
216 * @covers ::doGetChangeEntries
217 */
218 public function testDoPurgeOldLogs() {
219 $journal = $this->getJournal( [ 'ttlDays' => 1 ] );
220 $now = time();
221
222 // One day and one second ago
223 ConvertibleTimestamp::setFakeTime( $now - 86401 );
224 $this->assertEquals( StatusValue::newGood(), $journal->logChangeBatch(
225 [ [ 'op' => 'null', 'path' => '', 'newSha1' => false ] ], 'batch1' ) );
226
227 // One day ago exactly, won't get purged
228 ConvertibleTimestamp::setFakeTime( $now - 86400 );
229 $this->assertEquals( StatusValue::newGood(), $journal->logChangeBatch(
230 [ [ 'op' => 'null', 'path' => '', 'newSha1' => false ] ], 'batch2' ) );
231
232 ConvertibleTimestamp::setFakeTime( $now );
233 $this->assertCount( 2, $journal->getChangeEntries() );
234 $journal->purgeOldLogs();
235 $this->assertCount( 1, $journal->getChangeEntries() );
236 }
237 }