diff --git a/repo/rest-api/routes.dev.json b/repo/rest-api/routes.dev.json index 0202f0bcff..04841836df 100644 --- a/repo/rest-api/routes.dev.json +++ b/repo/rest-api/routes.dev.json @@ -13,5 +13,10 @@ "path": "/wikibase/v0/entities/items/{item_id}/descriptions_with_language_fallback/{language_code}", "method": "GET", "factory": "Wikibase\\Repo\\RestApi\\RouteHandlers\\GetItemDescriptionWithFallbackRouteHandler::factory" + }, + { + "path": "/wikibase/v0/entities/properties/{property_id}/descriptions_with_language_fallback/{language_code}", + "method": "GET", + "factory": "Wikibase\\Repo\\RestApi\\RouteHandlers\\GetPropertyDescriptionWithFallbackRouteHandler::factory" } ] diff --git a/repo/rest-api/specs/resources/descriptions/description-with-fallback-for-property.json b/repo/rest-api/specs/resources/descriptions/description-with-fallback-for-property.json new file mode 100644 index 0000000000..9d1bb2cac9 --- /dev/null +++ b/repo/rest-api/specs/resources/descriptions/description-with-fallback-for-property.json @@ -0,0 +1,25 @@ +{ + "get": { + "operationId": "getPropertyDescriptionWithFallback", + "tags": [ "descriptions" ], + "summary": "[WIP] Retrieve a Property's description in a specific language, with language fallback", + "description": "This endpoint is currently in development and is not recommended for production use", + "parameters": [ + { "$ref": "../../global/parameters.json#/PropertyId" }, + { "$ref": "../../global/parameters.json#/LanguageCode" }, + { "$ref": "../../global/parameters.json#/IfNoneMatch" }, + { "$ref": "../../global/parameters.json#/IfModifiedSince" }, + { "$ref": "../../global/parameters.json#/IfMatch" }, + { "$ref": "../../global/parameters.json#/IfUnmodifiedSince" }, + { "$ref": "../../global/parameters.json#/Authorization" } + ], + "responses": { + "200": { "$ref": "../../global/responses.json#/Description" }, + "304": { "$ref": "../../global/responses.json#/NotModified" }, + "400": { "$ref": "../../global/responses.json#/InvalidTermByLanguageInput" }, + "404": { "$ref": "../../global/responses.json#/ResourceNotFound" }, + "412": { "$ref": "../../global/responses.json#/PreconditionFailedError" }, + "500": { "$ref": "../../global/responses.json#/UnexpectedError" } + } + } +} diff --git a/repo/rest-api/specs/resources/index.json b/repo/rest-api/specs/resources/index.json index c7529fa73f..973a3f75ca 100644 --- a/repo/rest-api/specs/resources/index.json +++ b/repo/rest-api/specs/resources/index.json @@ -82,6 +82,9 @@ "/entities/properties/{property_id}/descriptions/{language_code}": { "$ref": "./descriptions/description-in-language-for-property.json" }, + "/entities/properties/{property_id}/descriptions_with_language_fallback/{language_code}": { + "$ref": "./descriptions/description-with-fallback-for-property.json" + }, "/entities/items/{item_id}/statements": { "$ref": "./statements/list-for-item.json" }, diff --git a/repo/rest-api/src/Application/UseCaseRequestValidation/DeserializedRequestAdapter.php b/repo/rest-api/src/Application/UseCaseRequestValidation/DeserializedRequestAdapter.php index 5b02571de5..1ffbab86cc 100644 --- a/repo/rest-api/src/Application/UseCaseRequestValidation/DeserializedRequestAdapter.php +++ b/repo/rest-api/src/Application/UseCaseRequestValidation/DeserializedRequestAdapter.php @@ -32,6 +32,7 @@ use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyAliasesInLanguage\DeserializedGetPropertyAliasesInLanguageRequest; use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyDescription\DeserializedGetPropertyDescriptionRequest; use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyDescriptions\DeserializedGetPropertyDescriptionsRequest; +use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyDescriptionWithFallback\DeserializedGetPropertyDescriptionWithFallbackRequest; use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyLabel\DeserializedGetPropertyLabelRequest; use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyLabels\DeserializedGetPropertyLabelsRequest; use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyLabelWithFallback\DeserializedGetPropertyLabelWithFallbackRequest; @@ -88,6 +89,7 @@ class DeserializedRequestAdapter implements DeserializedGetPropertyRequest, DeserializedGetPropertyLabelsRequest, DeserializedGetPropertyDescriptionsRequest, + DeserializedGetPropertyDescriptionWithFallbackRequest, DeserializedGetPropertyAliasesRequest, DeserializedGetPropertyAliasesInLanguageRequest, DeserializedGetPropertyStatementRequest, diff --git a/repo/rest-api/src/Application/UseCases/GetPropertyDescriptionWithFallback/DeserializedGetPropertyDescriptionWithFallbackRequest.php b/repo/rest-api/src/Application/UseCases/GetPropertyDescriptionWithFallback/DeserializedGetPropertyDescriptionWithFallbackRequest.php new file mode 100644 index 0000000000..d9b65f798f --- /dev/null +++ b/repo/rest-api/src/Application/UseCases/GetPropertyDescriptionWithFallback/DeserializedGetPropertyDescriptionWithFallbackRequest.php @@ -0,0 +1,12 @@ +validator = $validator; + $this->getRevisionMetadata = $getRevisionMetadata; + $this->descriptionRetriever = $descriptionRetriever; + } + + /** + * @throws UseCaseError + */ + public function execute( GetPropertyDescriptionWithFallbackRequest $request ): GetPropertyDescriptionWithFallbackResponse { + $deserializedRequest = $this->validator->validateAndDeserialize( $request ); + $propertyId = $deserializedRequest->getPropertyId(); + $languageCode = $deserializedRequest->getLanguageCode(); + + [ $revisionId, $lastModified ] = $this->getRevisionMetadata->execute( $propertyId ); + + $description = $this->descriptionRetriever->getDescription( $propertyId, $languageCode ); + if ( !$description ) { + throw UseCaseError::newResourceNotFound( 'description' ); + } + + return new GetPropertyDescriptionWithFallbackResponse( $description, $lastModified, $revisionId ); + } +} diff --git a/repo/rest-api/src/Application/UseCases/GetPropertyDescriptionWithFallback/GetPropertyDescriptionWithFallbackRequest.php b/repo/rest-api/src/Application/UseCases/GetPropertyDescriptionWithFallback/GetPropertyDescriptionWithFallbackRequest.php new file mode 100644 index 0000000000..a52655146f --- /dev/null +++ b/repo/rest-api/src/Application/UseCases/GetPropertyDescriptionWithFallback/GetPropertyDescriptionWithFallbackRequest.php @@ -0,0 +1,30 @@ +propertyId = $propertyId; + $this->languageCode = $languageCode; + } + + public function getPropertyId(): string { + return $this->propertyId; + } + + public function getLanguageCode(): string { + return $this->languageCode; + } + +} diff --git a/repo/rest-api/src/Application/UseCases/GetPropertyDescriptionWithFallback/GetPropertyDescriptionWithFallbackResponse.php b/repo/rest-api/src/Application/UseCases/GetPropertyDescriptionWithFallback/GetPropertyDescriptionWithFallbackResponse.php new file mode 100644 index 0000000000..076c5a5ab7 --- /dev/null +++ b/repo/rest-api/src/Application/UseCases/GetPropertyDescriptionWithFallback/GetPropertyDescriptionWithFallbackResponse.php @@ -0,0 +1,34 @@ +description = $description; + $this->lastModified = $lastModified; + $this->revisionId = $revisionId; + } + + public function getDescription(): Description { + return $this->description; + } + + public function getLastModified(): string { + return $this->lastModified; + } + + public function getRevisionId(): int { + return $this->revisionId; + } + +} diff --git a/repo/rest-api/src/Application/UseCases/GetPropertyDescriptionWithFallback/GetPropertyDescriptionWithFallbackValidator.php b/repo/rest-api/src/Application/UseCases/GetPropertyDescriptionWithFallback/GetPropertyDescriptionWithFallbackValidator.php new file mode 100644 index 0000000000..f79498067e --- /dev/null +++ b/repo/rest-api/src/Application/UseCases/GetPropertyDescriptionWithFallback/GetPropertyDescriptionWithFallbackValidator.php @@ -0,0 +1,18 @@ +useCase = $useCase; + $this->middlewareHandler = $middlewareHandler; + $this->responseFactory = $responseFactory; + } + + public static function factory(): self { + return new self( + WbRestApi::getGetPropertyDescriptionWithFallback(), + new MiddlewareHandler( [ + WbRestApi::getUnexpectedErrorHandlerMiddleware(), + new UserAgentCheckMiddleware(), + new AuthenticationMiddleware(), + WbRestApi::getPreconditionMiddlewareFactory()->newPreconditionMiddleware( + fn( RequestInterface $request ): string => $request->getPathParam( self::PROPERTY_ID_PATH_PARAM ) + ), + ] ), + new ResponseFactory() + ); + } + + public function needsWriteAccess(): bool { + return false; + } + + /** + * @param mixed ...$args + */ + public function run( ...$args ): Response { + return $this->middlewareHandler->run( $this, [ $this, 'runUseCase' ], $args ); + } + + public function runUseCase( string $propertyId, string $languageCode ): Response { + try { + return $this->newSuccessHttpResponse( + $this->useCase->execute( new GetPropertyDescriptionWithFallbackRequest( $propertyId, $languageCode ) ) + ); + } catch ( UseCaseError $e ) { + return $this->responseFactory->newErrorResponseFromException( $e ); + } + } + + public function getParamSettings(): array { + return [ + self::PROPERTY_ID_PATH_PARAM => [ + self::PARAM_SOURCE => 'path', + ParamValidator::PARAM_TYPE => 'string', + ParamValidator::PARAM_REQUIRED => true, + ], + self::LANGUAGE_CODE_PATH_PARAM => [ + self::PARAM_SOURCE => 'path', + ParamValidator::PARAM_TYPE => 'string', + ParamValidator::PARAM_REQUIRED => true, + ], + ]; + } + + private function newSuccessHttpResponse( GetPropertyDescriptionWithFallbackResponse $useCaseResponse ): Response { + $httpResponse = $this->getResponseFactory()->create(); + $httpResponse->setHeader( 'Content-Type', 'application/json' ); + $httpResponse->setHeader( 'Last-Modified', wfTimestamp( TS_RFC2822, $useCaseResponse->getLastModified() ) ); + $this->setEtagFromRevId( $httpResponse, $useCaseResponse->getRevisionId() ); + $httpResponse->setBody( new StringStream( json_encode( $useCaseResponse->getDescription()->getText() ) ) ); + + return $httpResponse; + } + + private function setEtagFromRevId( Response $response, int $revId ): void { + $response->setHeader( 'ETag', "\"$revId\"" ); + } + + /** + * Preconditions are checked via {@link PreconditionMiddleware} + */ + public function checkPreconditions(): ?ResponseInterface { + return null; + } + +} diff --git a/repo/rest-api/src/RouteHandlers/openapi.json b/repo/rest-api/src/RouteHandlers/openapi.json index 6655cbe9c0..07805e8034 100644 --- a/repo/rest-api/src/RouteHandlers/openapi.json +++ b/repo/rest-api/src/RouteHandlers/openapi.json @@ -1441,6 +1441,59 @@ } } }, + "/entities/properties/{property_id}/descriptions_with_language_fallback/{language_code}": { + "get": { + "operationId": "getPropertyDescriptionWithFallback", + "tags": [ + "descriptions" + ], + "summary": "[WIP] Retrieve a Property's description in a specific language, with language fallback", + "description": "This endpoint is currently in development and is not recommended for production use", + "parameters": [ + { + "$ref": "#/components/parameters/PropertyId" + }, + { + "$ref": "#/components/parameters/LanguageCode" + }, + { + "$ref": "#/components/parameters/IfNoneMatch" + }, + { + "$ref": "#/components/parameters/IfModifiedSince" + }, + { + "$ref": "#/components/parameters/IfMatch" + }, + { + "$ref": "#/components/parameters/IfUnmodifiedSince" + }, + { + "$ref": "#/components/parameters/Authorization" + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/Description" + }, + "304": { + "$ref": "#/components/responses/NotModified" + }, + "400": { + "$ref": "#/components/responses/InvalidTermByLanguageInput" + }, + "404": { + "$ref": "#/components/responses/ResourceNotFound" + }, + "412": { + "$ref": "#/components/responses/PreconditionFailedError" + }, + "500": { + "$ref": "#/components/responses/UnexpectedError" + } + } + } + }, "/entities/items/{item_id}/statements": { "get": { "operationId": "getItemStatements", diff --git a/repo/rest-api/src/WbRestApi.ServiceWiring.php b/repo/rest-api/src/WbRestApi.ServiceWiring.php index 8c1a25b9eb..a0ebba267c 100644 --- a/repo/rest-api/src/WbRestApi.ServiceWiring.php +++ b/repo/rest-api/src/WbRestApi.ServiceWiring.php @@ -82,6 +82,7 @@ use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyAliasesInLanguage\GetPropertyAliasesInLanguage; use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyDescription\GetPropertyDescription; use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyDescriptions\GetPropertyDescriptions; +use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyDescriptionWithFallback\GetPropertyDescriptionWithFallback; use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyLabel\GetPropertyLabel; use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyLabels\GetPropertyLabels; use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyLabelWithFallback\GetPropertyLabelWithFallback; @@ -680,6 +681,14 @@ function( MediaWikiServices $services ): ItemSerializationRequestValidatingDeser ); }, + 'WbRestApi.GetPropertyDescriptionWithFallback' => function( MediaWikiServices $services ): GetPropertyDescriptionWithFallback { + return new GetPropertyDescriptionWithFallback( + WbRestApi::getValidatingRequestDeserializer( $services ), + WbRestApi::getGetLatestPropertyRevisionMetadata( $services ), + WbRestApi::getTermLookupEntityTermsRetriever( $services ) + ); + }, + 'WbRestApi.GetPropertyLabel' => function( MediaWikiServices $services ): GetPropertyLabel { return new GetPropertyLabel( WbRestApi::getValidatingRequestDeserializer( $services ), diff --git a/repo/rest-api/src/WbRestApi.php b/repo/rest-api/src/WbRestApi.php index 3ac090b5e0..1a01f43d63 100644 --- a/repo/rest-api/src/WbRestApi.php +++ b/repo/rest-api/src/WbRestApi.php @@ -35,6 +35,7 @@ use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyAliasesInLanguage\GetPropertyAliasesInLanguage; use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyDescription\GetPropertyDescription; use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyDescriptions\GetPropertyDescriptions; +use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyDescriptionWithFallback\GetPropertyDescriptionWithFallback; use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyLabel\GetPropertyLabel; use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyLabels\GetPropertyLabels; use Wikibase\Repo\RestApi\Application\UseCases\GetPropertyLabelWithFallback\GetPropertyLabelWithFallback; @@ -452,6 +453,13 @@ public static function getGetPropertyDescriptions( ContainerInterface $services ->get( 'WbRestApi.GetPropertyDescriptions' ); } + public static function getGetPropertyDescriptionWithFallback( + ContainerInterface $services = null + ): GetPropertyDescriptionWithFallback { + return ( $services ?: MediaWikiServices::getInstance() ) + ->get( 'WbRestApi.GetPropertyDescriptionWithFallback' ); + } + public static function getGetPropertyAliasesInLanguage( ContainerInterface $services = null ): GetPropertyAliasesInLanguage { return ( $services ?: MediaWikiServices::getInstance() ) ->get( 'WbRestApi.GetPropertyAliasesInLanguage' ); diff --git a/repo/rest-api/tests/mocha/api-testing/GetPropertyDescriptionwithFallbackTest.js b/repo/rest-api/tests/mocha/api-testing/GetPropertyDescriptionwithFallbackTest.js new file mode 100644 index 0000000000..f8dbe12068 --- /dev/null +++ b/repo/rest-api/tests/mocha/api-testing/GetPropertyDescriptionwithFallbackTest.js @@ -0,0 +1,83 @@ +'use strict'; + +const { assert } = require( 'api-testing' ); +const { expect } = require( '../helpers/chaiHelper' ); +const { createEntity, getLatestEditMetadata } = require( '../helpers/entityHelper' ); +const { newGetPropertyDescriptionWithFallbackRequestBuilder } = require( '../helpers/RequestBuilderFactory' ); +const { utils } = require( 'api-testing' ); +const { assertValidError } = require( '../helpers/responseValidator' ); + +describe( newGetPropertyDescriptionWithFallbackRequestBuilder().getRouteDescription(), () => { + let propertyId; + const propertyEnDescription = `string-property-description-${utils.uniq()}`; + + before( async () => { + const testProperty = await createEntity( 'property', { + labels: { en: { language: 'en', value: `string-property-${utils.uniq()}` } }, + descriptions: { + en: { language: 'en', value: propertyEnDescription } + }, + datatype: 'string' + } ); + + propertyId = testProperty.entity.id; + } ); + + it( 'can get a language specific description of a property', async () => { + const testPropertyCreationMetadata = await getLatestEditMetadata( propertyId ); + const response = await newGetPropertyDescriptionWithFallbackRequestBuilder( propertyId, 'en' ) + .assertValidRequest().makeRequest(); + + expect( response ).to.have.status( 200 ); + assert.strictEqual( response.body, propertyEnDescription ); + assert.strictEqual( response.header.etag, `"${testPropertyCreationMetadata.revid}"` ); + assert.strictEqual( response.header[ 'last-modified' ], testPropertyCreationMetadata.timestamp ); + } ); + + it( '400 - invalid property ID', async () => { + const response = await newGetPropertyDescriptionWithFallbackRequestBuilder( 'X123', 'en' ) + .assertInvalidRequest() + .makeRequest(); + + assertValidError( + response, + 400, + 'invalid-path-parameter', + { parameter: 'property_id' } + ); + } ); + + it( '400 - invalid language code', async () => { + const response = await newGetPropertyDescriptionWithFallbackRequestBuilder( propertyId, '1e' ) + .assertInvalidRequest() + .makeRequest(); + + assertValidError( + response, + 400, + 'invalid-path-parameter', + { parameter: 'language_code' } + ); + } ); + + it( 'responds 404 in case the property does not exist', async () => { + const nonExistentProperty = 'P99999999'; + const response = await newGetPropertyDescriptionWithFallbackRequestBuilder( nonExistentProperty, 'en' ) + .assertValidRequest() + .makeRequest(); + + assertValidError( response, 404, 'resource-not-found', { resource_type: 'property' } ); + assert.strictEqual( response.body.message, 'The requested resource does not exist' ); + } ); + + it( 'responds 404 in case the property has no description in the requested language', async () => { + const languageCode = 'ko'; + const response = await newGetPropertyDescriptionWithFallbackRequestBuilder( propertyId, languageCode ) + .assertValidRequest() + .makeRequest(); + + assertValidError( response, 404, 'resource-not-found', { resource_type: 'description' } ); + assert.strictEqual( response.body.message, 'The requested resource does not exist' ); + } ); + +} ); diff --git a/repo/rest-api/tests/mocha/helpers/RequestBuilderFactory.js b/repo/rest-api/tests/mocha/helpers/RequestBuilderFactory.js index 5a8ac352c2..6d9e4df948 100644 --- a/repo/rest-api/tests/mocha/helpers/RequestBuilderFactory.js +++ b/repo/rest-api/tests/mocha/helpers/RequestBuilderFactory.js @@ -82,6 +82,13 @@ module.exports = { .withPathParam( 'language_code', languageCode ); }, + newGetPropertyDescriptionWithFallbackRequestBuilder( propertyId, languageCode ) { + return new RequestBuilder() + .withRoute( 'GET', '/entities/properties/{property_id}/descriptions_with_language_fallback/{language_code}' ) + .withPathParam( 'property_id', propertyId ) + .withPathParam( 'language_code', languageCode ); + }, + newGetItemDescriptionsRequestBuilder( itemId ) { return new RequestBuilder() .withRoute( 'GET', '/entities/items/{item_id}/descriptions' ) diff --git a/repo/rest-api/tests/mocha/openapi-validation/GetPropertyDescriptionWithFallbackTest.js b/repo/rest-api/tests/mocha/openapi-validation/GetPropertyDescriptionWithFallbackTest.js new file mode 100644 index 0000000000..8bf6f07dc9 --- /dev/null +++ b/repo/rest-api/tests/mocha/openapi-validation/GetPropertyDescriptionWithFallbackTest.js @@ -0,0 +1,61 @@ +'use strict'; + +const { utils } = require( 'api-testing' ); +const { expect } = require( '../helpers/chaiHelper' ); +const { createEntity } = require( '../helpers/entityHelper' ); +const { newGetPropertyDescriptionWithFallbackRequestBuilder } = require( '../helpers/RequestBuilderFactory' ); + +describe( newGetPropertyDescriptionWithFallbackRequestBuilder().getRouteDescription(), () => { + + let propertyId; + let lastRevisionId; + const languageCode = 'en'; + + before( async () => { + const createPropertyResponse = await createEntity( 'property', { + descriptions: [ { language: languageCode, value: 'an-English-description-' + utils.uniq() } ], + datatype: 'string' + } ); + + propertyId = createPropertyResponse.entity.id; + lastRevisionId = createPropertyResponse.entity.lastrevid; + } ); + + it( '200 OK response is valid', async () => { + const response = await newGetPropertyDescriptionWithFallbackRequestBuilder( propertyId, languageCode ).makeRequest(); + + expect( response ).to.have.status( 200 ); + expect( response ).to.satisfyApiSpec; + } ); + + it( '304 Not Modified response is valid', async () => { + const response = await newGetPropertyDescriptionWithFallbackRequestBuilder( propertyId, languageCode ) + .withHeader( 'If-None-Match', `"${lastRevisionId}"` ) + .makeRequest(); + + expect( response ).to.have.status( 304 ); + expect( response ).to.satisfyApiSpec; + } ); + + it( '400 Bad Request response is valid for an invalid property ID', async () => { + const response = await newGetPropertyDescriptionWithFallbackRequestBuilder( 'X123', languageCode ).makeRequest(); + + expect( response ).to.have.status( 400 ); + expect( response ).to.satisfyApiSpec; + } ); + + it( '404 Not Found response is valid for a non-existing property', async () => { + const response = await newGetPropertyDescriptionWithFallbackRequestBuilder( 'P99999', languageCode ).makeRequest(); + + expect( response ).to.have.status( 404 ); + expect( response ).to.satisfyApiSpec; + } ); + + it( '404 Not Found response is valid if there is no description in the requested language', async () => { + const response = await newGetPropertyDescriptionWithFallbackRequestBuilder( propertyId, 'ko' ).makeRequest(); + + expect( response ).to.have.status( 404 ); + expect( response ).to.satisfyApiSpec; + } ); + +} ); diff --git a/repo/rest-api/tests/phpunit/Application/UseCases/GetPropertyDescriptionWithFallback/GetPropertyDescriptionWithFallbackTest.php b/repo/rest-api/tests/phpunit/Application/UseCases/GetPropertyDescriptionWithFallback/GetPropertyDescriptionWithFallbackTest.php new file mode 100644 index 0000000000..d3a7bf29b0 --- /dev/null +++ b/repo/rest-api/tests/phpunit/Application/UseCases/GetPropertyDescriptionWithFallback/GetPropertyDescriptionWithFallbackTest.php @@ -0,0 +1,105 @@ +getRevisionMetadata = $this->createStub( GetLatestPropertyRevisionMetadata::class ); + $this->descriptionRetriever = $this->createStub( PropertyDescriptionRetriever::class ); + } + + public function testSuccess(): void { + $languageCode = 'en'; + $description = new Description( $languageCode, 'English test property' ); + + $propertyId = new NumericPropertyId( 'P10' ); + $lastModified = '20201111070707'; + $revisionId = 2; + + $this->getRevisionMetadata = $this->createStub( GetLatestPropertyRevisionMetadata::class ); + $this->getRevisionMetadata->method( 'execute' )->willReturn( [ $revisionId, $lastModified ] ); + + $this->descriptionRetriever = $this->createMock( PropertyDescriptionRetriever::class ); + $this->descriptionRetriever->expects( $this->once() ) + ->method( 'getDescription' ) + ->with( $propertyId, $languageCode ) + ->willReturn( $description ); + + $response = $this->newUseCase() + ->execute( new GetPropertyDescriptionWIthFallbackRequest( "$propertyId", $languageCode ) ); + $this->assertEquals( new GetPropertyDescriptionWithFallbackResponse( $description, $lastModified, $revisionId ), $response ); + } + + public function testGivenPropertyNotFound_throws(): void { + $expectedException = $this->createStub( UseCaseError::class ); + $this->getRevisionMetadata = $this->createStub( GetLatestPropertyRevisionMetadata::class ); + $this->getRevisionMetadata->method( 'execute' ) + ->willThrowException( $expectedException ); + + try { + $this->newUseCase()->execute( new GetPropertyDescriptionWithFallbackRequest( 'P999999', 'en' ) ); + $this->fail( 'this should not be reached' ); + } catch ( UseCaseError $e ) { + $this->assertSame( $expectedException, $e ); + } + } + + public function testGivenDescriptionDoesNotExist_throws(): void { + $propertyId = 'P123'; + $languageCode = 'en'; + + $this->getRevisionMetadata = $this->createStub( GetLatestPropertyRevisionMetadata::class ); + $this->getRevisionMetadata->method( 'execute' )->willReturn( [ 123, '20230926070707' ] ); + + try { + $this->newUseCase()->execute( new GetPropertyDescriptionWithFallbackRequest( $propertyId, $languageCode ) ); + $this->fail( 'expected exception was not thrown' ); + } catch ( UseCaseError $e ) { + $this->assertEquals( + UseCaseError::newResourceNotFound( 'description' ), + $e + ); + } + } + + public function testGivenInvalidRequest_throws(): void { + try { + $this->newUseCase()->execute( new GetPropertyDescriptionWithFallbackRequest( 'X321', 'en' ) ); + $this->fail( 'this should not be reached' ); + } catch ( UseCaseError $e ) { + $this->assertSame( UseCaseError::INVALID_PATH_PARAMETER, $e->getErrorCode() ); + } + } + + private function newUseCase(): GetPropertyDescriptionWithFallback { + return new GetPropertyDescriptionWithFallback( + new TestValidatingRequestDeserializer(), + $this->getRevisionMetadata, + $this->descriptionRetriever + ); + } +}