Current File : /home/jvzmxxx/wiki1/extensions/Wikibase/repo/tests/phpunit/includes/Api/EditEntityTest.php
<?php

namespace Wikibase\Test\Repo\Api;

use UsageException;
use Wikibase\DataModel\Entity\Item;
use Wikibase\DataModel\Entity\Property;
use Wikibase\Repo\WikibaseRepo;

/**
 * @covers Wikibase\Repo\Api\EditEntity
 * @covers Wikibase\Repo\Api\ModifyEntity
 *
 * @license GPL-2.0+
 * @author Addshore
 * @author Michal Lazowik
 *
 * @group API
 * @group Wikibase
 * @group WikibaseAPI
 * @group WikibaseRepo
 * @group EditEntityTest
 * @group BreakingTheSlownessBarrier
 * @group Database
 * @group medium
 */
class EditEntityTest extends WikibaseApiTestCase {

	/**
	 * @var string[]
	 */
	private static $idMap;

	/**
	 * @var bool
	 */
	private static $hasSetup;

	protected function setUp() {
		parent::setUp();

		if ( !isset( self::$hasSetup ) ) {
			$wikibaseRepo = WikibaseRepo::getDefaultInstance();
			$store = $wikibaseRepo->getEntityStore();

			$prop = Property::newFromType( 'string' );
			$store->saveEntity( $prop, 'EditEntityTestP56', $GLOBALS['wgUser'], EDIT_NEW );
			self::$idMap['%P56%'] = $prop->getId()->getSerialization();
			self::$idMap['%StringProp%'] = $prop->getId()->getSerialization();

			$prop = Property::newFromType( 'string' );
			$store->saveEntity( $prop, 'EditEntityTestP72', $GLOBALS['wgUser'], EDIT_NEW );
			self::$idMap['%P72%'] = $prop->getId()->getSerialization();

			$this->initTestEntities( array( 'Berlin' ), self::$idMap );
			self::$idMap['%Berlin%'] = EntityTestHelper::getId( 'Berlin' );

			$p56 = self::$idMap['%P56%'];
			$berlinData = EntityTestHelper::getEntityOutput( 'Berlin' );
			self::$idMap['%BerlinP56%'] = $berlinData['claims'][$p56][0]['id'];

			$badge = new Item();
			$store->saveEntity( $badge, 'EditEntityTestQ42', $GLOBALS['wgUser'], EDIT_NEW );
			self::$idMap['%Q42%'] = $badge->getId()->getSerialization();

			$badge = new Item();
			$store->saveEntity( $badge, 'EditEntityTestQ149', $GLOBALS['wgUser'], EDIT_NEW );
			self::$idMap['%Q149%'] = $badge->getId()->getSerialization();

			$badge = new Item();
			$store->saveEntity( $badge, 'EditEntityTestQ32', $GLOBALS['wgUser'], EDIT_NEW );
			self::$idMap['%Q32%'] = $badge->getId()->getSerialization();

			$wikibaseRepo->getSettings()->setSetting( 'badgeItems', array(
				self::$idMap['%Q42%'] => '',
				self::$idMap['%Q149%'] => '',
				'Q99999' => '', // Just in case we have a wrong config
			) );
		}
		self::$hasSetup = true;
	}

