WikibaseApiTestCase::getLastRevIdFromResult()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 3
nc 3
nop 1
1
<?php
2
3
namespace Wikibase\Repo\Tests\Api;
4
5
use ApiTestCase;
6
use ApiUsageException;
7
use ChangeTags;
8
use MediaWiki\MediaWikiServices;
9
use OutOfBoundsException;
10
use PHPUnit\Framework\Constraint\Constraint;
11
use TestSites;
12
use TestUser;
13
use Title;
14
use User;
15
use Wikibase\Repo\WikibaseRepo;
16
use Wikimedia\TestingAccessWrapper;
17
use WikiPage;
18
19
/**
20
 * @license GPL-2.0-or-later
21
 * @author John Erling Blad < [email protected] >
22
 * @author Daniel Kinzler
23
 * @author Addshore
24
 */
25
abstract class WikibaseApiTestCase extends ApiTestCase {
26
27
	/** @var User */
28
	protected $user;
29
30
	protected function setUp(): void {
31
32
		parent::setUp();
33
34
		$this->setupUser();
35
36
		$this->setupSiteLinkGroups();
37
38
		$siteStore = new \HashSiteStore( TestSites::getSites() );
39
		$this->setService( 'SiteStore', $siteStore );
40
		$this->setService( 'SiteLookup', $siteStore );
41
	}
42
43
	protected function createTestUser() {
44
		return new TestUser(
45
			'Apitesteditor',
46
			'Api Test Editor',
47
			'[email protected]',
48
			[ 'wbeditor' ]
49
		);
50
	}
51
52
	private function setupUser() {
53
		self::$users['wbeditor'] = $this->createTestUser();
54
55
		$this->user = self::$users['wbeditor']->getUser();
56
		$this->setMwGlobals( 'wgGroupPermissions', [ '*' => [
57
			'property-create' => true,
58
			'createpage' => true,
59
			'bot' => true,
60
			'item-term' => true,
61
			'item-merge' => true,
62
			'item-redirect' => true,
63
			'property-term' => true,
64
			'read' => true,
65
			'edit' => true,
66
			'writeapi' => true
67
		] ] );
68
	}
69
70
	private function setupSiteLinkGroups() {
71
		global $wgWBRepoSettings;
72
73
		$customRepoSettings = $wgWBRepoSettings;
74
		$customRepoSettings['siteLinkGroups'] = [ 'wikipedia' ];
75
		$this->setMwGlobals( 'wgWBRepoSettings', $customRepoSettings );
76
		MediaWikiServices::getInstance()->resetServiceForTesting( 'SiteLookup' );
77
	}
78
79
	/**
80
	 * Appends an edit token to a request.
81
	 *
82
	 * @param array $params
83
	 * @param array|null $session
84
	 * @param User|null $user
85
	 * @param string $tokenType
86
	 *
87
	 * @throws ApiUsageException
88
	 * @return array( array|null $resultData, WebRequest $request, array $sessionArray )
0 ignored issues
show
Documentation introduced by
The doc-type array( could not be parsed: Expected "|" or "end of type", but got "(" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
89
	 */
90
	protected function doApiRequestWithToken(
91
		array $params,
92
		array $session = null,
93
		User $user = null,
94
		$tokenType = 'csrf'
95
	) {
96
		if ( !$user ) {
97
			$user = \RequestContext::getMain()->getUser();
98
		}
99
100
		if ( !array_key_exists( 'token', $params ) ) {
101
			$params['token'] = $user->getEditToken();
102
		}
103
104
		return $this->doApiRequest( $params, $session, false, $user, $tokenType );
105
	}
106
107
	/**
108
	 * @param string[] $handles
109
	 * @param string[] $idMap
110
	 */
111
	protected function initTestEntities( array $handles, array $idMap = [] ) {
112
		$activeHandles = EntityTestHelper::getActiveHandles();
113
		$user = $this->getTestSysop()->getUser();
114
115
		foreach ( $activeHandles as $handle => $id ) {
116
			$title = $this->getTestEntityTitle( $handle );
117
118
			$page = WikiPage::factory( $title );
119
			$page->doDeleteArticleReal( 'Test reset', $user );
120
			EntityTestHelper::unRegisterEntity( $handle );
121
		}
122
123
		foreach ( $handles as $handle ) {
124
			$params = EntityTestHelper::getEntity( $handle );
125
			$params['action'] = 'wbeditentity';
126
127
			EntityTestHelper::injectIds( $params, $idMap );
128
			EntityTestHelper::injectIds( $params, EntityTestHelper::$defaultPlaceholderValues );
129
130
			list( $res, , ) = $this->doApiRequestWithToken( $params );
131
			EntityTestHelper::registerEntity( $handle, $res['entity']['id'], $res['entity'] );
132
133
			$idMap["%$handle%"] = $res['entity']['id'];
134
		}
135
	}
136
137
	/**
138
	 * @param string $handle
139
	 *
140
	 * @return null|Title
141
	 */
142
	protected function getTestEntityTitle( $handle ) {
143
		try {
144
			$wikibaseRepo = WikibaseRepo::getDefaultInstance();
145
			$idString = EntityTestHelper::getId( $handle );
146
			$id = $wikibaseRepo->getEntityIdParser()->parse( $idString );
147
			$title = $wikibaseRepo->getEntityTitleLookup()->getTitleForId( $id );
148
		} catch ( OutOfBoundsException $ex ) {
149
			$title = null;
150
		}
151
152
		return $title;
153
	}
154
155
	/**
156
	 * Loads an entity from the database (via an API call).
157
	 *
158
	 * @param string $id
159
	 *
160
	 * @return array
161
	 */
162
	protected function loadEntity( $id ) {
163
		list( $res, , ) = $this->doApiRequest(
164
			[
165
				'action' => 'wbgetentities',
166
				'format' => 'json', // make sure IDs are used as keys.
167
				'ids' => $id ]
168
		);
169
170
		return $res['entities'][$id];
171
	}
172
173
	/**
174
	 * Do the test for exceptions from Api queries.
175
	 *
176
	 * @param array $params Array of params for the API query.
177
	 * @param array $exception Details of the exception to expect (type, code, message, message-key).
178
	 * @param User|null $user
179
	 * @param bool $token Whether to include a CSRF token
180
	 */
181
	protected function doTestQueryExceptions(
182
		array $params,
183
		array $exception,
184
		User $user = null,
185
		$token = true
186
	) {
187
		try {
188
			if ( $token ) {
189
				$this->doApiRequestWithToken( $params, null, $user );
190
			} else {
191
				$this->doApiRequest( $params, null, false, $user );
192
			}
193
194
			$this->fail( 'Failed to throw ApiUsageException' );
195
		} catch ( ApiUsageException $e ) {
0 ignored issues
show
Bug introduced by
The class ApiUsageException does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
196
			if ( array_key_exists( 'type', $exception ) ) {
197
				$this->assertInstanceOf( $exception['type'], $e );
198
			}
199
200
			if ( array_key_exists( 'code', $exception ) ) {
201
				$msg = TestingAccessWrapper::newFromObject( $e )->getApiMessage();
202
				$this->assertThat(
203
					$msg->getApiCode(),
204
					$exception['code'] instanceof Constraint
0 ignored issues
show
Bug introduced by
The class PHPUnit\Framework\Constraint\Constraint does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
205
						? $exception['code']
206
						: $this->equalTo( $exception['code'] )
207
				);
208
			}
209
210
			if ( array_key_exists( 'message', $exception ) ) {
211
				$this->assertStringContainsString( $exception['message'], $e->getMessage() );
212
			}
213
214
			if ( array_key_exists( 'message-key', $exception ) ) {
215
				$status = $e->getStatusValue();
216
				$this->assertTrue(
217
					$status->hasMessage( $exception['message-key'] ),
218
					'Status message key'
219
				);
220
			}
221
		}
222
	}
223
224
	/**
225
	 * Utility function for converting an array from "deep" (indexed) to "flat" (keyed) structure.
226
	 * Arrays that already use a flat structure are left unchanged.
227
	 *
228
	 * Arrays with a deep structure are expected to be list of entries that are associative arrays,
229
	 * where which entry has at least the fields given by $keyField and $valueField.
230
	 *
231
	 * Arrays with a flat structure are associative and assign values to meaningful keys.
232
	 *
233
	 * @param array $data the input array.
234
	 * @param string $keyField The name of the field in each entry that shall be used as the key in
235
	 *  the flat structure.
236
	 * @param string $valueField The name of the field in each entry that shall be used as the value
237
	 *  in the flat structure.
238
	 * @param bool $multiValue Whether the value in the flat structure shall be an indexed array of
239
	 *  values instead of a single value.
240
	 * @param array &$into optional aggregator.
241
	 *
242
	 * @return array The flat version of $data.
243
	 */
244
	protected function flattenArray( array $data, $keyField, $valueField, $multiValue = false, array &$into = [] ) {
245
		foreach ( $data as $index => $value ) {
246
			if ( is_array( $value ) ) {
247
				if ( isset( $value[$keyField] ) && isset( $value[$valueField] ) ) {
248
					// found "deep" entry in the array
249
					$k = $value[ $keyField ];
250
					$v = $value[ $valueField ];
251
				} elseif ( isset( $value[0] ) && !is_array( $value[0] ) && $multiValue ) {
252
					// found "flat" multi-value entry in the array
253
					$k = $index;
254
					$v = $value;
255
				} else {
256
					// found list, recurse
257
					$this->flattenArray( $value, $keyField, $valueField, $multiValue, $into );
258
					continue;
259
				}
260
			} else {
261
				// found "flat" entry in the array
262
				$k = $index;
263
				$v = $value;
264
			}
265
266
			if ( $multiValue ) {
267
				if ( is_array( $v ) ) {
268
					$into[$k] = empty( $into[$k] ) ? $v : array_merge( $into[$k], $v );
269
				} else {
270
					$into[$k][] = $v;
271
				}
272
			} else {
273
				$into[$k] = $v;
274
			}
275
		}
276
277
		return $into;
278
	}
279
280
	/**
281
	 * Compares two entity structures and asserts that they are equal. Only fields present in $expected are considered.
282
	 * $expected and $actual can both be either in "flat" or in "deep" form, they are converted as needed before comparison.
283
	 *
284
	 * @param array $expected
285
	 * @param array $actual
286
	 * @param bool $expectEmptyArrays Should we expect empty arrays or just ignore them?
287
	 */
288
	protected function assertEntityEquals( array $expected, array $actual, $expectEmptyArrays = true ) {
289
		if ( isset( $expected['id'] ) && !empty( $expected['id'] ) ) {
290
			$this->assertEquals( $expected['id'], $actual['id'], 'id' );
291
		}
292
		if ( isset( $expected['lastrevid'] ) ) {
293
			$this->assertEquals( $expected['lastrevid'], $actual['lastrevid'], 'lastrevid' );
294
		}
295
		if ( isset( $expected['type'] ) ) {
296
			$this->assertEquals( $expected['type'], $actual['type'], 'type' );
297
		}
298
299
		if ( isset( $expected['labels'] ) ) {
300
			if ( !( $expectEmptyArrays === false && $expected['labels'] === [] ) ) {
301
				$data = $this->flattenArray( $actual['labels'], 'language', 'value' );
302
				$exp = $this->flattenArray( $expected['labels'], 'language', 'value' );
303
304
				// keys are significant in flat form
305
				$this->assertArrayEquals( $exp, $data, false, true );
306
			}
307
		}
308
309
		if ( isset( $expected['descriptions'] ) ) {
310
			if ( !( $expectEmptyArrays === false && $expected['descriptions'] === [] ) ) {
311
				$data = $this->flattenArray( $actual['descriptions'], 'language', 'value' );
312
				$exp = $this->flattenArray( $expected['descriptions'], 'language', 'value' );
313
314
				// keys are significant in flat form
315
				$this->assertArrayEquals( $exp, $data, false, true );
316
			}
317
		}
318
319
		if ( isset( $expected['sitelinks'] ) ) {
320
			if ( !( $expectEmptyArrays === false && $expected['sitelinks'] === [] ) ) {
321
				$data = $this->flattenArray( $actual['sitelinks'] ?? [], 'site', 'title' );
322
				$exp = $this->flattenArray( $expected['sitelinks'], 'site', 'title' );
323
324
				// keys are significant in flat form
325
				$this->assertArrayEquals( $exp, $data, false, true );
326
			}
327
		}
328
329
		if ( isset( $expected['aliases'] ) ) {
330
			if ( !( $expectEmptyArrays === false && $expected['aliases'] === [] ) ) {
331
				$data = $this->flattenArray( $actual['aliases'], 'language', 'value', true );
332
				$exp = $this->flattenArray( $expected['aliases'], 'language', 'value', true );
333
334
				// keys are significant in flat form
335
				$this->assertArrayEquals( $exp, $data, false, true );
336
			}
337
		}
338
339
		if ( isset( $expected['claims'] ) ) {
340
			if ( !( $expectEmptyArrays === false && $expected['claims'] === [] ) ) {
341
				$data = $this->flattenArray( $actual['claims'], 'mainsnak', 'value', true );
342
				$exp = $this->flattenArray( $expected['claims'], 'language', 'value', true );
343
				$count = count( $expected['claims'] );
344
345
				for ( $i = 0; $i < $count; $i++ ) {
346
					$this->assertArrayHasKey( $i, $data['id'] );
347
					$this->assertGreaterThanOrEqual( 39, strlen( $data['id'][$i] ) );
348
				}
349
				//unset stuff we dont actually want to compare
350
				if ( isset( $exp['id'] ) ) {
351
					$this->assertArrayHasKey( 'id', $data );
352
				}
353
				unset( $exp['id'] );
354
				unset( $exp['datatype'] );
355
				unset( $exp['hash'] );
356
				unset( $exp['qualifiers-order'] );
357
				unset( $data['datatype'] );
358
				unset( $data['id'] );
359
				unset( $data['hash'] );
360
				unset( $data['qualifiers-order'] );
361
				$this->assertArrayEquals( $exp, $data, false, true );
362
			}
363
		}
364
	}
365
366
	/**
367
	 * Asserts that the given API response represents a successful call.
368
	 *
369
	 * @param array $response
370
	 */
371
	protected function assertResultSuccess( array $response ) {
372
		$this->assertArrayHasKey( 'success', $response, "Missing 'success' marker in response." );
373
		$this->assertResultHasEntityType( $response );
374
	}
375
376
	/**
377
	 * Asserts that the given API response has a valid entity type if the result contains an entity
378
	 *
379
	 * @param array $response
380
	 */
381
	protected function assertResultHasEntityType( array $response ) {
382
		$wikibaseRepo = WikibaseRepo::getDefaultInstance();
383
384
		if ( isset( $response['entity'] ) ) {
385
			if ( isset( $response['entity']['type'] ) ) {
386
				$this->assertContains(
387
					$response['entity']['type'],
388
					$wikibaseRepo->getEnabledEntityTypes(),
389
					"Missing valid 'type' in response."
390
				);
391
			}
392
		} elseif ( isset( $response['entities'] ) ) {
393
			foreach ( $response['entities'] as $entity ) {
394
				if ( isset( $entity['type'] ) ) {
395
					$this->assertContains(
396
						$entity['type'],
397
						$wikibaseRepo->getEnabledEntityTypes(),
398
						"Missing valid 'type' in response."
399
					);
400
				}
401
			}
402
		}
403
	}
404
405
	/**
406
	 * Asserts that the revision with the given ID has a summary matching $regex
407
	 *
408
	 * @param string|string[] $regex The regex to match, or an array to build a regex from.
409
	 * @param int $revid
410
	 */
411
	protected function assertRevisionSummary( $regex, $revid ) {
412
		if ( is_array( $regex ) ) {
413
			$r = '';
414
415
			foreach ( $regex as $s ) {
416
				if ( $r !== '' ) {
417
					$r .= '.*';
418
				}
419
420
				$r .= preg_quote( $s, '!' );
421
			}
422
423
			$regex = "!$r!";
424
		}
425
426
		$revRecord = MediaWikiServices::getInstance()
427
			->getRevisionLookup()
428
			->getRevisionById( $revid );
429
		$this->assertNotNull( $revRecord, "revision not found: $revid" );
430
431
		$comment = $revRecord->getComment();
432
		$this->assertInstanceOf( 'CommentStoreComment', $comment );
433
		$this->assertRegExp( $regex, $comment->text );
434
	}
435
436
	protected function assertCanTagSuccessfulRequest(
437
		array $params,
438
		array $session = null,
439
		User $user = null,
440
		$tokenType = 'csrf'
441
	) {
442
		$dummyTag = __METHOD__ . '-dummy-tag';
443
		ChangeTags::defineTag( $dummyTag );
444
445
		$params[ 'tags' ] = $dummyTag;
446
447
		list( $result, , ) = $this->doApiRequestWithToken( $params, $session, $user, $tokenType );
448
449
		$this->assertArrayNotHasKey( 'warnings', $result, json_encode( $result ) );
450
		$this->assertArrayHasKey( 'success', $result );
451
		$lastRevid = $this->getLastRevIdFromResult( $result );
452
		if ( $lastRevid === null ) {
453
			$this->fail(
454
				'API result does not have lastrevid. Actual result: '
455
				. json_encode( $result, JSON_PRETTY_PRINT )
456
			);
457
		}
458
459
		$this->assertTrue( in_array(
460
			$dummyTag,
461
			ChangeTags::getTags( wfGetDB( DB_MASTER ), null, $lastRevid )
462
		) );
463
	}
464
465
	private function getLastRevIdFromResult( array $result ) {
466
		if ( isset( $result['entity']['lastrevid'] ) ) {
467
			return $result['entity']['lastrevid'];
468
		}
469
		if ( isset( $result['pageinfo']['lastrevid'] ) ) {
470
			return $result['pageinfo']['lastrevid'];
471
		}
472
473
		return null;
474
	}
475
476
}
477