1<?php
2
3use MediaWiki\MediaWikiServices;
4
5/**
6 * @covers ChangeTags
7 * @group Database
8 */
9class ChangeTagsTest extends MediaWikiIntegrationTestCase {
10
11	protected function setUp() : void {
12		parent::setUp();
13
14		$this->tablesUsed[] = 'change_tag';
15		$this->tablesUsed[] = 'change_tag_def';
16
17		// Truncate these to avoid the supposed-to-be-unused IDs in tests here turning
18		// out to be used, leading ChangeTags::updateTags() to pick up bogus rc_id,
19		// log_id, or rev_id values and run into unique constraint violations.
20		$this->tablesUsed[] = 'recentchanges';
21		$this->tablesUsed[] = 'logging';
22		$this->tablesUsed[] = 'revision';
23		$this->tablesUsed[] = 'archive';
24	}
25
26	public function tearDown() : void {
27		ChangeTags::$avoidReopeningTablesForTesting = false;
28		parent::tearDown();
29	}
30
31	private function emptyChangeTagsTables() {
32		$dbw = wfGetDB( DB_MASTER );
33		$dbw->delete( 'change_tag', '*' );
34		$dbw->delete( 'change_tag_def', '*' );
35	}
36
37	// TODO most methods are not tested
38
39	/** @dataProvider provideModifyDisplayQuery */
40	public function testModifyDisplayQuery(
41		$origQuery,
42		$filter_tag,
43		$useTags,
44		$avoidReopeningTables,
45		$modifiedQuery
46	) {
47		$this->setMwGlobals( 'wgUseTagFilter', $useTags );
48
49		if ( $avoidReopeningTables && $this->db->getType() !== 'mysql' ) {
50			$this->markTestSkipped( 'MySQL only' );
51		}
52
53		ChangeTags::$avoidReopeningTablesForTesting = $avoidReopeningTables;
54
55		$rcId = 123;
56		ChangeTags::updateTags( [ 'foo', 'bar' ], [], $rcId );
57		// HACK resolve deferred group concats (see comment in provideModifyDisplayQuery)
58		if ( isset( $modifiedQuery['fields']['ts_tags'] ) ) {
59			$modifiedQuery['fields']['ts_tags'] = wfGetDB( DB_REPLICA )
60				->buildGroupConcatField( ...$modifiedQuery['fields']['ts_tags'] );
61		}
62		if ( isset( $modifiedQuery['exception'] ) ) {
63			$this->expectException( $modifiedQuery['exception'] );
64		}
65		ChangeTags::modifyDisplayQuery(
66			$origQuery['tables'],
67			$origQuery['fields'],
68			$origQuery['conds'],
69			$origQuery['join_conds'],
70			$origQuery['options'],
71			$filter_tag
72		);
73		if ( !isset( $modifiedQuery['exception'] ) ) {
74			$this->assertArrayEquals(
75				$modifiedQuery,
76				$origQuery,
77				/* ordered = */ false,
78				/* named = */ true
79			);
80		}
81	}
82
83	public function provideModifyDisplayQuery() {
84		// HACK if we call $dbr->buildGroupConcatField() now, it will return the wrong table names
85		// We have to have the test runner call it instead
86		$baseConcats = [ ',', [ 'change_tag', 'change_tag_def' ], 'ctd_name' ];
87		$joinConds = [ 'change_tag_def' => [ 'JOIN', 'ct_tag_id=ctd_id' ] ];
88		$groupConcats = [
89			'recentchanges' => array_merge( $baseConcats, [ 'ct_rc_id=rc_id', $joinConds ] ),
90			'logging' => array_merge( $baseConcats, [ 'ct_log_id=log_id', $joinConds ] ),
91			'revision' => array_merge( $baseConcats, [ 'ct_rev_id=rev_id', $joinConds ] ),
92			'archive' => array_merge( $baseConcats, [ 'ct_rev_id=ar_rev_id', $joinConds ] ),
93		];
94
95		return [
96			'simple recentchanges query' => [
97				[
98					'tables' => [ 'recentchanges' ],
99					'fields' => [ 'rc_id', 'rc_timestamp' ],
100					'conds' => [ "rc_timestamp > '20170714183203'" ],
101					'join_conds' => [],
102					'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
103				],
104				'', // no tag filter
105				true, // tag filtering enabled
106				false, // not avoiding reopening tables
107				[
108					'tables' => [ 'recentchanges' ],
109					'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
110					'conds' => [ "rc_timestamp > '20170714183203'" ],
111					'join_conds' => [],
112					'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
113				]
114			],
115			'simple query with strings' => [
116				[
117					'tables' => 'recentchanges',
118					'fields' => 'rc_id',
119					'conds' => "rc_timestamp > '20170714183203'",
120					'join_conds' => [],
121					'options' => 'ORDER BY rc_timestamp DESC',
122				],
123				'', // no tag filter
124				true, // tag filtering enabled
125				false, // not avoiding reopening tables
126				[
127					'tables' => [ 'recentchanges' ],
128					'fields' => [ 'rc_id', 'ts_tags' => $groupConcats['recentchanges'] ],
129					'conds' => [ "rc_timestamp > '20170714183203'" ],
130					'join_conds' => [],
131					'options' => [ 'ORDER BY rc_timestamp DESC' ],
132				]
133			],
134			'recentchanges query with single tag filter' => [
135				[
136					'tables' => [ 'recentchanges' ],
137					'fields' => [ 'rc_id', 'rc_timestamp' ],
138					'conds' => [ "rc_timestamp > '20170714183203'" ],
139					'join_conds' => [],
140					'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
141				],
142				'foo',
143				true, // tag filtering enabled
144				false, // not avoiding reopening tables
145				[
146					'tables' => [ 'recentchanges', 'change_tag' ],
147					'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
148					'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
149					'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rc_id=rc_id' ] ],
150					'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
151				]
152			],
153			'logging query with single tag filter and strings' => [
154				[
155					'tables' => 'logging',
156					'fields' => 'log_id',
157					'conds' => "log_timestamp > '20170714183203'",
158					'join_conds' => [],
159					'options' => 'ORDER BY log_timestamp DESC',
160				],
161				'foo',
162				true, // tag filtering enabled
163				false, // not avoiding reopening tables
164				[
165					'tables' => [ 'logging', 'change_tag' ],
166					'fields' => [ 'log_id', 'ts_tags' => $groupConcats['logging'] ],
167					'conds' => [ "log_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
168					'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_log_id=log_id' ] ],
169					'options' => [ 'ORDER BY log_timestamp DESC' ],
170				]
171			],
172			'revision query with single tag filter' => [
173				[
174					'tables' => [ 'revision' ],
175					'fields' => [ 'rev_id', 'rev_timestamp' ],
176					'conds' => [ "rev_timestamp > '20170714183203'" ],
177					'join_conds' => [],
178					'options' => [ 'ORDER BY' => 'rev_timestamp DESC' ],
179				],
180				'foo',
181				true, // tag filtering enabled
182				false, // not avoiding reopening tables
183				[
184					'tables' => [ 'revision', 'change_tag' ],
185					'fields' => [ 'rev_id', 'rev_timestamp', 'ts_tags' => $groupConcats['revision'] ],
186					'conds' => [ "rev_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
187					'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rev_id=rev_id' ] ],
188					'options' => [ 'ORDER BY' => 'rev_timestamp DESC' ],
189				]
190			],
191			'archive query with single tag filter' => [
192				[
193					'tables' => [ 'archive' ],
194					'fields' => [ 'ar_id', 'ar_timestamp' ],
195					'conds' => [ "ar_timestamp > '20170714183203'" ],
196					'join_conds' => [],
197					'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
198				],
199				'foo',
200				true, // tag filtering enabled
201				false, // not avoiding reopening tables
202				[
203					'tables' => [ 'archive', 'change_tag' ],
204					'fields' => [ 'ar_id', 'ar_timestamp', 'ts_tags' => $groupConcats['archive'] ],
205					'conds' => [ "ar_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
206					'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rev_id=ar_rev_id' ] ],
207					'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
208				]
209			],
210			'archive query with single tag filter, avoiding reopening tables' => [
211				[
212					'tables' => [ 'archive' ],
213					'fields' => [ 'ar_id', 'ar_timestamp' ],
214					'conds' => [ "ar_timestamp > '20170714183203'" ],
215					'join_conds' => [],
216					'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
217				],
218				'foo',
219				true, // tag filtering enabled
220				true, // avoid reopening tables
221				[
222					'tables' => [ 'archive', 'change_tag_for_display_query' ],
223					'fields' => [ 'ar_id', 'ar_timestamp', 'ts_tags' => $groupConcats['archive'] ],
224					'conds' => [ "ar_timestamp > '20170714183203'", 'ct_tag_id' => [ 1 ] ],
225					'join_conds' => [ 'change_tag_for_display_query' => [ 'JOIN', 'ct_rev_id=ar_rev_id' ] ],
226					'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
227				]
228			],
229			'unsupported table name throws exception (even without tag filter)' => [
230				[
231					'tables' => [ 'foobar' ],
232					'fields' => [ 'fb_id', 'fb_timestamp' ],
233					'conds' => [ "fb_timestamp > '20170714183203'" ],
234					'join_conds' => [],
235					'options' => [ 'ORDER BY' => 'fb_timestamp DESC' ],
236				],
237				'',
238				true, // tag filtering enabled
239				false, // not avoiding reopening tables
240				[ 'exception' => MWException::class ]
241			],
242			'tag filter ignored when tag filtering is disabled' => [
243				[
244					'tables' => [ 'archive' ],
245					'fields' => [ 'ar_id', 'ar_timestamp' ],
246					'conds' => [ "ar_timestamp > '20170714183203'" ],
247					'join_conds' => [],
248					'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
249				],
250				'foo',
251				false, // tag filtering disabled
252				false, // not avoiding reopening tables
253				[
254					'tables' => [ 'archive' ],
255					'fields' => [ 'ar_id', 'ar_timestamp', 'ts_tags' => $groupConcats['archive'] ],
256					'conds' => [ "ar_timestamp > '20170714183203'" ],
257					'join_conds' => [],
258					'options' => [ 'ORDER BY' => 'ar_timestamp DESC' ],
259				]
260			],
261			'recentchanges query with multiple tag filter' => [
262				[
263					'tables' => [ 'recentchanges' ],
264					'fields' => [ 'rc_id', 'rc_timestamp' ],
265					'conds' => [ "rc_timestamp > '20170714183203'" ],
266					'join_conds' => [],
267					'options' => [ 'ORDER BY' => 'rc_timestamp DESC' ],
268				],
269				[ 'foo', 'bar' ],
270				true, // tag filtering enabled
271				false, // not avoiding reopening tables
272				[
273					'tables' => [ 'recentchanges', 'change_tag' ],
274					'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
275					'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1, 2 ] ],
276					'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rc_id=rc_id' ] ],
277					'options' => [ 'ORDER BY' => 'rc_timestamp DESC', 'DISTINCT' ],
278				]
279			],
280			'recentchanges query with multiple tag filter that already has DISTINCT' => [
281				[
282					'tables' => [ 'recentchanges' ],
283					'fields' => [ 'rc_id', 'rc_timestamp' ],
284					'conds' => [ "rc_timestamp > '20170714183203'" ],
285					'join_conds' => [],
286					'options' => [ 'DISTINCT', 'ORDER BY' => 'rc_timestamp DESC' ],
287				],
288				[ 'foo', 'bar' ],
289				true, // tag filtering enabled
290				false, // not avoiding reopening tables
291				[
292					'tables' => [ 'recentchanges', 'change_tag' ],
293					'fields' => [ 'rc_id', 'rc_timestamp', 'ts_tags' => $groupConcats['recentchanges'] ],
294					'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1, 2 ] ],
295					'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rc_id=rc_id' ] ],
296					'options' => [ 'DISTINCT', 'ORDER BY' => 'rc_timestamp DESC' ],
297				]
298			],
299			'recentchanges query with multiple tag filter with strings' => [
300				[
301					'tables' => 'recentchanges',
302					'fields' => 'rc_id',
303					'conds' => "rc_timestamp > '20170714183203'",
304					'join_conds' => [],
305					'options' => 'ORDER BY rc_timestamp DESC',
306				],
307				[ 'foo', 'bar' ],
308				true, // tag filtering enabled
309				false, // not avoiding reopening tables
310				[
311					'tables' => [ 'recentchanges', 'change_tag' ],
312					'fields' => [ 'rc_id', 'ts_tags' => $groupConcats['recentchanges'] ],
313					'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1, 2 ] ],
314					'join_conds' => [ 'change_tag' => [ 'JOIN', 'ct_rc_id=rc_id' ] ],
315					'options' => [ 'ORDER BY rc_timestamp DESC', 'DISTINCT' ],
316				]
317			],
318			'recentchanges query with multiple tag filter with strings, avoiding reopening tables' => [
319				[
320					'tables' => 'recentchanges',
321					'fields' => 'rc_id',
322					'conds' => "rc_timestamp > '20170714183203'",
323					'join_conds' => [],
324					'options' => 'ORDER BY rc_timestamp DESC',
325				],
326				[ 'foo', 'bar' ],
327				true, // tag filtering enabled
328				true, // avoid reopening tables
329				[
330					'tables' => [ 'recentchanges', 'change_tag_for_display_query' ],
331					'fields' => [ 'rc_id', 'ts_tags' => $groupConcats['recentchanges'] ],
332					'conds' => [ "rc_timestamp > '20170714183203'", 'ct_tag_id' => [ 1, 2 ] ],
333					'join_conds' => [ 'change_tag_for_display_query' => [ 'JOIN', 'ct_rc_id=rc_id' ] ],
334					'options' => [ 'ORDER BY rc_timestamp DESC', 'DISTINCT' ],
335				]
336			],
337		];
338	}
339
340	public static function dataGetSoftwareTags() {
341		return [
342			[
343				[
344					'mw-contentModelChange' => true,
345					'mw-redirect' => true,
346					'mw-rollback' => true,
347					'mw-blank' => true,
348					'mw-replace' => true
349				],
350				[
351					'mw-rollback',
352					'mw-replace',
353					'mw-blank'
354				]
355			],
356
357			[
358				[
359					'mw-contentmodelchanged' => true,
360					'mw-replace' => true,
361					'mw-new-redirects' => true,
362					'mw-changed-redirect-target' => true,
363					'mw-rolback' => true,
364					'mw-blanking' => false
365				],
366				[
367					'mw-replace',
368					'mw-changed-redirect-target'
369				]
370			],
371
372			[
373				[
374					null,
375					false,
376					'Lorem ipsum',
377					'mw-translation'
378				],
379				[]
380			],
381
382			[
383				[],
384				[]
385			]
386		];
387	}
388
389	/**
390	 * @dataProvider dataGetSoftwareTags
391	 * @covers ChangeTags::getSoftwareTags
392	 */
393	public function testGetSoftwareTags( $softwareTags, $expected ) {
394		$this->setMwGlobals( 'wgSoftwareTags', $softwareTags );
395
396		$actual = ChangeTags::getSoftwareTags();
397		// Order of tags in arrays is not important
398		sort( $expected );
399		sort( $actual );
400		$this->assertEquals( $expected, $actual );
401	}
402
403	public function testUpdateTags() {
404		// FIXME: fails under postgres
405		$this->markTestSkippedIfDbType( 'postgres' );
406
407		$this->emptyChangeTagsTables();
408
409		$rcId = 123;
410		$revId = 341;
411		ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId, $revId );
412
413		$dbr = wfGetDB( DB_REPLICA );
414
415		$expected = [
416			(object)[
417				'ctd_name' => 'tag1',
418				'ctd_id' => 1,
419				'ctd_count' => 1
420			],
421			(object)[
422				'ctd_name' => 'tag2',
423				'ctd_id' => 2,
424				'ctd_count' => 1
425			],
426		];
427		$res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' );
428		$this->assertEquals( $expected, iterator_to_array( $res, false ) );
429
430		$expected2 = [
431			(object)[
432				'ct_tag_id' => 1,
433				'ct_rc_id' => 123,
434				'ct_rev_id' => 341
435			],
436			(object)[
437				'ct_tag_id' => 2,
438				'ct_rc_id' => 123,
439				'ct_rev_id' => 341
440			],
441		];
442		$res2 = $dbr->select( 'change_tag', [ 'ct_tag_id', 'ct_rc_id', 'ct_rev_id' ], '' );
443		$this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
444
445		$rcId = 124;
446		$revId = 342;
447		ChangeTags::updateTags( [ 'tag1' ], [], $rcId, $revId );
448		ChangeTags::updateTags( [ 'tag3' ], [], $rcId, $revId );
449
450		$expected = [
451			(object)[
452				'ctd_name' => 'tag1',
453				'ctd_id' => 1,
454				'ctd_count' => 2
455			],
456			(object)[
457				'ctd_name' => 'tag2',
458				'ctd_id' => 2,
459				'ctd_count' => 1
460			],
461			(object)[
462				'ctd_name' => 'tag3',
463				'ctd_id' => 3,
464				'ctd_count' => 1
465			],
466		];
467		$res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' );
468		$this->assertEquals( $expected, iterator_to_array( $res, false ) );
469
470		$expected2 = [
471			(object)[
472				'ct_tag_id' => 1,
473				'ct_rc_id' => 123,
474				'ct_rev_id' => 341
475			],
476			(object)[
477				'ct_tag_id' => 1,
478				'ct_rc_id' => 124,
479				'ct_rev_id' => 342
480			],
481			(object)[
482				'ct_tag_id' => 2,
483				'ct_rc_id' => 123,
484				'ct_rev_id' => 341
485			],
486			(object)[
487				'ct_tag_id' => 3,
488				'ct_rc_id' => 124,
489				'ct_rev_id' => 342
490			],
491		];
492		$res2 = $dbr->select( 'change_tag', [ 'ct_tag_id', 'ct_rc_id', 'ct_rev_id' ], '' );
493		$this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
494	}
495
496	public function testUpdateTagsSkipDuplicates() {
497		// FIXME: fails under postgres
498		$this->markTestSkippedIfDbType( 'postgres' );
499
500		$this->emptyChangeTagsTables();
501
502		$rcId = 123;
503		ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
504		ChangeTags::updateTags( [ 'tag2', 'tag3' ], [], $rcId );
505
506		$dbr = wfGetDB( DB_REPLICA );
507
508		$expected = [
509			(object)[
510				'ctd_name' => 'tag1',
511				'ctd_id' => 1,
512				'ctd_count' => 1
513			],
514			(object)[
515				'ctd_name' => 'tag2',
516				'ctd_id' => 2,
517				'ctd_count' => 1
518			],
519			(object)[
520				'ctd_name' => 'tag3',
521				'ctd_id' => 3,
522				'ctd_count' => 1
523			],
524		];
525		$res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' );
526		$this->assertEquals( $expected, iterator_to_array( $res, false ) );
527
528		$expected2 = [
529			(object)[
530				'ct_tag_id' => 1,
531				'ct_rc_id' => 123
532			],
533			(object)[
534				'ct_tag_id' => 2,
535				'ct_rc_id' => 123
536			],
537			(object)[
538				'ct_tag_id' => 3,
539				'ct_rc_id' => 123
540			],
541		];
542		$res2 = $dbr->select( 'change_tag', [ 'ct_tag_id', 'ct_rc_id' ], '' );
543		$this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
544	}
545
546	public function testUpdateTagsDoNothingOnRepeatedCall() {
547		// FIXME: fails under postgres
548		$this->markTestSkippedIfDbType( 'postgres' );
549
550		$this->emptyChangeTagsTables();
551
552		$rcId = 123;
553		ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
554		$res = ChangeTags::updateTags( [ 'tag2', 'tag1' ], [], $rcId );
555		$this->assertEquals( [ [], [], [ 'tag1', 'tag2' ] ], $res );
556
557		$dbr = wfGetDB( DB_REPLICA );
558
559		$expected = [
560			(object)[
561				'ctd_name' => 'tag1',
562				'ctd_id' => 1,
563				'ctd_count' => 1
564			],
565			(object)[
566				'ctd_name' => 'tag2',
567				'ctd_id' => 2,
568				'ctd_count' => 1
569			],
570		];
571		$res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' );
572		$this->assertEquals( $expected, iterator_to_array( $res, false ) );
573
574		$expected2 = [
575			(object)[
576				'ct_tag_id' => 1,
577				'ct_rc_id' => 123
578			],
579			(object)[
580				'ct_tag_id' => 2,
581				'ct_rc_id' => 123
582			],
583		];
584		$res2 = $dbr->select( 'change_tag', [ 'ct_tag_id', 'ct_rc_id' ], '' );
585		$this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
586	}
587
588	public function testDeleteTags() {
589		$this->emptyChangeTagsTables();
590		MediaWikiServices::getInstance()->resetServiceForTesting( 'NameTableStoreFactory' );
591
592		$rcId = 123;
593		ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
594
595		ChangeTags::updateTags( [], [ 'tag2' ], $rcId );
596
597		$dbr = wfGetDB( DB_REPLICA );
598
599		$expected = [
600			(object)[
601				'ctd_name' => 'tag1',
602				'ctd_id' => 1,
603				'ctd_count' => 1
604			],
605		];
606		$res = $dbr->select( 'change_tag_def', [ 'ctd_name', 'ctd_id', 'ctd_count' ], '' );
607		$this->assertEquals( $expected, iterator_to_array( $res, false ) );
608
609		$expected2 = [
610			(object)[
611				'ct_tag_id' => 1,
612				'ct_rc_id' => 123
613			]
614		];
615		$res2 = $dbr->select( 'change_tag', [ 'ct_tag_id', 'ct_rc_id' ], '' );
616		$this->assertEquals( $expected2, iterator_to_array( $res2, false ) );
617	}
618
619	public function provideTags() {
620		$tags = [ 'tag 1', 'tag 2', 'tag 3' ];
621		$rcId = 123;
622		$revId = 456;
623		$logId = 789;
624
625		yield [ $tags, $rcId, null, null ];
626		yield [ $tags, null, $revId, null ];
627		yield [ $tags, null, null, $logId ];
628		yield [ $tags, $rcId, $revId, null ];
629		yield [ $tags, $rcId, null, $logId ];
630		yield [ $tags, $rcId, $revId, $logId ];
631	}
632
633	/**
634	 * @dataProvider provideTags
635	 */
636	public function testGetTags( array $tags, $rcId, $revId, $logId ) {
637		ChangeTags::addTags( $tags, $rcId, $revId, $logId );
638
639		$actualTags = ChangeTags::getTags( $this->db, $rcId, $revId, $logId );
640
641		$this->assertSame( $tags, $actualTags );
642	}
643
644	public function testGetTags_multiple_arguments() {
645		$rcId = 123;
646		$revId = 456;
647		$logId = 789;
648
649		ChangeTags::addTags( [ 'tag 1' ], $rcId );
650		ChangeTags::addTags( [ 'tag 2' ], $rcId, $revId );
651		ChangeTags::addTags( [ 'tag 3' ], $rcId, $revId, $logId );
652
653		$tags3 = [ 'tag 3' ];
654		$tags2 = array_merge( $tags3, [ 'tag 2' ] );
655		$tags1 = array_merge( $tags2, [ 'tag 1' ] );
656		$this->assertArrayEquals( $tags3, ChangeTags::getTags( $this->db, $rcId, $revId, $logId ) );
657		$this->assertArrayEquals( $tags2, ChangeTags::getTags( $this->db, $rcId, $revId ) );
658		$this->assertArrayEquals( $tags1, ChangeTags::getTags( $this->db, $rcId ) );
659	}
660
661	public function testGetTagsWithData() {
662		$rcId1 = 123;
663		$rcId2 = 456;
664		$rcId3 = 789;
665		ChangeTags::addTags( [ 'tag 1' ], $rcId1, null, null, 'data1' );
666		ChangeTags::addTags( [ 'tag 3_1' ], $rcId3, null, null );
667		ChangeTags::addTags( [ 'tag 3_2' ], $rcId3, null, null, 'data3_2' );
668
669		$data = ChangeTags::getTagsWithData( $this->db, $rcId1 );
670		$this->assertSame( [ 'tag 1' => 'data1' ], $data );
671
672		$data = ChangeTags::getTagsWithData( $this->db, $rcId2 );
673		$this->assertSame( [], $data );
674
675		$data = ChangeTags::getTagsWithData( $this->db, $rcId3 );
676		$this->assertArrayEquals( [ 'tag 3_1' => null, 'tag 3_2' => 'data3_2' ], $data, false, true );
677	}
678
679	public function testTagUsageStatistics() {
680		$this->emptyChangeTagsTables();
681		MediaWikiServices::getInstance()->resetServiceForTesting( 'NameTableStoreFactory' );
682
683		$rcId = 123;
684		ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
685
686		$rcId = 124;
687		ChangeTags::updateTags( [ 'tag1' ], [], $rcId );
688
689		$this->assertEquals( [ 'tag1' => 2, 'tag2' => 1 ], ChangeTags::tagUsageStatistics() );
690	}
691
692	public function testListExplicitlyDefinedTags() {
693		$this->emptyChangeTagsTables();
694
695		$rcId = 123;
696		ChangeTags::updateTags( [ 'tag1', 'tag2' ], [], $rcId );
697		ChangeTags::defineTag( 'tag2' );
698
699		$this->assertEquals( [ 'tag2' ], ChangeTags::listExplicitlyDefinedTags() );
700		$dbr = wfGetDB( DB_REPLICA );
701
702		$expected = [
703			(object)[
704				'ctd_name' => 'tag1',
705				'ctd_user_defined' => 0
706			],
707			(object)[
708				'ctd_name' => 'tag2',
709				'ctd_user_defined' => 1
710			],
711		];
712		$res = $dbr->select(
713			'change_tag_def',
714			[ 'ctd_name', 'ctd_user_defined' ],
715			'',
716			__METHOD__,
717			[ 'ORDER BY' => 'ctd_name' ]
718		);
719		$this->assertEquals( $expected, iterator_to_array( $res, false ) );
720	}
721}
722