	/**
	 * Provide data for a sequence of requests that will work when run in order
	 * @return array
	 */
	public function provideData() {
		return array(
			'new item' => array(
				'p' => array( 'new' => 'item', 'data' => '{}' ),
				'e' => array( 'type' => 'item' ) ),
			'new property' => array( // make sure if we pass in a valid type it is accepted
				'p' => array( 'new' => 'property', 'data' => '{"datatype":"string"}' ),
				'e' => array( 'type' => 'property' ) ),
			'new property with data' => array( // this is our current example in the api doc
				'p' => array(
					'new' => 'property',
					'data' => '{"labels":{"en-gb":{"language":"en-gb","value":"Propertylabel"}},'
						. '"descriptions":{"en-gb":{"language":"en-gb","value":"Propertydescription"}},'
						. '"datatype":"string"}'
				),
				'e' => array( 'type' => 'property' ) ),
			'add a sitelink..' => array( // make sure if we pass in a valid id it is accepted
				'p' => array(
					'data' => '{"sitelinks":{"dewiki":{"site":"dewiki",'
						. '"title":"TestPage!","badges":["%Q42%","%Q149%"]}}}'
				),
				'e' => array(
					'sitelinks' => array(
						array(
							'site' => 'dewiki',
							'title' => 'TestPage!',
							'badges' => array( '%Q42%', '%Q149%' )
						)
					)
				)
			),
			'add a label..' => array(
				'p' => array( 'data' => '{"labels":{"en":{"language":"en","value":"A Label"}}}' ),
				'e' => array(
					'sitelinks' => array(
						array(
							'site' => 'dewiki',
							'title' => 'TestPage!',
							'badges' => array( '%Q42%', '%Q149%' )
						)
					),
					'labels' => array( 'en' => 'A Label' )
				)
			),
			'add a description..' => array(
				'p' => array( 'data' => '{"descriptions":{"en":{"language":"en","value":"DESC"}}}' ),
				'e' => array(
					'sitelinks' => array(
						array(
							'site' => 'dewiki',
							'title' => 'TestPage!',
							'badges' => array( '%Q42%', '%Q149%' )
						)
					),
					'labels' => array( 'en' => 'A Label' ),
					'descriptions' => array( 'en' => 'DESC' )
				)
			),
			'remove a sitelink..' => array(
				'p' => array( 'data' => '{"sitelinks":{"dewiki":{"site":"dewiki","title":""}}}' ),
				'e' => array(
					'labels' => array( 'en' => 'A Label' ),
					'descriptions' => array( 'en' => 'DESC' ) )
				),
			'remove a label..' => array(
				'p' => array( 'data' => '{"labels":{"en":{"language":"en","value":""}}}' ),
				'e' => array( 'descriptions' => array( 'en' => 'DESC' ) ) ),
			'remove a description..' => array(
				'p' => array( 'data' => '{"descriptions":{"en":{"language":"en","value":""}}}' ),
				'e' => array( 'type' => 'item' ) ),
			'clear an item with some new value' => array(
				'p' => array(
					'data' => '{"sitelinks":{"dewiki":{"site":"dewiki","title":"page"}}}',
					'clear' => ''
				),
				'e' => array(
					'type' => 'item',
					'sitelinks' => array(
						array(
							'site' => 'dewiki',
							'title' => 'Page',
							'badges' => array()
						)
					)
				)
			),
			'clear an item with no value' => array(
				'p' => array( 'data' => '{}', 'clear' => '' ),
				'e' => array( 'type' => 'item' ) ),
			'add 2 labels' => array(
				'p' => array( 'data' => '{"labels":{"en":{"language":"en","value":"A Label"},'
					. '"sv":{"language":"sv","value":"SVLabel"}}}' ),
				'e' => array( 'labels' => array( 'en' => 'A Label', 'sv' => 'SVLabel' ) ) ),
			'remove a label with remove' => array(
				'p' => array( 'data' => '{"labels":{"en":{"language":"en","remove":true}}}' ),
				'e' => array( 'labels' => array( 'sv' => 'SVLabel' ) ) ),
			'override and add 2 descriptions' => array(
				'p' => array( 'clear' => '', 'data' => '{"descriptions":{'
					. '"en":{"language":"en","value":"DESC1"},'
					. '"de":{"language":"de","value":"DESC2"}}}' ),
				'e' => array( 'descriptions' => array( 'en' => 'DESC1', 'de' => 'DESC2' ) ) ),
			'remove a description with remove' => array(
				'p' => array( 'data' => '{"descriptions":{"en":{"language":"en","remove":true}}}' ),
				'e' => array( 'descriptions' => array( 'de' => 'DESC2' ) ) ),
			'override and add 2 sitelinks..' => array(
				'p' => array( 'data' => '{"sitelinks":{'
					. '"dewiki":{"site":"dewiki","title":"BAA"},'
					. '"svwiki":{"site":"svwiki","title":"FOO"}}}' ),
				'e' => array(
					'type' => 'item',
					'sitelinks' => array(
						array(
							'site' => 'dewiki',
							'title' => 'BAA',
							'badges' => array()
						),
						array(
							'site' => 'svwiki',
							'title' => 'FOO',
							'badges' => array()
						)
					)
				)
			),
			'unset a sitelink using the other sitelink' => array(
				'p' => array(
					'site' => 'svwiki',
					'title' => 'FOO',
					'data' => '{"sitelinks":{"dewiki":{"site":"dewiki","title":""}}}'
				),
				'e' => array(
					'type' => 'item',
					'sitelinks' => array(
						array(
							'site' => 'svwiki',
							'title' => 'FOO',
							'badges' => array()
						)
					)
				)
			),
			'set badges for a existing sitelink, title intact' => array(
				'p' => array(
					'data' => '{"sitelinks":{"svwiki":{"site":"svwiki","badges":["%Q149%","%Q42%"]}}}'
				),
				'e' => array(
					'type' => 'item',
					'sitelinks' => array(
						array(
							'site' => 'svwiki',
							'title' => 'FOO',
							'badges' => array( "%Q149%", "%Q42%" )
						)
					)
				)
			),
			'set title for a existing sitelink, badges intact' => array(
				'p' => array( 'data' => '{"sitelinks":{"svwiki":{"site":"svwiki","title":"FOO2"}}}' ),
				'e' => array(
					'type' => 'item',
					'sitelinks' => array(
						array(
							'site' => 'svwiki',
							'title' => 'FOO2',
							'badges' => array( "%Q149%", "%Q42%" )
						)
					)
				)
			),
			'delete sitelink by providing neither title nor badges' => array(
				'p' => array( 'data' => '{"sitelinks":{"svwiki":{"site":"svwiki"}}}' ),
				'e' => array(
					'type' => 'item',
				)
			),
			'add a claim' => array(
				'p' => array( 'data' => '{"claims":[{"mainsnak":{"snaktype":"value",'
					. '"property":"%P56%","datavalue":{"value":"imastring","type":"string"}},'
					. '"type":"statement","rank":"normal"}]}' ),
				'e' => array( 'claims' => array(
					'%P56%' => array(
						'mainsnak' => array(
							'snaktype' => 'value',
							'property' => '%P56%',
							'datavalue' => array( 'value' => 'imastring', 'type' => 'string' )
						),
						'type' => 'statement',
						'rank' => 'normal'
					)
				) )
			),
			'change the claim' => array(
				'p' => array( 'data' => array(
					'claims' => array(
							array(
								'id' => '%lastClaimId%',
								'mainsnak' => array(
									'snaktype' => 'value',
									'property' => '%P56%',
									'datavalue' => array(
										'value' => 'diffstring',
										'type' => 'string'
									),
								),
								'type' => 'statement',
								'rank' => 'normal',
							),
						),
					) ),
				'e' => array( 'claims' => array(
					'%P56%' => array(
						'mainsnak' => array( 'snaktype' => 'value', 'property' => '%P56%',
							'datavalue' => array(
								'value' => 'diffstring',
								'type' => 'string' ) ),
						'type' => 'statement',
						'rank' => 'normal'
					)
				) )
			),
			'remove the claim' => array(
				'p' => array( 'data' => '{"claims":[{"id":"%lastClaimId%","remove":""}]}' ),
				'e' => array( 'claims' => array() )
			),
			'add multiple claims' => array(
				'p' => array( 'data' => '{"claims":['
					. '{"mainsnak":{"snaktype":"value","property":"%P56%","datavalue":'
					. '{"value":"imastring1","type":"string"}},"type":"statement","rank":"normal"},'
					. '{"mainsnak":{"snaktype":"value","property":"%P56%","datavalue":'
					. '{"value":"imastring2","type":"string"}},"type":"statement","rank":"normal"}'
					. ']}' ),
				'e' => array( 'claims' => array(
					array(
						'mainsnak' => array(
							'snaktype' => 'value', 'property' => '%P56%',
							'datavalue' => array(
								'value' => 'imastring1',
								'type' => 'string' ) ),
						'type' => 'statement',
						'rank' => 'normal' ),
					array(
						'mainsnak' => array(
							'snaktype' => 'value', 'property' => '%P56%',
							'datavalue' => array(
								'value' => 'imastring2',
								'type' => 'string' ) ),
						'type' => 'statement',
						'rank' => 'normal' )
				) ),
			),
			'remove all stuff' => array(
				'p' => array( 'clear' => '', 'data' => '{}' ),
				'e' => array(
					'labels' => array(),
					'descriptions' => array(),
					'aliases' => array(),
					'sitelinks' => array(),
					'claims' => array()
				)
			),
			'add lots of data again' => array(
				'p' => array( 'data' => '{"claims":['
					. '{"mainsnak":{"snaktype":"value","property":"%P56%","datavalue":'
					. '{"value":"imastring1","type":"string"}},"type":"statement","rank":"normal"},'
					. '{"mainsnak":{"snaktype":"value","property":"%P56%","datavalue":'
					. '{"value":"imastring2","type":"string"}},"type":"statement","rank":"normal"}'
					. '],'
					. '"sitelinks":{"dewiki":{"site":"dewiki","title":"page"}},'
					. '"labels":{"en":{"language":"en","value":"A Label"}},'
					. '"descriptions":{"en":{"language":"en","value":"A description"}}}' ),
				'e' => array( 'type' => 'item' )
			),
			'make a null edit' => array(
				'p' => array( 'data' => '{}' ),
				'e' => array( 'nochange' => '' )
			),
			'remove all stuff in another way' => array(
				'p' => array( 'clear' => true, 'data' => '{}' ),
				'e' => array(
					'labels' => array(),
					'descriptions' => array(),
					'aliases' => array(),
					'sitelinks' => array(),
					'claims' => array()
				)
			),
		);
	}

