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

namespace Wikibase\Test\Repo\Api;

use UsageException;

/**
 * @covers Wikibase\Repo\Api\GetEntities
 *
 * Test cases are generated using the data provided in the various static arrays below.
 *
 * @license GPL-2.0+
 * @author Addshore
 *
 * @group API
 * @group Wikibase
 * @group WikibaseAPI
 * @group WikibaseRepo
 * @group GetEntitiesTest
 * @group BreakingTheSlownessBarrier
 * @group Database
 * @group medium
 */
class GetEntitiesTest extends WikibaseApiTestCase {

	private static $hasSetup;
	private static $usedHandles = array( 'StringProp', 'Berlin', 'London', 'Oslo', 'Guangzhou', 'Empty' );

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

		if ( !isset( self::$hasSetup ) ) {
			$this->initTestEntities( self::$usedHandles );
		}
		self::$hasSetup = true;
	}

	/**
	 * The key 'p' contains the params, the key 'e' contains things to expect
	 * handles will automatically be converted into the ID pointing to the item
	 */
	protected static $goodItems = array(
		array( //1 good id
			'p' => array( 'handles' => array( 'Berlin' ) ),
			'e' => array( 'count' => 1 ) ),
		array( //2 good ids
			'p' => array( 'handles' => array( 'London', 'Oslo' ) ),
			'e' => array( 'count' => 2 ) ),
		array( //1 id requested twice (should only return 1 entity)
			'p' => array( 'handles' => array( 'London', 'London' ) ),
			'e' => array( 'count' => 1 ) ),
		array( //1 good site title combination
			'p' => array( 'sites' => 'dewiki', 'titles' => 'Berlin' ),
			'e' => array( 'count' => 1 ) ),
		array( //2 title, 1 site should return 2 entities
			'p' => array( 'sites' => 'dewiki', 'titles' => 'Berlin|London' ),
			'e' => array( 'count' => 2 ) ),
		array( //2 sites, 1 title should return 1 entity
			'p' => array( 'sites' => 'dewiki|enwiki', 'titles' => 'Oslo' ),
			'e' => array( 'count' => 1 ) ),
		array( //2 sites and 2 titles should return the two entities
			'p' => array( 'sites' => 'dewiki|enwiki', 'titles' => 'Oslo|London' ),
			'e' => array( 'count' => 2 ) ),
		array( //1 id and 2 site title combinations returns 3 entities
			'p' => array( 'handles' => array( 'Berlin' ), 'sites' => 'dewiki|enwiki', 'titles' => 'Oslo|London' ),
			'e' => array( 'count' => 3 ) ),
		array( //1 title with normalization works and gets normalized
			'p' => array( 'sites' => 'dewiki', 'titles' => 'berlin', 'normalize' => '' ),
			'e' => array( 'count' => 1, 'normalized' => true ) ),
		array( //1 title with normalization works and doesn't get normalized if it doesn't need to
			'p' => array( 'sites' => 'dewiki', 'titles' => 'Berlin', 'normalize' => '' ),
			'e' => array( 'count' => 1, 'normalized' => false ) ),
		array( //we still normalise even for non existing pages
			'p' => array( 'sites' => 'dewiki', 'titles' => 'hoo', 'normalize' => '' ),
			'e' => array( 'count' => 1, 'missing' => 1, 'normalized' => true ) ),
		array( //1 non existing item by id
			'p' => array( 'ids' => 'q999999999' ),
			'e' => array( 'count' => 1, 'missing' => 1 ) ),
		array( //2 non existing items by id
			'p' => array( 'ids' => 'q999999999|q7777777' ),
			'e' => array( 'count' => 2, 'missing' => 2 ) ),
		array( //1 non existing item by site and title
			'p' => array( 'sites' => 'enwiki', 'titles' => 'IDoNotExist' ),
			'e' => array( 'count' => 1, 'missing' => 1 ) ),
		array( //2 non existing items by site and title
			'p' => array( 'sites' => 'enwiki', 'titles' => 'IDoNotExist|ImNotHere' ),
			'e' => array( 'count' => 2, 'missing' => 2 ) ),
	);

	/**
	 * goodProps contains many combinations of props that should work when used with the api module
	 * Each property in the array will cause extra assertions when the tests run
	 */
	protected static $goodProps = array(
		//individual props
		'info',
		'sitelinks',
		'aliases',
		'labels',
		'descriptions',
		'claims',
		'datatype',
		//multiple props
		'labels|sitelinks/urls|info|claims',
	);

	/**
	 * Each language in the array will cause extra assertions when the tests run
	 */
	protected static $goodLangs = array(
		//single languages
		'de',
		'zh',
		//multiple languages
		'de|nn|nb|en|en-gb|it|es|zh|ar'
	);

	/**
	 * These are all available formats for the API. we need to make sure they all work
	 * Each format is only tested against the first set of good parameters, from then on json is always used
	 */
	protected static $goodFormats = array(
		'json',
		'php',
		'xml',
	);

	/**
	 * This method builds an array of test cases using the data provided in the static arrays above
	 * @return array
	 */
	public function provideData() {
		$testCases = array();

		// Test cases for props filter
		foreach ( self::$goodProps  as $propData ) {
			foreach ( self::$goodItems as $testCase ) {
				$testCase['p']['props'] = $propData;
				$testCases[] = $testCase;
			}
		}

		// Test cases for languages
		foreach ( self::$goodLangs as $langData ) {
			foreach ( self::$goodItems as $testCase ) {
				$testCase['p']['languages'] = $langData;
				$testCases[] = $testCase;
			}
		}

		// Test cases for different formats (for one item)
		foreach ( self::$goodFormats as $formatData ) {
			$testCase = reset( self::$goodItems );
			$testCase['p']['format'] = $formatData;
			$testCases[] = $testCase;
		}

		return $testCases;
	}

	/**
	 * This method tests all valid API requests
	 * @dataProvider provideData
	 */
	public function testGetEntities( array $params, array $expected ) {
		// -- setup any further data -----------------------------------------------
		$params['ids'] = implode( '|', $this->getIdsFromHandlesAndIds( $params ) );
		$params = $this->removeHandles( $params );
		$params['action'] = 'wbgetentities';
		$expected = $this->calculateExpectedData( $expected, $params );

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

		// -- check the result --------------------------------------------------------
		$this->assertArrayHasKey( 'success', $result, "Missing 'success' marker in response." );
		$this->assertArrayHasKey( 'entities', $result, "Missing 'entities' section in response." );
		$this->assertEquals( $expected['count'], count( $result['entities'] ),
			"Request returned incorrect number of entities" );

		foreach ( $result['entities'] as $entity ) {
			if ( array_key_exists( 'missing', $expected ) && array_key_exists( 'missing', $entity ) ) {
				$this->assertArrayHasKey( 'missing', $entity );
				$this->assertGreaterThanOrEqual( 0, $expected['missing'],
					'Got missing entity but not expecting any more' );
				$expected['missing']--;

			} else {
				$this->assertEntityResult( $entity, $expected );
			}
		}

		//Our missing counter should now be at 0, if it is not we have seen too many or not enough missing entities
		if ( array_key_exists( 'missing', $expected ) ) {
			$this->assertEquals( 0, $expected['missing'] );
		}

		if ( array_key_exists( 'normalized', $expected ) && $expected['normalized'] === true ) {
			$this->assertNormalization( $result, $params );
		} else {
			$this->assertArrayNotHasKey( 'normalized', $result );
		}
	}

	private function getIdsFromHandlesAndIds( array $params ) {
		if ( array_key_exists( 'ids', $params ) ) {
			$ids = explode( '|', $params['ids'] );
		} else {
			$ids = array();
		}

		if ( array_key_exists( 'handles', $params ) ) {
			foreach ( $params['handles'] as $handle ) {
				//For every id we use we add both the uppercase and lowercase id to the test
				//This then makes sure we only get 1 entity when the only difference between the ids is the case
				$ids[] = strtolower( EntityTestHelper::getId( $handle ) );
				$ids[] = strtoupper( EntityTestHelper::getId( $handle ) );
			}
		}
		return $ids;
	}

	private function removeHandles( array $params ) {
		if ( array_key_exists( 'handles', $params ) ) {
			unset( $params['handles'] );
		}
		return $params;
	}

	private function calculateExpectedData( array $expected, array $params ) {
		//expect the props in params or the default props of the api
		if ( array_key_exists( 'props', $params ) ) {
			$expected['props'] = explode( '|', $params['props'] );
		} else {
			$expected['props'] = array( 'info', 'sitelinks', 'aliases', 'labels', 'descriptions', 'claims', 'datatype' );
		}

		//implied props
		if ( in_array( 'sitelinks/urls', $expected['props'] ) ) {
			$expected['props'][] = 'sitelinks';
		}

		//expect the languages in params or just all languages
		if ( array_key_exists( 'languages', $params ) ) {
			$expected['languages'] = explode( '|', $params['languages'] );
		} else {
			$expected['languages'] = null;
		}
		//expect order in params or expect default
		if ( array_key_exists( 'dir', $params ) ) {
			$expected['dir'] = $params['dir'];
		} else {
			$expected['dir'] = 'ascending';
		}
		return $expected;
	}

	private function assertEntityResult( array $entity, array $expected ) {
		//Assert individual props of each entity (if we want them, make sure they are there)
		if ( in_array( 'info', $expected['props'] ) ) {
			$this->assertEntityPropsInfo( $entity );
		}
		if ( in_array( 'datatype', $expected['props'] ) ) {
			$this->assertArrayHasKey( 'type', $entity, 'An entity is missing the type value' );
		}
		if ( in_array( 'sitelinks', $expected['props'] ) ) {
			$this->assertEntityPropsSitelinksBadges( $entity );
		}
		if ( in_array( 'sitelinks/urls', $expected['props'] ) ) {
			$this->assertEntityPropsSitelinksUrls( $entity );
		}
		if ( array_key_exists( 'dir', $expected ) && array_key_exists( 'sitelinks', $entity ) ) {
			$this->assertEntitySitelinkSorting( $entity, $expected );
		}

		//Assert the whole entity is as expected (claims, sitelinks, aliases, descriptions, labels)
		$expectedEntityOutput = EntityTestHelper::getEntityOutput(
			EntityTestHelper::getHandle( $entity['id'] ),
			$expected['props'],
			$expected['languages']
		);
		$this->assertEntityEquals(
			$expectedEntityOutput,
			$entity,
			false
		);
	}

	/**
	 * @param array $entity
	 */
	private function assertEntityPropsInfo( array $entity ) {
		$this->assertArrayHasKey( 'pageid', $entity, 'An entity is missing the pageid value' );
		$this->assertInternalType( 'integer', $entity['pageid'] );
		$this->assertGreaterThan( 0, $entity['pageid'] );

		$this->assertArrayHasKey( 'ns', $entity, 'An entity is missing the ns value' );
		$this->assertInternalType( 'integer', $entity['ns'] );
		$this->assertGreaterThanOrEqual( 0, $entity['ns'] );

		$this->assertArrayHasKey( 'title', $entity, 'An entity is missing the title value' );
		$this->assertInternalType( 'string', $entity['title'] );
		$this->assertNotEmpty( $entity['title'] );

		$this->assertArrayHasKey( 'lastrevid', $entity, 'An entity is missing the lastrevid value' );
		$this->assertInternalType( 'integer', $entity['lastrevid'] );
		$this->assertGreaterThanOrEqual( 0, $entity['lastrevid'] );

		$this->assertArrayHasKey( 'modified', $entity, 'An entity is missing the modified value' );
		$this->assertRegExp(
			'/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z$/',
			$entity['modified'],
			"should be in ISO 8601 format"
		);

		$this->assertArrayHasKey( 'id', $entity, 'An entity is missing the id value' );
		$this->assertArrayHasKey( 'type', $entity, 'An entity is missing the type value' );
	}

	/**
	 * @param array $entity
	 */
	private function assertEntityPropsSitelinksUrls( array $entity ) {
		foreach ( $entity['sitelinks'] as $siteLink ) {
			$this->assertArrayHasKey( 'url', $siteLink );
			$this->assertNotEmpty( $siteLink['url'] );
		}
	}

	/**
	 * @param array $entity
	 */
	private function assertEntityPropsSitelinksBadges( array $entity ) {
		foreach ( $entity['sitelinks'] as $siteLink ) {
			$this->assertArrayHasKey( 'badges', $siteLink );
			$this->assertInternalType( 'array', $siteLink['badges'] );

			foreach ( $siteLink['badges'] as $badge ) {
				$this->assertStringStartsWith( 'Q', $badge );
				$this->assertGreaterThan( 1, strlen( $badge ) );
			}
		}
	}

	private function assertEntitySitelinkSorting( array $entity, array $expected ) {
		$last = '';
		if ( $expected['dir'] == 'descending' ) {
			$last = 'zzzzzzzz';
		}

		foreach ( $entity['sitelinks'] as $link ) {
			$site = $link['site'];

			if ( $expected['dir'] == 'ascending' ) {
				$this->assertTrue(
					strcmp( $last, $site ) <= 0,
					"Failed to assert order of sitelinks, ('{$last}' vs '{$site}') <=0"
				);
			} else {
				$this->assertTrue(
					strcmp( $last, $site ) >= 0,
					"Failed to assert order of sitelinks, ('{$last}' vs '{$site}') >=0"
				);
			}
			$last = $site;
		}
	}

	private function assertNormalization( array $result, array $params ) {
		$this->assertArrayHasKey( 'normalized', $result );
		$this->assertEquals(
			$params['titles'],
			$result['normalized']['n']['from']
		);
		$this->assertEquals(
			// Normalization in unit tests is actually using Title::getPrefixedText instead of a real API call
			\Title::newFromText( $params['titles'] )->getPrefixedText(),
			$result['normalized']['n']['to']
		);
	}

	public function provideExceptionData() {
		// TODO: More exception checks should be added once bug T55038 is resolved.
		return array(
			array( //0 no params
				'p' => array(),
				'e' => array( 'exception' => array( 'type' => UsageException::class, 'code' => 'param-missing' ) ) ),
			array( //1 bad id
				'p' => array( 'ids' => 'ABCD' ),
				'e' => array( 'exception' => array( 'type' => UsageException::class, 'code' => 'no-such-entity', 'id' => 'ABCD' ) ) ),
			array( //2 bad site
				'p' => array( 'sites' => 'qwertyuiop', 'titles' => 'Berlin' ),
				'e' => array( 'exception' => array( 'type' => UsageException::class, 'code' => 'param-missing' ) ) ),
			array( //3 bad and good id
				'p' => array( 'ids' => 'q1|aaaa' ),
				'e' => array( 'exception' => array( 'type' => UsageException::class, 'code' => 'no-such-entity', 'id' => 'aaaa' ) ) ),
			array( //4 site and no title
				'p' => array( 'sites' => 'enwiki' ),
				'e' => array( 'exception' => array( 'type' => UsageException::class, 'code' => 'param-missing' ) ) ),
			array( //5 title and no site
				'p' => array( 'titles' => 'Berlin' ),
				'e' => array( 'exception' => array( 'type' => UsageException::class, 'code' => 'param-missing' ) ) ),
			array( //6 normalization fails with 2 titles
				'p' => array( 'sites' => 'enwiki', 'titles' => 'Foo|Bar' ,'normalize' => '' ),
				'e' => array( 'exception' => array( 'type' => UsageException::class, 'code' => 'params-illegal' ) ) ),
			array( //7 normalization fails with 2 sites
				'p' => array( 'sites' => 'enwiki|dewiki', 'titles' => 'Boo' ,'normalize' => '' ),
				'e' => array( 'exception' => array( 'type' => UsageException::class, 'code' => 'params-illegal' ) ) ),
			array( //8 normalization fails with 2 sites and 2 titles
				'p' => array( 'sites' => 'enwiki|dewiki', 'titles' => 'Foo|Bar' ,'normalize' => '' ),
				'e' => array( 'exception' => array( 'type' => UsageException::class, 'code' => 'params-illegal' ) ) ),
			array( //9 must request one site, one title, or an equal number of sites and titles
				'p' => array( 'sites' => 'dewiki|enwiki', 'titles' => 'Oslo|Berlin|London' ),
				'e' => array( 'exception' => array( 'type' => UsageException::class, 'code' => 'params-illegal' ) ) ),
		);
	}

	/**
	 * @dataProvider provideExceptionData
	 */
	public function testGetEntitiesExceptions( array $params, array $expected ) {
		// -- set any defaults ------------------------------------
		$params['action'] = 'wbgetentities';
		if ( array_key_exists( 'handles', $params ) ) {
			$ids = array();
			foreach ( $params['handles'] as $handle ) {
				$ids[ $handle ] = EntityTestHelper::getId( $handle );
			}
			$params['ids'] = implode( '|', $ids );
			unset( $params['handles'] );
		}
		$this->doTestQueryExceptions( $params, $expected['exception'] );
	}

	public function provideLanguageFallback() {
		return array(
			'Guangzhou Fallback' => array(
				'Guangzhou',
				array( 'de-formal', 'en', 'fr', 'yue', 'zh-cn', 'zh-hk' ),
				array(
					'de-formal' => array(
						'language' => 'de',
						'value' => 'Guangzhou',
						'for-language' => 'de-formal',
					),
					'yue' => array(
						'language' => 'yue',
						'value' => '廣州',
					),
					'zh-cn' => array(
						'language' => 'zh-cn',
						'value' => '广州市',
					),
					'zh-hk' => array(
						'language' => 'zh-hk',
						'source-language' => 'zh-cn',
						'value' => '廣州市',
					),
				),
				array(
					'de-formal' => array(
						'language' => 'en',
						'value' => 'Capital of Guangdong.',
						'for-language' => 'de-formal',
					),
					'en' => array(
						'language' => 'en',
						'value' => 'Capital of Guangdong.',
					),
					'fr' => array(
						'language' => 'en',
						'value' => 'Capital of Guangdong.',
						'for-language' => 'fr',
					),
					'yue' => array(
						'language' => 'en',
						'value' => 'Capital of Guangdong.',
						'for-language' => 'yue',
					),
					'zh-cn' => array(
						'language' => 'zh-cn',
						'source-language' => 'zh-hk',
						'value' => '广东的省会。',
					),
					'zh-hk' => array(
						'language' => 'zh-hk',
						'value' => '廣東的省會。',
					),
				),
			),
			'Oslo Fallback' => array(
				'Oslo',
				array( 'sli', 'de-formal', 'kn', 'nb' ),
				array(
					'sli' => array(
						'language' => 'de',
						'value' => 'Oslo',
						'for-language' => 'sli',
					),
					'de-formal' => array(
						'language' => 'de',
						'value' => 'Oslo',
						'for-language' => 'de-formal',
					),
					'kn' => array(
						'language' => 'en',
						'value' => 'Oslo',
						'for-language' => 'kn',
					),
					'nb' => array(
						'language' => 'nb',
						'value' => 'Oslo',
					),
				),
				array(
					'sli' => array(
						'language' => 'de',
						'value' => 'Hauptstadt der Norwegen.',
						'for-language' => 'sli',
					),
					'de-formal' => array(
						'language' => 'de',
						'value' => 'Hauptstadt der Norwegen.',
						'for-language' => 'de-formal',
					),
					'kn' => array(
						'language' => 'en',
						'value' => 'Capital city in Norway.',
						'for-language' => 'kn',
					),
					'nb' => array(
						'language' => 'nb',
						'value' => 'Hovedsted i Norge.',
					),
				),
			),
			'Oslo Fallback - Labels Only' => array(
				'Oslo',
				array( 'sli', 'de-formal', 'kn', 'nb' ),
				array(
					'sli' => array(
						'language' => 'de',
						'value' => 'Oslo',
						'for-language' => 'sli',
					),
					'de-formal' => array(
						'language' => 'de',
						'value' => 'Oslo',
						'for-language' => 'de-formal',
					),
					'kn' => array(
						'language' => 'en',
						'value' => 'Oslo',
						'for-language' => 'kn',
					),
					'nb' => array(
						'language' => 'nb',
						'value' => 'Oslo',
					),
				),
				null,
				array( 'labels' )
			),
		);
	}

	/**
	 * @dataProvider provideLanguageFallback
	 */
	public function testLanguageFallback(
		$handle,
		array $languages,
		array $expectedLabels = null,
		array $expectedDescriptions = null,
		array $props = array()
	) {
		$id = EntityTestHelper::getId( $handle );

		$params = array(
			'action' => 'wbgetentities',
			'languages' => join( '|', $languages ),
			'languagefallback' => '',
			'ids' => $id,
		);

		if ( !empty( $props ) ) {
			$params['props'] = implode( '|', $props );
		}

		list( $res,, ) = $this->doApiRequest( $params );

		if ( $expectedLabels !== null ) {
			$this->assertEquals( $expectedLabels, $res['entities'][$id]['labels'] );
		}
		if ( $expectedDescriptions !== null ) {
			$this->assertEquals( $expectedDescriptions, $res['entities'][$id]['descriptions'] );
		}
	}

	public function testSiteLinkFilter() {
		$id = EntityTestHelper::getId( 'Oslo' );

		list( $res,, ) = $this->doApiRequest(
			array(
				'action' => 'wbgetentities',
				'sitefilter' => 'dewiki|enwiki',
				'format' => 'json', // make sure IDs are used as keys
				'ids' => $id,
			)
		);

		$expectedSiteLinks = array(
			'dewiki' => array(
				'site' => 'dewiki',
				'title' => 'Oslo',
				'badges' => array(),
			),
			'enwiki' => array(
				'site' => 'enwiki',
				'title' => 'Oslo',
				'badges' => array(),
			),
		);
		$this->assertEquals( $expectedSiteLinks, $res['entities'][$id]['sitelinks'] );
	}

}