Completed
Push — master ( 951284...b5c57e )
by
unknown
06:36 queued 11s
created

integration/includes/Changes/ChangeHandlerTest.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Wikibase\Client\Tests\Integration\Changes;
4
5
use ArrayIterator;
6
use InvalidArgumentException;
7
use MediaWiki\MediaWikiServices;
8
use MediaWikiIntegrationTestCase;
9
use Psr\Log\NullLogger;
10
use SiteLookup;
11
use Title;
12
use TitleFactory;
13
use Wikibase\Client\Changes\AffectedPagesFinder;
14
use Wikibase\Client\Changes\ChangeHandler;
15
use Wikibase\Client\Changes\ChangeRunCoalescer;
16
use Wikibase\Client\Changes\PageUpdater;
17
use Wikibase\Client\Usage\EntityUsage;
18
use Wikibase\Client\Usage\PageEntityUsages;
19
use Wikibase\Client\Usage\UsageLookup;
20
use Wikibase\DataModel\Entity\Item;
21
use Wikibase\DataModel\Entity\ItemId;
22
use Wikibase\Lib\Changes\Change;
23
use Wikibase\Lib\Changes\EntityChange;
24
use Wikibase\Lib\Store\SiteLinkLookup;
25
use Wikibase\Lib\Tests\Changes\TestChanges;
26
use Wikibase\Lib\Tests\MockRepository;
27
28
/**
29
 * @covers \Wikibase\Client\Changes\ChangeHandler
30
 *
31
 * @group Wikibase
32
 * @group WikibaseClient
33
 * @group WikibaseChange
34
 *
35
 * @group Database
36
 *
37
 * @license GPL-2.0-or-later
38
 * @author Daniel Kinzler
39
 * @author Jeroen De Dauw < [email protected] >
40
 */