	/**
	 * Applies self::$idMap to all data in the given data structure, recursively.
	 *
	 * @param mixed &$data
	 */
	protected function injectIds( &$data ) {
		EntityTestHelper::injectIds( $data, self::$idMap );
	}

	/**
	 * @dataProvider provideData
	 */
	public function testEditEntity( $params, $expected ) {
		$this->injectIds( $params );
		$this->injectIds( $expected );

		$p56 = '%P56%';
		$this->injectIds( $p56 );

		if ( isset( $params['data'] ) && is_array( $params['data'] ) ) {
			$params['data'] = json_encode( $params['data'] );
		}

		// -- set any defaults ------------------------------------
		$params['action'] = 'wbeditentity';
		if ( !array_key_exists( 'id', $params )
			&& !array_key_exists( 'new', $params )
			&& !array_key_exists( 'site', $params )
			&& !array_key_exists( 'title', $params )
		) {
			$params['id'] = self::$idMap['!lastEntityId!'];
		}

		// -- do the request --------------------------------------------------
		list( $result, , ) = $this->doApiRequestWithToken( $params );

		// -- steal ids for later tests -------------------------------------
		if ( array_key_exists( 'new', $params ) && stristr( $params['new'], 'item' ) ) {
			self::$idMap['!lastEntityId!'] = $result['entity']['id'];
		}
		if ( array_key_exists( 'claims', $result['entity'] )
			&& array_key_exists( $p56, $result['entity']['claims'] )
		) {
			foreach ( $result['entity']['claims'][$p56] as $claim ) {
				if ( array_key_exists( 'id', $claim ) ) {
					self::$idMap['%lastClaimId%'] = $claim['id'];
				}
			}
		}

		// -- check the result ------------------------------------------------
		$this->assertArrayHasKey( 'success', $result, "Missing 'success' marker in response." );
		$this->assertResultHasEntityType( $result );
		$this->assertArrayHasKey( 'entity', $result, "Missing 'entity' section in response." );

		$this->assertArrayHasKey(
			'id',
			$result['entity'],
			"Missing 'id' section in entity in response."
		);

		$this->assertEntityEquals( $expected, $result['entity'] );

		// -- check null edits ---------------------------------------------
		if ( isset( $expected['nochange'] ) ) {
			$this->assertArrayHasKey( 'nochange', $result['entity'] );
		}

		// -- check the item in the database -------------------------------
		$dbEntity = $this->loadEntity( $result['entity']['id'] );
		$this->assertEntityEquals( $expected, $dbEntity, false );

		// -- check the edit summary --------------------------------------------
		if ( !array_key_exists( 'warning', $expected )
			|| $expected['warning'] != 'edit-no-change'
		) {
			$this->assertRevisionSummary(
				array( 'wbeditentity' ),
				$result['entity']['lastrevid']
			);

			if ( array_key_exists( 'summary', $params ) ) {
				$this->assertRevisionSummary(
					'/' . $params['summary'] . '/',
					$result['entity']['lastrevid']
				);
			}
		}
	}

