Completed
Push — master ( 62ddfb...bdf429 )
by
unknown
06:39 queued 13s
created

testGivenErroneousSaveStatus_attemptSaveDiesWithError()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.8333
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
3
namespace Wikibase\Repo\Tests\Api;
4
5
use ApiUsageException;
6
use FauxRequest;
7
use LogicException;
8
use MediaWiki\Debug\DeprecatablePropertyArray;
9
use MediaWiki\MediaWikiServices;
10
use RequestContext;
11
use Status;
12
use User;
13
use Wikibase\DataModel\Entity\EntityDocument;
14
use Wikibase\DataModel\Entity\EntityId;
15
use Wikibase\DataModel\Entity\Item;
16
use Wikibase\DataModel\Entity\ItemId;
17
use Wikibase\DataModel\Entity\ItemIdParser;
18
use Wikibase\DataModel\Entity\PropertyId;
19
use Wikibase\DataModel\Snak\PropertyNoValueSnak;
20
use Wikibase\Lib\Store\EntityStore;
21
use Wikibase\MediaInfo\DataModel\MediaInfo;
22
use Wikibase\MediaInfo\DataModel\MediaInfoId;
23
use Wikibase\Repo\Api\EntityLoadingHelper;
24
use Wikibase\Repo\Api\EntitySavingHelper;
25
use Wikibase\Repo\EditEntity\EditEntity;
26
use Wikibase\Repo\EditEntity\MediawikiEditEntityFactory;
27
use Wikibase\Repo\SummaryFormatter;
28
use Wikibase\Repo\WikibaseRepo;
29
30
/**
31
 * @covers \Wikibase\Repo\Api\EntitySavingHelper
32
 *
33
 * @group Database
34
 * @group Wikibase
35
 * @group WikibaseAPI
36
 *
37
 * @license GPL-2.0-or-later
38
 * @author Addshore
39
 * @author Daniel Kinzler
40
 */