41
class ChangeHandlerTest extends MediaWikiIntegrationTestCase {
42
43
	private function getAffectedPagesFinder( UsageLookup $usageLookup, TitleFactory $titleFactory ) {
44
		// @todo: mock the finder directly
45
		return new AffectedPagesFinder(
46
			$usageLookup,
47
			$titleFactory,
48
			MediaWikiServices::getInstance()->getLinkBatchFactory(),
49
			'enwiki',
50
			null,
51
			false
52
		);
53
	}
54
55
	/**
56
	 * @return ChangeRunCoalescer
57
	 */
58
	private function getChangeRunCoalescer() {
59
		$transformer = $this->getMockBuilder( ChangeRunCoalescer::class )
60
			->disableOriginalConstructor()
61
			->getMock();
62
63
		$transformer->expects( $this->any() )
64
			->method( 'transformChangeList' )
65
			->will( $this->returnArgument( 0 ) );
66
67
		return $transformer;
68
	}
69
70
	private function getChangeHandler(
71
		array $pageNamesPerItemId = [],
72
		PageUpdater $updater = null
73
	) {
74
		$siteLinkLookup = $this->getSiteLinkLookup( $pageNamesPerItemId );
75
		$usageLookup = $this->getUsageLookup( $siteLinkLookup );
76
		$titleFactory = $this->getTitleFactory( $pageNamesPerItemId );
77
		$affectedPagesFinder = $this->getAffectedPagesFinder( $usageLookup, $titleFactory );
78
79
		$handler = new ChangeHandler(
80
			$affectedPagesFinder,
81
			$titleFactory,
82
			$updater ?: new MockPageUpdater(),
83
			$this->getChangeRunCoalescer(),
84
			$this->createMock( SiteLookup::class ),
85
			new NullLogger(),
86
			true
87
		);
88
89
		return $handler;
90
	}
91
92
	/**
93
	 * @param array $pageNamesPerItemId
94
	 *
95
	 * @return SiteLinkLookup
96
	 */
97
	private function getSiteLinkLookup( array $pageNamesPerItemId ) {
98
		$repo = new MockRepository();
99
100
		// entity 1, revision 11
101
		$entity1 = new Item( new ItemId( 'Q1' ) );
102
		$entity1->setLabel( 'en', 'one' );
103
		$repo->putEntity( $entity1, 11 );
104
105
		// entity 1, revision 12
106
		$entity1->setLabel( 'de', 'eins' );
107
		$repo->putEntity( $entity1, 12 );
108
109
		// entity 1, revision 13
110
		$entity1->setLabel( 'it', 'uno' );
111
		$repo->putEntity( $entity1, 13 );
112
113
		// entity 1, revision 1111
114
		$entity1->setDescription( 'en', 'the first' );
115
		$repo->putEntity( $entity1, 1111 );
116
117
		// entity 2, revision 21
118
		$entity1 = new Item( new ItemId( 'Q2' ) );
119
		$entity1->setLabel( 'en', 'two' );
120
		$repo->putEntity( $entity1, 21 );
121
122
		// entity 2, revision 22
123
		$entity1->setLabel( 'de', 'zwei' );
124
		$repo->putEntity( $entity1, 22 );
125
126
		// entity 2, revision 23
127
		$entity1->setLabel( 'it', 'due' );
128
		$repo->putEntity( $entity1, 23 );
129
130
		// entity 2, revision 1211
131
		$entity1->setDescription( 'en', 'the second' );
132
		$repo->putEntity( $entity1, 1211 );
133
134
		$this->updateMockRepository( $repo, $pageNamesPerItemId );
135
136
		return $repo;
137
	}
138
139
	public function provideHandleChanges() {
140
		$empty = new Item( new ItemId( 'Q55668877' ) );
141
142
		$changeFactory = TestChanges::getEntityChangeFactory();
143
		$itemCreation = $changeFactory->newFromUpdate( EntityChange::ADD, null, $empty );
144
		$itemDeletion = $changeFactory->newFromUpdate( EntityChange::REMOVE, $empty, null );
145
146
		$itemCreation->setField( 'time', '20130101010101' );
147
		$itemDeletion->setField( 'time', '20130102020202' );
148
149
		return [
150
			[],
151
			[ $itemCreation ],
152
			[ $itemDeletion ],
153
			[ $itemCreation, $itemDeletion ],
154
		];
155
	}
156
157
	/**
158
	 * @dataProvider provideHandleChanges
159
	 */
160
	public function testHandleChanges( ...$changes ) {
161
		$spy = (object)[
162
			'handleChangeCallCount' => 0,
163
			'handleChangesCallCount' => 0,
164
		];
165
166
		$testHooks = [
167
			'WikibaseHandleChange' => [ function( Change $change ) use ( $spy ) {
168
				$spy->handleChangeCallCount++;
169
				return true;
170
			} ],
171
			'WikibaseHandleChanges' => [ function( array $changes ) use ( $spy ) {
172
				$spy->handleChangesCallCount++;
173
				return true;
174
			} ]
175
		];
176
177
		$this->mergeMwGlobalArrayValue( 'wgHooks', $testHooks );
178
179
		$changeHandler = $this->getChangeHandler();
180
		$changeHandler->handleChanges( $changes );
181
182
		$this->assertSame( count( $changes ), $spy->handleChangeCallCount );
183
		$this->assertSame( 1, $spy->handleChangesCallCount );
184
	}
185
186
	/**
187
	 * Returns a map of fake local page IDs to the corresponding local page names.
188
	 * The fake page IDs are the IDs of the items that have a sitelink to the
189
	 * respective page on the local wiki:
190
	 *
191
	 * Example: If Q100 has a link enwiki => 'Emmy',
192
	 * then 100 => 'Emmy' will be in the map returned by this method.
193
	 *
194
	 * @param array[] $pageNamesPerItemId Assoc array mapping entity IDs to lists of sitelinks.
195
	 *
196
	 * @return string[]
197
	 */
198
	private function getFakePageIdMap( array $pageNamesPerItemId ) {
199
		$titlesByPageId = [];
200
		$siteId = 'enwiki';
201
202
		foreach ( $pageNamesPerItemId as $idString => $pageNames ) {
203
			$itemId = new ItemId( $idString );
204
205
			// If $links[0] is set, it's considered a link to the local wiki.
206
			// The index 0 is effectively an alias for $siteId;
207
			if ( isset( $pageNames[0] ) ) {
208
				$pageNames[$siteId] = $pageNames[0];
209
			}
210
211
			if ( isset( $pageNames[$siteId] ) ) {
212
				$pageId = $itemId->getNumericId();
213
				$titlesByPageId[$pageId] = $pageNames[$siteId];
214
			}
215
		}
216
217
		return $titlesByPageId;
218
	}
219
220
	/**
221
	 * Title factory, using spoofed local page ids that correspond to the ids of items linked to
222
	 * the respective page (see getUsageLookup).
223
	 *
224
	 * @param array[] $pageNamesPerItemId Assoc array mapping entity IDs to lists of sitelinks.
225
	 *
226
	 * @return TitleFactory
227
	 */
228
	private function getTitleFactory( array $pageNamesPerItemId ) {
229
		$titlesById = $this->getFakePageIdMap( $pageNamesPerItemId );
230
		$pageIdsByTitle = array_flip( $titlesById );
231
232
		$titleFactory = $this->createMock( TitleFactory::class );
233
234
		$titleFactory->method( 'newFromIDs' )
235
			->willReturnCallback( function ( array $ids ) use ( $titlesById ) {
236
				$titles = [];
237
				foreach ( $ids as $id ) {
238
					if ( isset( $titlesById[$id] ) ) {
239
						$title = Title::newFromText( $titlesById[$id] );
240
						$title->resetArticleID( $id );
241
						$titles[] = $title;
242
					} else {
243
						throw new InvalidArgumentException( 'Unknown ID: ' . $id );
244
					}
245
				}
246
				return $titles;
247
			} );
248
249
		$titleFactory->expects( $this->any() )
250
			->method( 'newFromText' )
251
			->will( $this->returnCallback( function( $text, $defaultNs = \NS_MAIN ) use ( $pageIdsByTitle ) {
252
				$title = Title::newFromText( $text, $defaultNs );
253
254
				if ( !$title ) {
255
					return $title;
256
				}
257
258
				if ( isset( $pageIdsByTitle[$text] ) ) {
259
					$title->resetArticleID( $pageIdsByTitle[$text] );
260
				} else {
261
					throw new InvalidArgumentException( 'Unknown title text: ' . $text );
262
				}
263
264
				return $title;
265
			} ) );
266
267
		return $titleFactory;
268
	}
269
270
	/**
271
	 * Returns a usage lookup based on $siteLinklookup.
272
	 * Local page IDs are spoofed using the numeric item ID as the local page ID.
273
	 *
274
	 * @param SiteLinkLookup $siteLinkLookup
275
	 *
276
	 * @return UsageLookup
277
	 */
278
	private function getUsageLookup( SiteLinkLookup $siteLinkLookup ) {
279
		$usageLookup = $this->createMock( UsageLookup::class );
280
		$usageLookup->expects( $this->any() )
281
			->method( 'getPagesUsing' )
282
			->will( $this->returnCallback(
283
				function( $ids, $aspects ) use ( $siteLinkLookup ) {
284
					$pages = [];
285
286
					foreach ( $ids as $id ) {
287
						if ( !( $id instanceof ItemId ) ) {
288
							continue;
289
						}
290
291
						$links = $siteLinkLookup->getSiteLinksForItem( $id );
292
						foreach ( $links as $link ) {
293
							if ( $link->getSiteId() === 'enwiki' ) {
294
								// we use the numeric item id as the fake page id of the local page!
295
								$usedAspects = array_intersect(
296
									[ EntityUsage::SITELINK_USAGE, EntityUsage::LABEL_USAGE . '.en' ],
297
									$aspects
298
								);
299
								if ( !$usedAspects ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $usedAspects of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
300
									continue;
301
								}
302
								$usages = [];
303
								foreach ( $usedAspects as $aspect ) {
304
									$usages[] = new EntityUsage(
305
										$id,
306
										EntityUsage::splitAspectKey( $aspect )[0],
307
										EntityUsage::splitAspectKey( $aspect )[1]
308
									);
309
								}
310
								$pages[] = new PageEntityUsages(
311
									$id->getNumericId(),
312
									$usages
313
								);
314
							}
315
						}
316
					}
317
318
					return new ArrayIterator( $pages );
319
				} ) );
320
321
		return $usageLookup;
322
	}
323
324
	/**
325
	 * @param MockRepository $mockRepository
326
	 * @param array $pageNamesPerItemId Associative array of item id string => either Item object
327
	 * or array of site id => page name.
328
	 */
329
	private function updateMockRepository( MockRepository $mockRepository, array $pageNamesPerItemId ) {
330
		foreach ( $pageNamesPerItemId as $idString => $pageNames ) {
331
			if ( is_array( $pageNames ) ) {
332
				$item = new Item( new ItemId( $idString ) );
333
334
				foreach ( $pageNames as $siteId => $pageName ) {
335
					if ( !is_string( $siteId ) ) {
336
						$siteId = 'enwiki';
337
					}
338
339
					$item->getSiteLinkList()->addNewSiteLink( $siteId, $pageName );
340
				}
341
			} else {
342
				$item = $pageNames;
343
			}
344
345
			$mockRepository->putEntity( $item );
346
		}
347
	}
348
349
	public function provideHandleChange() {
350
		$changes = TestChanges::getChanges();
351
		$userEmmy2 = Title::newFromText( 'User:Emmy2' )->getPrefixedText();
352
353
		$empty = [
354
			'scheduleRefreshLinks' => [],
355
			'purgeWebCache' => [],
356
			'injectRCRecord' => [],
357
		];
358
359
		$emmy2PurgeParser = [
360
			'scheduleRefreshLinks' => [ 'Emmy2' => true ],
361
			'purgeWebCache' => [ 'Emmy2' => true ],
362
			'injectRCRecord' => [ 'Emmy2' => true ],
363
		];
364
365
		$userEmmy2PurgeParser = [
366
			'scheduleRefreshLinks' => [ $userEmmy2 => true ],
367
			'purgeWebCache' => [ $userEmmy2 => true ],
368
			'injectRCRecord' => [ $userEmmy2 => true ],
369
		];
370
371
		$emmyUpdateLinks = [
372
			'scheduleRefreshLinks' => [ 'Emmy' => true ],
373
			'purgeWebCache' => [ 'Emmy' => true ],
374
			'injectRCRecord' => [ 'Emmy' => true ],
375
		];
376
377
		$emmy2UpdateLinks = [
378
			'scheduleRefreshLinks' => [ 'Emmy2' => true ],
379
			'purgeWebCache' => [ 'Emmy2' => true ],
380
			'injectRCRecord' => [ 'Emmy2' => true ],
381
		];
382
383
		$emmy2UpdateAll = [
384
			'scheduleRefreshLinks' => [ 'Emmy2' => true ],
385
			'purgeWebCache' => [ 'Emmy2' => true ],
386
			'injectRCRecord' => [ 'Emmy2' => true ],
387
		];
388
389
		return [
390
			[ // #0
391
				$changes['property-creation'],
392
				[ 'Q100' => [] ],
393
				$empty
394
			],
395
			[ // #1
396
				$changes['property-deletion'],
397
				[ 'Q100' => [] ],
398
				$empty
399
			],
400
			[ // #2
401
				$changes['property-set-label'],
402
				[ 'Q100' => [] ],
403
				$empty
404
			],
405
406
			[ // #3
407
				$changes['item-creation'],
408
				[ 'Q100' => [] ],
409
				$empty
410
			],
411
			[ // #4
412
				$changes['item-deletion'],
413
				[ 'Q100' => [] ],
414
				$empty
415
			],
416
			[ // #5
417
				$changes['item-deletion-linked'],
418
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
419
				$emmy2UpdateAll
420
			],
421
422
			[ // #6
423
				$changes['set-de-label'],
424
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
425
				$empty, // For the dummy page, only label and sitelink usage is defined.
426
			],
427
			[ // #7
428
				$changes['set-en-label'],
429
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
430
				$emmy2PurgeParser
431
			],
432
			[ // #8
433
				$changes['set-en-label'],
434
				[ 'Q100' => [ 'enwiki' => 'User:Emmy2' ] ], // user namespace
435
				$userEmmy2PurgeParser
436
			],
437
			[ // #9
438
				$changes['set-en-aliases'],
439
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
440
				$empty, // For the dummy page, only label and sitelink usage is defined.
441
			],
442
443
			[ // #10
444
				$changes['add-claim'],
445
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
446
				$empty // statements are ignored
447
			],
448
			[ // #11
449
				$changes['remove-claim'],
450
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
451
				$empty // statements are ignored
452
			],
453
454
			[ // #12
455
				$changes['set-dewiki-sitelink'],
456
				[ 'Q100' => [] ],
457
				$empty // not yet linked
458
			],
459
			[ // #13
460
				$changes['set-enwiki-sitelink'],
461
				[ 'Q100' => [ 'enwiki' => 'Emmy' ] ],
462
				$emmyUpdateLinks
463
			],
464
465
			[ // #14
466
				$changes['change-dewiki-sitelink'],
467
				[ 'Q100' => [ 'enwiki' => 'Emmy' ] ],
468
				$emmyUpdateLinks
469
			],
470
			[ // #15
471
				$changes['change-enwiki-sitelink'],
472
				[ 'Q100' => [ 'enwiki' => 'Emmy' ], 'Q200' => [ 'enwiki' => 'Emmy2' ] ],
473
				[
474
					'scheduleRefreshLinks' => [ 'Emmy' => true, 'Emmy2' => true ],
475
					'purgeWebCache' => [ 'Emmy' => true, 'Emmy2' => true ],
476
					'injectRCRecord' => [ 'Emmy' => true, 'Emmy2' => true ],
477
				]
478
			],
479
			[ // #16
480
				$changes['change-enwiki-sitelink-badges'],
481
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
482
				$emmy2UpdateLinks
483
			],
484
485
			[ // #17
486
				$changes['remove-dewiki-sitelink'],
487
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
488
				$emmy2UpdateLinks
489
			],
490
			[ // #18
491
				$changes['remove-enwiki-sitelink'],
492
				[ 'Q100' => [ 'enwiki' => 'Emmy2' ] ],
493
				$emmy2UpdateLinks
494
			],
495
		];
496
	}
497
498
	/**
499
	 * @dataProvider provideHandleChange
500
	 */
501
	public function testHandleChange( EntityChange $change, array $pageNamesPerItemId, array $expected ) {
502
		$updater = new MockPageUpdater();
503
		$handler = $this->getChangeHandler( $pageNamesPerItemId, $updater );
504
505
		$handler->handleChange( $change );
506
		$updates = $updater->getUpdates();
507
508
		$this->assertSameSize( $expected, $updates );
509
510
		foreach ( $expected as $k => $exp ) {
511
			$up = $updates[$k];
512
			$this->assertSame( array_keys( $exp ), array_keys( $up ), $k );
513
		}
514
	}
515
516
	/**
517
	 * @param int|null $id
518
	 * @param string $type
519
	 * @param string $objectId
520
	 * @param array $info
521
	 *
522
	 * @return EntityChange
523
	 */
524
	private function newChange( $id, $type, $objectId, $info = [] ) {
525
		$fields = [
526
			'id' => $id,
527
			'time' => '20121212121212',
528
			'type' => $type,
529
			'objectid' => $objectId,
530
			'info' => $info,
531
		];
532
533
		return new EntityChange( $fields );
534
	}
535
536
	public function provideHandleChange_rootJobParams() {
537
		$ids = [ 18, 19, 17 ]; // note: provide these out of order, to check canonical sorting!
538
		$regularChange = $this->newChange( 17, 'x~y', 'Q100', [] );
539
		$coalescedChange = $this->newChange( 0, 'x~y', 'Q100', [ 'change-ids' => $ids ] );
540
		$strangeChange = $this->newChange( 0, 'x~y', 'Q100', [ 'kittens' => 13 ] );
541
542
		$q100 = new ItemId( 'Q100' );
543
		$usages = [ // note: provide these out of order, to check canonical sorting!
544
			102 => new PageEntityUsages( 102, [ new EntityUsage( $q100, 'X' ) ] ),
545
			101 => new PageEntityUsages( 101, [ new EntityUsage( $q100, 'X' ) ] ),
546
		];
547
548
		$titleBatchHash = 'f0b873699a63c858667e54cd071f7d9209faeda1';
549
		$strangeHash = 'cadbb4899603593164f06a4754f597fdcb2c07b4';
550
551
		$regularRootJobParams = [
552
			'purgeWebCache' => [ 'rootJobSignature' => "title-batch:$titleBatchHash" ],
553
			'scheduleRefreshLinks' => [ 'rootJobSignature' => "title-batch:$titleBatchHash" ],
554
			'injectRCRecord' => [ 'rootJobSignature' => "title-batch:$titleBatchHash&change-id:17" ],
555
		];
556
557
		$coalescedRootJobParams = [
558
			'purgeWebCache' => [ 'rootJobSignature' => "title-batch:$titleBatchHash" ],
559
			'scheduleRefreshLinks' => [ 'rootJobSignature' => "title-batch:$titleBatchHash" ],
560
			'injectRCRecord' => [ 'rootJobSignature' => "title-batch:$titleBatchHash&change-batch:17,18,19" ],
561
		];
562
563
		$strangeRootJobParams = [
564
			'purgeWebCache' => [ 'rootJobSignature' => "title-batch:$titleBatchHash" ],
565
			'scheduleRefreshLinks' => [ 'rootJobSignature' => "title-batch:$titleBatchHash" ],
566
			'injectRCRecord' => [ 'rootJobSignature' => "title-batch:$titleBatchHash&change-hash:$strangeHash" ],
567
		];
568
569
		return [
570
			[ $regularChange, $usages, $regularRootJobParams ],
571
			[ $coalescedChange, $usages, $coalescedRootJobParams ],
572
			[ $strangeChange, $usages, $strangeRootJobParams ],
573
		];
574
	}
575
576
	/**
577
	 * @dataProvider provideHandleChange_rootJobParams
578
	 */
579
	public function testHandleChange_rootJobParams(
580
		EntityChange $change,
581
		array $usages,
582
		array $expectedRootJobParams
583
	) {
584
		$updater = new MockPageUpdater();
585
586
		$affectedPagesFinder = $this->getMockBuilder( AffectedPagesFinder::class )
587
			->disableOriginalConstructor()
588
			->getMock();
589
		$affectedPagesFinder->expects( $this->any() )
590
			->method( 'getAffectedUsagesByPage' )
591
			->will( $this->returnValue( $usages ) );
592
593
		$titleFactory = $this->getMockBuilder( TitleFactory::class )
594
			->disableOriginalConstructor()
595
			->getMock();
596
		$titleFactory->method( 'newFromIDs' )
597
			->willReturnCallback( function ( array $ids ) {
598
				// NOTE: the fake title construction influences the expected hash values
599
				// defined in provideHandleChange_rootJobParams!
600
				$titles = [];
601
				foreach ( $ids as $id ) {
602
					$title = Title::makeTitle( NS_MAIN, 'Page_No_' . $id );
603
					$title->resetArticleID( $id );
604
					$titles[] = $title;
605
				}
606
				return $titles;
607
			} );
608
609
		$handler = new ChangeHandler(
610
			$affectedPagesFinder,
611
			$titleFactory,
612
			$updater,
613
			$this->getChangeRunCoalescer(),
614
			$this->createMock( SiteLookup::class ),
615
			new NullLogger()
616
		);
617
618
		$inputRootJobParams = [ 'rootJobTimestamp' => '20171122040506' ];
619
620
		$handler->handleChange( $change, $inputRootJobParams );
621
		$actualRootJobParams = $updater->getRootJobParams();
622
623
		$this->assertSameSize( $expectedRootJobParams, $actualRootJobParams );
624
625
		foreach ( $expectedRootJobParams as $k => $exp ) {
626
			$act = $actualRootJobParams[$k];
627
			if ( $k !== 'scheduleRefreshLinks' ) {
628
				$this->assertSame( '20171122040506', $act['rootJobTimestamp'], "$k/rootJobTimestamp" );
629
			}
630
			$this->assertSame( $exp['rootJobSignature'], $act['rootJobSignature'], "$k/rootJobSignature" );
631
		}
632
	}
633
634
}
635