	/**
	 * Provide data for requests that will fail with a set exception, code and message
	 * @return array
	 */
	public function provideExceptionData() {
		return array(
			'no entity id given' => array(
				'p' => array( 'id' => '', 'data' => '{}' ),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'no-such-entity-id'
				) ) ),
			'invalid id' => array(
				'p' => array( 'id' => 'abcde', 'data' => '{}' ),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'no-such-entity-id'
				) ) ),
			'invalid explicit id' => array(
				'p' => array( 'id' => '1234', 'data' => '{}' ),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'no-such-entity-id'
				) ) ),
			'non existent sitelink' => array(
				'p' => array( 'site' => 'dewiki','title' => 'NonExistent', 'data' => '{}' ),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'no-such-entity-link'
				) ) ),
			'missing site (also bad title)' => array(
				'p' => array( 'title' => 'abcde', 'data' => '{}' ),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'param-missing'
				) ) ),
			'cant have id and new' => array(
				'p' => array( 'id' => 'q666', 'new' => 'item', 'data' => '{}' ),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'param-missing'
				) ) ),
			'when clearing must also have data!' => array(
				'p' => array( 'site' => 'enwiki', 'new' => 'Berlin', 'clear' => '' ),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'nodata'
				) ) ),
			'bad site' => array(
				'p' => array( 'site' => 'abcde', 'data' => '{}' ),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'unknown_site'
				) ) ),
			'no data provided' => array(
				'p' => array( 'site' => 'enwiki', 'title' => 'Berlin' ),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'nodata' // see 'no$1' in ApiBase::$messageMap
				) )
			),
			'malformed json' => array(
				'p' => array( 'site' => 'enwiki', 'title' => 'Berlin', 'data' => '{{{}' ),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'invalid-json'
				) ) ),
			'must be a json object (json_decode s this an an int)' => array(
				'p' => array( 'site' => 'enwiki', 'title' => 'Berlin', 'data' => '1234' ),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'not-recognized-array'
				) ) ),
			'must be a json object (json_decode s this an an indexed array)' => array(
				'p' => array( 'site' => 'enwiki', 'title' => 'Berlin', 'data' => '[ "xyz" ]' ),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'not-recognized-string'
					) ) ),
			'must be a json object (json_decode s this an a string)' => array(
				'p' => array( 'site' => 'enwiki', 'title' => 'Berlin', 'data' => '"string"' ),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'not-recognized-array'
				) ) ),
			'inconsistent site in json' => array(
				'p' => array(
					'site' => 'enwiki',
					'title' => 'Berlin',
					'data' => '{"sitelinks":{"ptwiki":{"site":"svwiki","title":"TestPage!"}}}'
				),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'inconsistent-site'
				) ) ),
			'inconsistent lang in json' => array(
				'p' => array(
					'site' => 'enwiki',
					'title' => 'Berlin',
					'data' => '{"labels":{"de":{"language":"pt","value":"TestPage!"}}}'
				),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'inconsistent-language'
				) ) ),
			'inconsistent unknown site in json' => array(
				'p' => array(
					'site' => 'enwiki',
					'title' => 'Berlin',
					'data' => '{"sitelinks":{"BLUB":{"site":"BLUB","title":"TestPage!"}}}'
				),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'not-recognized-site'
				) ) ),
			'inconsistent unknown languages' => array(
				'p' => array(
					'site' => 'enwiki',
					'title' => 'Berlin',
					'data' => '{"labels":{"BLUB":{"language":"BLUB","value":"ImaLabel"}}}'
				),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'not-recognized-language'
				) ) ),
			// @todo the error codes in the overly long string tests make no sense
			// and should be corrected...
			'overly long label' => array(
				'p' => array(
					'site' => 'enwiki',
					'title' => 'Berlin',
					'data' => '{"labels":{"en":{"language":"en","value":"'
						. TermTestHelper::makeOverlyLongString() . '"}}}'
				),
				'e' => array( 'exception' => array( 'type' => UsageException::class ) ) ),
			'overly long description' => array(
				'p' => array(
					'site' => 'enwiki',
					'title' => 'Berlin',
					'data' => '{"descriptions":{"en":{"language":"en","value":"'
						. TermTestHelper::makeOverlyLongString() . '"}}}'
				),
				'e' => array( 'exception' => array( 'type' => UsageException::class ) ) ),
			'missing language in labels (T54731)' => array(
				'p' => array(
					'site' => 'enwiki',
					'title' => 'Berlin',
					'data' => '{"labels":{"de":{"site":"pt","title":"TestString"}}}'
				),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'missing-language',
					'message' => '\'language\' was not found in the label or description json for de'
				) )
			),
			'removing invalid claim fails' => array(
				'p' => array(
					'site' => 'enwiki',
					'title' => 'Berlin',
					'data' => '{"claims":[{"remove":""}]}'
				),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'invalid-claim',
					'message' => 'Cannot remove a claim with no GUID'
				) )
			),
			'removing valid claim with no guid fails' => array(
				'p' => array(
					'site' => 'enwiki',
					'title' => 'Berlin',
					'data' => '{
						"remove": "",
						"claims": [ {
							"mainsnak": {
								"snaktype": "value",
								"property": "%P56%",
								"datavalue": { "value": "imastring", "type": "string" }
							},
							"type": "statement",
							"rank": "normal"
						} ]
					}'
				),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'not-recognized',
					'message' => 'Unknown key in json: remove' )
				)
			),
			'bad badge id' => array(
				'p' => array(
					'site' => 'enwiki',
					'title' => 'Berlin',
					'data' => '{"sitelinks":{"dewiki":{"site":"dewiki","title":"TestPage!",'
						. '"badges":["abc","%Q149%"]}}}'
				),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'invalid-entity-id'
				) )
			),
			'badge id is not an item id' => array(
				'p' => array(
					'site' => 'enwiki',
					'title' => 'Berlin',
					'data' => '{"sitelinks":{"dewiki":{"site":"dewiki","title":"TestPage!",'
						. '"badges":["P2","%Q149%"]}}}'
				),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'invalid-entity-id'
				) )
			),
			'badge id is not specified' => array(
				'p' => array(
					'site' => 'enwiki',
					'title' => 'Berlin',
					'data' => '{"sitelinks":{"dewiki":{"site":"dewiki","title":"TestPage!",'
						. '"badges":["%Q149%","%Q32%"]}}}'
				),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'not-badge'
				) )
			),
			'badge item does not exist' => array(
				'p' => array(
					'site' => 'enwiki',
					'title' => 'Berlin',
					'data' => '{"sitelinks":{"dewiki":{"site":"dewiki","title":"TestPage!",'
						. '"badges":["Q99999","%Q149%"]}}}'
				),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'no-such-entity'
				) )
			),
			'no sitelink - cannot change badges' => array(
				'p' => array(
					'site' => 'enwiki',
					'title' => 'Berlin',
					'data' => '{"sitelinks":{"svwiki":{"site":"svwiki",'
						. '"badges":["%Q42%","%Q149%"]}}}'
				),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'no-such-sitelink'
				) )
			),
			'bad id in serialization' => array(
				'p' => array( 'id' => '%Berlin%', 'data' => '{"id":"Q13244"}' ),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'param-invalid',
					'message' => 'Invalid field used in call: "id", must match id parameter'
				) )
			),
			'bad type in serialization' => array(
				'p' => array( 'id' => '%Berlin%', 'data' => '{"id":"%Berlin%","type":"foobar"}' ),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'param-invalid',
					'message' => 'Invalid field used in call: "type", '
						. 'must match type associated with id'
				) )
			),
			'bad main snak replacement' => array(
				'p' => array( 'id' => '%Berlin%', 'data' => json_encode( array(
						'claims' => array(
							array(
								'id' => '%BerlinP56%',
								'mainsnak' => array(
									'snaktype' => 'value',
									'property' => '%P72%',
									'datavalue' => array(
										'value' => 'anotherstring',
										'type' => 'string'
									),
								),
								'type' => 'statement',
								'rank' => 'normal' ),
						),
					) ) ),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'modification-failed',
					'message' => 'uses property %P56%, can\'t change to %P72%' ) ) ),
			'invalid main snak' => array(
				'p' => array( 'id' => '%Berlin%', 'data' => json_encode( array(
					'claims' => array(
						array(
							'id' => '%BerlinP56%',
							'mainsnak' => array(
								'snaktype' => 'value',
								'property' => '%P56%',
								'datavalue' => array( 'value' => '   ', 'type' => 'string' ),
							),
							'type' => 'statement',
							'rank' => 'normal' ),
					),
				) ) ),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'modification-failed' ) ) ),
			'properties cannot have sitelinks' => array(
				'p' => array(
					'id' => '%P56%',
					'data' => '{"sitelinks":{"dewiki":{"site":"dewiki","title":"TestPage!"}}}',
				),
				'e' => array( 'exception' => array(
					'type' => UsageException::class,
					'code' => 'not-supported',
					'message' => 'Non Items cannot have sitelinks'
				) ) ),
		);
	}

	/**
	 * @dataProvider provideExceptionData
	 */
	public function testEditEntityExceptions( $params, $expected ) {
		$this->injectIds( $params );
		$this->injectIds( $expected );

		// -- set any defaults ------------------------------------
		$params['action'] = 'wbeditentity';
		$this->doTestQueryExceptions( $params, $expected['exception'] );
	}

	public function testPropertyLabelConflict() {
		$params = array(
			'action' => 'wbeditentity',
			'data' => '{
				"datatype": "string",
				"labels": { "de": { "language": "de", "value": "LabelConflict" } }
			}',
			'new' => 'property',
		);
		$this->doApiRequestWithToken( $params );

		$expectedException = array(
			'type' => UsageException::class,
			'code' => 'failed-save',
		);
		// Repeating the same request with the same label should fail.
		$this->doTestQueryExceptions( $params, $expectedException );
	}

	public function testItemLabelWithoutDescriptionNotConflicting() {
		$params = array(
			'action' => 'wbeditentity',
			'data' => '{ "labels": { "de": { "language": "de", "value": "NotConflicting" } } }',
			'new' => 'item',
		);
		$this->doApiRequestWithToken( $params );

		// Repeating the same request with the same label should not fail.
		list( $result, , ) = $this->doApiRequestWithToken( $params );
		$this->assertArrayHasKey( 'success', $result );
	}

	public function testItemLabelDescriptionConflict() {
		$this->markTestSkippedOnMySql();

		$params = array(
			'action' => 'wbeditentity',
			'new' => 'item',
			'data' => '{
				"labels": { "de": { "language": "de", "value": "LabelDescriptionConflict" } },
				"descriptions": { "de": { "language": "de", "value": "LabelDescriptionConflict" } }
			}',
		);
		$this->doApiRequestWithToken( $params );

		$expectedException = array(
			'type' => UsageException::class,
			'code' => 'modification-failed',
		);
		// Repeating the same request with the same label and description should fail.
		$this->doTestQueryExceptions( $params, $expectedException );
	}

	public function testClearFromBadRevId() {
		$params = array(
			'action' => 'wbeditentity',
			'id' => '%Berlin%',
			'data' => '{}',
			// 'baserevid' => '', // baserevid is set below
			'clear' => '' );
		$this->injectIds( $params );

		$setupParams = array(
			'action' => 'wbeditentity',
			'id' => $params['id'],
			'clear' => '',
			'data' => '{"descriptions":{"en":{"language":"en","value":"ClearFromBadRevidDesc1"}}}',
		);

		list( $result, , ) = $this->doApiRequestWithToken( $setupParams );
		$params['baserevid'] = $result['entity']['lastrevid'];
		$setupParams['data'] = '{"descriptions":{"en":{"language":"en","value":"ClearFromBadRevidDesc2"}}}';
		$this->doApiRequestWithToken( $setupParams );

		$expectedException = array( 'type' => UsageException::class, 'code' => 'editconflict' );
		$this->doTestQueryExceptions( $params, $expectedException );
	}

	/**
	 * @see http://bugs.mysql.com/bug.php?id=10327
	 * @see TermSqlIndexTest::markTestSkippedOnMySql
	 */
	private function markTestSkippedOnMySql() {
		if ( $this->db->getType() === 'mysql' ) {
			$this->markTestSkipped( 'MySQL doesn\'t support self-joins on temporary tables' );
		}
	}

}