41
class EntitySavingHelperTest extends EntityLoadingHelperTest {
42
43
	/**
44
	 * Skips a test of the given entity type is not enabled.
45
	 *
46
	 * @param string|null $requiredEntityType
47
	 */
48
	private function skipIfEntityTypeNotKnown( $requiredEntityType ) {
49
		if ( $requiredEntityType === null ) {
50
			return;
51
		}
52
53
		$enabledTypes = WikibaseRepo::getDefaultInstance()->getLocalEntityTypes();
54
		if ( !in_array( $requiredEntityType, $enabledTypes ) ) {
55
			$this->markTestSkipped( 'Entity type not enabled: ' . $requiredEntityType );
56
		}
57
	}
58
59
	/**
60
	 * @return SummaryFormatter
61
	 */
62
	private function getMockSummaryFormatter() {
63
		return $this->createMock( SummaryFormatter::class );
64
	}
65
66
	private function getMockEditEntity( ?int $calls, ?Status $status ): EditEntity {
67
		$mock = $this->createMock( EditEntity::class );
68
		$mock->expects( $calls === null ? $this->any() : $this->exactly( $calls ) )
69
			->method( 'attemptSave' )
70
			->willReturn( $status ?? Status::newGood() );
71
		return $mock;
72
	}
73
74
	private function getMockEditEntityFactory( ?int $calls, ?Status $status ): MediawikiEditEntityFactory {
75
		$mock = $this->createMock( MediawikiEditEntityFactory::class );
76
		$mock->expects( $calls === null ? $this->any() : $this->exactly( $calls ) )
77
			->method( 'newEditEntity' )
78
			->willReturn( $this->getMockEditEntity( $calls, $status ) );
79
		return $mock;
80
	}
81
82
	/**
83
	 * @return EntityStore
84
	 */
85
	private function getMockEntityStore() {
86
		$mock = $this->createMock( EntityStore::class );
87
		$mock->expects( $this->any() )
88
			->method( 'canCreateWithCustomId' )
89
			->will( $this->returnCallback( function ( EntityId $id ) {
90
				return $id->getEntityType() === 'mediainfo';
91
			} ) );
92
		$mock->expects( $this->any() )
93
			->method( 'assignFreshId' )
94
			->will( $this->returnCallback( function ( EntityDocument $entity ) {
95
				$entity->setId( new ItemId( 'Q333' ) );
96
			} ) );
97
98
		return $mock;
99
	}
100
101
	protected function getMockApiBase( array $params ) {
102
		$api = parent::getMockApiBase( $params );
103
104
		$api->expects( $this->any() )
105
			->method( 'getContext' )
106
			->will( $this->returnValue( $this->newContext( $params ) ) );
107
108
		return $api;
109
	}
110
111
	private function newContext( array $params ) {
112
		$context = new RequestContext();
113
		$context->setUser( $this->createMock( User::class ) );
114
		$context->setRequest( new FauxRequest( $params ) );
115
116
		return $context;
117
	}
118
119
	public function testLoadEntity_create_from_type() {
120
		$helper = $this->newEntitySavingHelper( [
121
			'allowCreation' => true,
122
			'params' => [ 'new' => 'item' ],
123
		] );
124
125
		$return = $helper->loadEntity();
126
		$this->assertInstanceOf( Item::class, $return );
127
		$this->assertNotNull( $return->getId(), 'New item should have a fresh ID' );
128
129
		$this->assertSame( 0, $helper->getBaseRevisionId() );
130
		$this->assertSame( EDIT_NEW, $helper->getSaveFlags() );
131
132
		$status = $helper->attemptSaveEntity( $return, 'Testing' );
133
		$this->assertTrue( $status->isGood(), 'isGood()' );
134
	}
135
136
	public function testLoadEntity_create_from_id() {
137
		$this->skipIfEntityTypeNotKnown( 'mediainfo' );
138
139
		$helper = $this->newEntitySavingHelper( [
140
			'allowCreation' => true,
141
			'params' => [ 'entity' => 'M7' ],
142
			'entityId' => new MediaInfoId( 'M7' ),
143
			'EntityIdParser' => WikibaseRepo::getDefaultInstance()->getEntityIdParser()
144
		] );
145
146
		$return = $helper->loadEntity();
147
		$this->assertInstanceOf( MediaInfo::class, $return );
148
		$this->assertSame( 'M7', $return->getId()->getSerialization() );
149
150
		$this->assertSame( 0, $helper->getBaseRevisionId() );
151
		$this->assertSame( EDIT_NEW, $helper->getSaveFlags() );
152
153
		$status = $helper->attemptSaveEntity( $return, 'Testing' );
154
		$this->assertTrue( $status->isGood(), 'isGood()' );
155
	}
156
157
	public function testLoadEntity_without_creation_support() {
158
		$helper = $this->newEntitySavingHelper( [
159
			'params' => [ 'new' => 'item' ],
160
			'dieErrorCode' => 'no-entity-id'
161
		] );
162
163
		$this->expectException( ApiUsageException::class );
164
		$helper->loadEntity();
165
	}
166
167
	public function provideLoadEntity_fail() {
168
		return [
169
			'no params' => [
170
				[],
171
				'no-entity-id'
172
			],
173
			'baserevid but no entity' => [
174
				[ 'baserevid' => 17 ],
175
				'param-illegal'
176
			],
177
			'new bad' => [
178
				[ 'new' => 'bad' ],
179
				'no-such-entity-type'
180
			],
181
			'unknown entity' => [
182
				[ 'entity' => 'Q123' ],
183
				'no-such-entity'
184
			],
185
		];
186
	}
187
188
	/**
189
	 * @dataProvider provideLoadEntity_fail
190
	 */
191
	public function testLoadEntity_fail( array $params, $dieErrorCode ) {
192
		$helper = $this->newEntityLoadingHelper( [
193
			'allowCreation' => true,
194
			'params' => $params,
195
			'dieErrorCode' => $dieErrorCode,
196
			'entityId' => isset( $params['entity'] ) ? new ItemId( $params['entity'] ) : null ,
197
		] );
198
199
		$this->expectException( ApiUsageException::class );
200
		$helper->loadEntity();
201
	}
202
203
	public function testLoadEntity_baserevid() {
204
		$itemId = new ItemId( 'Q1' );
205
206
		$revision = $this->getMockRevision();
207
		$revision->expects( $this->once() )
0 ignored issues
show
Bug introduced by
The method expects() does not seem to exist on object<Wikibase\Lib\Store\EntityRevision>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
208
			->method( 'getRevisionId' )
209
			->will( $this->returnValue( 17 ) );
210
211
		$entity = $revision->getEntity();
212
213
		$helper = $this->newEntitySavingHelper( [
214
			'params' => [ 'baserevid' => 17 ],
215
			'entityId' => $itemId,
216
			'revision' => $revision,
217
		] );
218
219
		$return = $helper->loadEntity( $itemId );
220
		$this->assertSame( $entity, $return );
221
222
		$this->assertSame( 17, $helper->getBaseRevisionId() );
223
		$this->assertSame( EDIT_UPDATE, $helper->getSaveFlags() );
224
	}
225
226
	public function testAttemptSave() {
227
		$helper = $this->newEntitySavingHelper( [
228
			'newEditEntityCalls' => 1
229
		] );
230
231
		$entity = new Item();
232
		$entity->setId( new ItemId( 'Q444' ) );
233
		$entity->getFingerprint()->setLabel( 'en', 'Foo' );
234
		$entity->getSiteLinkList()->addNewSiteLink( 'enwiki', 'APage' );
235
		$entity->getStatements()->addNewStatement( new PropertyNoValueSnak( new PropertyId( 'P8' ) ) );
236
237
		$summary = 'A String Summary';
238
		$flags = 0;
239
240
		$status = $helper->attemptSaveEntity( $entity, $summary, $flags );
241
		$this->assertTrue( $status->isGood(), 'isGood()' );
242
	}
243
244
	public function testSaveThrowsException_onNonWriteMode() {
245
		$helper = $this->newEntitySavingHelper( [
246
			'writeMode' => false,
247
			'newEditEntityCalls' => 0
248
		] );
249
250
		$this->expectException( LogicException::class );
251
		$helper->attemptSaveEntity( new Item(), '' );
252
	}
253
254
	/**
255
	 * @dataProvider errorStatusProvider
256
	 */
257
	public function testGivenErroneousSaveStatus_attemptSaveDiesWithError( array $statusValue, string $expectedErrorCode ) {
258
		$status = Status::newFatal( 'sad' );
259
260
		// intentionally not using a vanilla array as the Status value, because this was the source of a regression -> T260869
261
		$status->value = new DeprecatablePropertyArray( $statusValue, [], __METHOD__ . ' status' );
262
		$helper = $this->newEntitySavingHelper( [
263
			'dieErrorCode' => $expectedErrorCode,
264
			'attemptSaveStatus' => $status,
265
		] );
266
267
		$this->expectException( ApiUsageException::class );
268
		$helper->attemptSaveEntity( new Item(), '' );
269
	}
270
271
	public function errorStatusProvider() {
272
		yield 'with errorFlags' => [
273
			[ 'errorFlags' => EditEntity::SAVE_ERROR ],
274
			'failed-save',
275
		];
276
		yield 'with concrete errorCode' => [
277
			[ 'errorCode' => 'sadness' ],
278
			'sadness',
279
		];
280
	}
281
282
	/**
283
	 * @param array $config Associative configuration array. Known keys:
284
	 *   - params: request parameters, as an associative array
285
	 *   - revision: revision ID for EntityRevision
286
	 *   - isWriteMode: return value for isWriteMode
287
	 *   - EntityIdParser: the parser to use for entity ids
288
	 *   - entityId: The ID expected by getEntityRevision
289
	 *   - revision: EntityRevision to return from getEntityRevisions
290
	 *   - exception: Exception to throw from getEntityRevisions
291
	 *   - dieErrorCode: The error code expected by dieError
292
	 *   - dieExceptionCode: The error code expected by dieException
293
	 *   - newEditEntityCalls: expected number of calls to newEditEntity
294
	 *   - attemptSaveStatus: Status object returned by the call to EditEntity::attemptSave
295
	 *
296
	 * @return EntitySavingHelper
297
	 */
298
	protected function newEntitySavingHelper( array $config ) {
299
		$apiModule = $this->getMockApiBase( $config['params'] ?? [] );
300
		$apiModule->expects( $this->any() )
301
			->method( 'isWriteMode' )
302
			->will( $this->returnValue( $config['writeMode'] ?? true ) );
303
304
		$helper = new EntitySavingHelper(
305
			$apiModule,
306
			$config['EntityIdParser'] ?? new ItemIdParser(),
307
			$this->getMockEntityRevisionLookup(
308
				$config['entityId'] ?? null,
309
				$config['revision'] ?? null,
310
				$config['exception'] ?? null
311
			),
312
			$this->getMockErrorReporter(
313
				$config['dieExceptionCode'] ?? null,
314
				$config['dieErrorCode'] ?? null
315
			),
316
			$this->getMockSummaryFormatter(),
317
			$this->getMockEditEntityFactory(
318
				$config['newEditEntityCalls'] ?? null,
319
				$config['attemptSaveStatus'] ?? null
320
			),
321
			MediaWikiServices::getInstance()->getPermissionManager()
322
		);
323
324
		if ( $config['allowCreation'] ?? false ) {
325
			$helper->setEntityFactory( WikibaseRepo::getDefaultInstance()->getEntityFactory() );
326
			$helper->setEntityStore( $this->getMockEntityStore() );
327
		}
328
329
		return $helper;
330
	}
331
332
	/**
333
	 * @param array $config Associative configuration array. Known keys:
334
	 *   - params: request parameters, as an associative array
335
	 *   - entityId: The ID expected by getEntityRevision
336
	 *   - revision: EntityRevision to return from getEntityRevisions
337
	 *   - exception: Exception to throw from getEntityRevisions
338
	 *   - dieErrorCode: The error code expected by dieError
339
	 *   - dieExceptionCode: The error code expected by dieException
340
	 *
341
	 * @return EntityLoadingHelper
342
	 */
343
	protected function newEntityLoadingHelper( array $config ) {
344
		return $this->newEntitySavingHelper( $config );
345
	}
346
347
}
348