diff --git a/repo/rest-api/specs/global/responses.json b/repo/rest-api/specs/global/responses.json index 62679d342aa..59a895b851e 100644 --- a/repo/rest-api/specs/global/responses.json +++ b/repo/rest-api/specs/global/responses.json @@ -389,6 +389,7 @@ }, "patch-result-invalid-key": { "$ref": "./examples.json#/PatchResultInvalidKeyExample" }, "patch-result-invalid-value": { "$ref": "./examples.json#/PatchResultInvalidValueExample" }, + "patch-result-referenced-resource-not-found": { "$ref": "./examples.json#/PatchResultResourceNotFoundExample" }, "patch-result-value-too-long": { "$ref": "./examples.json#/PatchResultValueTooLongExample" }, "patch-result-modified-read-only-value": { "$ref": "./examples.json#/PatchResultModifiedReadOnlyValue" @@ -493,7 +494,8 @@ "patch-result-invalid-value": { "$ref": "./examples.json#/PatchResultInvalidValueExample" }, "patch-result-modified-read-only-value": { "$ref": "./examples.json#/PatchResultModifiedReadOnlyValue" - } + }, + "patch-result-referenced-resource-not-found": { "$ref": "./examples.json#/PatchResultResourceNotFoundExample" } } } }, diff --git a/repo/rest-api/src/Application/Serialization/Exceptions/PropertyNotFoundException.php b/repo/rest-api/src/Application/Serialization/Exceptions/PropertyNotFoundException.php new file mode 100644 index 00000000000..ca964f6e21e --- /dev/null +++ b/repo/rest-api/src/Application/Serialization/Exceptions/PropertyNotFoundException.php @@ -0,0 +1,28 @@ +value = $value; + $this->path = $path; + + parent::__construct( $message, 0, $previous ); + } + + public function getValue(): string { + return $this->value; + } + + public function getPath(): string { + return $this->path; + } +} diff --git a/repo/rest-api/src/Application/Serialization/PropertyValuePairDeserializer.php b/repo/rest-api/src/Application/Serialization/PropertyValuePairDeserializer.php index 1a5a3c11503..dd1cb866af9 100644 --- a/repo/rest-api/src/Application/Serialization/PropertyValuePairDeserializer.php +++ b/repo/rest-api/src/Application/Serialization/PropertyValuePairDeserializer.php @@ -13,6 +13,7 @@ use Wikibase\DataModel\Snak\Snak; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\InvalidFieldException; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\MissingFieldException; +use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\PropertyNotFoundException; use Wikibase\Repo\RestApi\Domain\ReadModel\Value; /** @@ -37,6 +38,7 @@ public function __construct( /** * @throws MissingFieldException * @throws InvalidFieldException + * @throws PropertyNotFoundException */ public function deserialize( array $serialization, string $basePath = '' ): Snak { $this->validateSerialization( $serialization, $basePath ); @@ -46,7 +48,7 @@ public function deserialize( array $serialization, string $basePath = '' ): Snak try { $dataTypeId = $this->dataTypeLookup->getDataTypeIdForProperty( $propertyId ); } catch ( Exception $e ) { - throw new InvalidFieldException( 'id', $serialization['property']['id'], "$basePath/property/id" ); + throw new PropertyNotFoundException( $serialization['property']['id'], "$basePath/property/id" ); } switch ( $serialization['value']['type'] ) { diff --git a/repo/rest-api/src/Application/Serialization/ReferenceDeserializer.php b/repo/rest-api/src/Application/Serialization/ReferenceDeserializer.php index e235a07d5cf..e624b0be743 100644 --- a/repo/rest-api/src/Application/Serialization/ReferenceDeserializer.php +++ b/repo/rest-api/src/Application/Serialization/ReferenceDeserializer.php @@ -5,6 +5,7 @@ use Wikibase\DataModel\Reference; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\InvalidFieldException; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\MissingFieldException; +use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\PropertyNotFoundException; /** * @license GPL-2.0-or-later @@ -20,6 +21,7 @@ public function __construct( PropertyValuePairDeserializer $propertyValuePairDes /** * @throws MissingFieldException * @throws InvalidFieldException + * @throws PropertyNotFoundException */ public function deserialize( array $serialization, string $basePath = '' ): Reference { if ( count( $serialization ) && array_is_list( $serialization ) ) { diff --git a/repo/rest-api/src/Application/Serialization/StatementDeserializer.php b/repo/rest-api/src/Application/Serialization/StatementDeserializer.php index a2d7d9e2ca9..49d759dc823 100644 --- a/repo/rest-api/src/Application/Serialization/StatementDeserializer.php +++ b/repo/rest-api/src/Application/Serialization/StatementDeserializer.php @@ -8,6 +8,7 @@ use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\InvalidFieldException; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\InvalidFieldTypeException; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\MissingFieldException; +use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\PropertyNotFoundException; /** * @license GPL-2.0-or-later @@ -29,6 +30,7 @@ public function __construct( * @throws InvalidFieldTypeException * @throws InvalidFieldException * @throws MissingFieldException + * @throws PropertyNotFoundException */ public function deserialize( array $serialization, string $basePath = '' ): Statement { if ( count( $serialization ) && array_is_list( $serialization ) ) { diff --git a/repo/rest-api/src/Application/UseCaseRequestValidation/ItemSerializationRequestValidatingDeserializer.php b/repo/rest-api/src/Application/UseCaseRequestValidation/ItemSerializationRequestValidatingDeserializer.php index 7658669d698..6b58b857a13 100644 --- a/repo/rest-api/src/Application/UseCaseRequestValidation/ItemSerializationRequestValidatingDeserializer.php +++ b/repo/rest-api/src/Application/UseCaseRequestValidation/ItemSerializationRequestValidatingDeserializer.php @@ -157,6 +157,7 @@ private function handleStatementsValidationErrors( ValidationError $validationEr throw UseCaseError::newInvalidValue( $context[StatementsValidator::CONTEXT_PATH] ); case StatementValidator::CODE_INVALID_FIELD: throw UseCaseError::newInvalidValue( $context[StatementValidator::CONTEXT_PATH] ); + case StatementValidator::CODE_PROPERTY_NOT_FOUND: case StatementValidator::CODE_INVALID_FIELD_TYPE: throw UseCaseError::newInvalidValue( $context[StatementValidator::CONTEXT_PATH] ); case StatementValidator::CODE_MISSING_FIELD: diff --git a/repo/rest-api/src/Application/UseCaseRequestValidation/StatementSerializationRequestValidatingDeserializer.php b/repo/rest-api/src/Application/UseCaseRequestValidation/StatementSerializationRequestValidatingDeserializer.php index cac33eee14e..4333a5fc381 100644 --- a/repo/rest-api/src/Application/UseCaseRequestValidation/StatementSerializationRequestValidatingDeserializer.php +++ b/repo/rest-api/src/Application/UseCaseRequestValidation/StatementSerializationRequestValidatingDeserializer.php @@ -26,6 +26,7 @@ public function validateAndDeserialize( StatementSerializationRequest $request ) if ( $validationError ) { $context = $validationError->getContext(); switch ( $validationError->getCode() ) { + case StatementValidator::CODE_PROPERTY_NOT_FOUND: case StatementValidator::CODE_INVALID_FIELD: throw UseCaseError::newInvalidValue( $context[StatementValidator::CONTEXT_PATH] ); case StatementValidator::CODE_MISSING_FIELD: diff --git a/repo/rest-api/src/Application/UseCases/PatchItem/PatchedItemValidator.php b/repo/rest-api/src/Application/UseCases/PatchItem/PatchedItemValidator.php index 1f19fec09bb..57e0f6e9e2f 100644 --- a/repo/rest-api/src/Application/UseCases/PatchItem/PatchedItemValidator.php +++ b/repo/rest-api/src/Application/UseCases/PatchItem/PatchedItemValidator.php @@ -395,34 +395,24 @@ private function assertValidStatements( array $serialization, Item $originalItem $context[StatementValidator::CONTEXT_PATH], $context[StatementValidator::CONTEXT_FIELD] ); - } - } - - // get StatementIds for all Statements in a StatementList, removing any that are null - $getStatementIds = fn( StatementList $statementList ) => array_filter( array_map( - fn( Statement $statement ) => $statement->getGuid(), - iterator_to_array( $statementList ) - ) ); + case StatementValidator::CODE_PROPERTY_NOT_FOUND: + throw UseCaseError::newPatchResultReferencedResourceNotFound( + $context[StatementValidator::CONTEXT_PATH], + $context[StatementValidator::CONTEXT_VALUE] + ); - $getStatementIdPath = function( array $serialization, string $id ): string { - foreach ( $serialization as $propertyId => $statementGroup ) { - foreach ( $statementGroup as $groupIndex => $statement ) { - if ( isset( $statement['id'] ) && $statement['id'] === $id ) { - return "/statements/$propertyId/$groupIndex"; - } - } + default: + throw new LogicException( "Unknown validation error code: {$validationError->getCode()}" ); } - - throw new LogicException( "Statement ID '$id' not found in patch result" ); - }; + } $originalStatements = $originalItem->getStatements(); - $originalStatementsIds = $getStatementIds( $originalStatements ); + $originalStatementsIds = $this->getStatementIds( $originalStatements ); $patchedStatements = $this->statementsValidator->getValidatedStatements(); - $patchedStatementsIds = $getStatementIds( $patchedStatements ); + $patchedStatementsIds = $this->getStatementIds( $patchedStatements ); foreach ( array_count_values( $patchedStatementsIds ) as $id => $occurrence ) { if ( $occurrence > 1 || !in_array( $id, $originalStatementsIds ) ) { - $path = "{$getStatementIdPath( $serialization['statements'], $id )}/id"; + $path = "{$this->getStatementIdPath( $serialization['statements'], $id )}/id"; throw UseCaseError::newPatchResultModifiedReadOnlyValue( $path ); } @@ -430,10 +420,29 @@ private function assertValidStatements( array $serialization, Item $originalItem if ( !$patchedStatements->getFirstStatementWithGuid( $id )->getPropertyId()->equals( $originalPropertyId ) ) { - $path = "{$getStatementIdPath( $serialization['statements'], $id )}/property/id"; + $path = "{$this->getStatementIdPath( $serialization['statements'], $id )}/property/id"; throw UseCaseError::newPatchResultModifiedReadOnlyValue( $path ); } } } + private function getStatementIds( StatementList $statementList ): array { + return array_filter( array_map( + fn( Statement $statement ) => $statement->getGuid(), + iterator_to_array( $statementList ) + ) ); + } + + private function getStatementIdPath( array $serialization, string $id ): string { + foreach ( $serialization as $propertyId => $statementGroup ) { + foreach ( $statementGroup as $groupIndex => $statement ) { + if ( isset( $statement['id'] ) && $statement['id'] === $id ) { + return "/statements/$propertyId/$groupIndex"; + } + } + } + + throw new LogicException( "Statement ID '$id' not found in patch result" ); + } + } diff --git a/repo/rest-api/src/Application/UseCases/PatchProperty/PatchedPropertyValidator.php b/repo/rest-api/src/Application/UseCases/PatchProperty/PatchedPropertyValidator.php index 347cbc3aa6d..cf2bb58abaa 100644 --- a/repo/rest-api/src/Application/UseCases/PatchProperty/PatchedPropertyValidator.php +++ b/repo/rest-api/src/Application/UseCases/PatchProperty/PatchedPropertyValidator.php @@ -303,6 +303,14 @@ private function assertValidStatements( UseCaseError::CONTEXT_STATEMENT_PROPERTY_ID => $context[ StatementsValidator::CONTEXT_PROPERTY_ID_VALUE ], ] ); + case StatementValidator::CODE_PROPERTY_NOT_FOUND: + throw UseCaseError::newPatchResultReferencedResourceNotFound( + $context[StatementValidator::CONTEXT_PATH], + $context[StatementValidator::CONTEXT_VALUE] + ); + + default: + throw new LogicException( "Unknown validation error code: {$validationError->getCode()}" ); } } @@ -316,31 +324,31 @@ private function assertValidStatements( $originalStatementsIds = $getStatementIds( $originalStatements ); $patchedStatementsIds = $getStatementIds( $patchedStatements ); - $getStatementIdPath = function( array $serialization, string $id ): string { - foreach ( $serialization as $propertyId => $statementGroup ) { - foreach ( $statementGroup as $groupIndex => $statement ) { - if ( isset( $statement['id'] ) && $statement['id'] === $id ) { - return "/statements/$propertyId/$groupIndex"; - } - } - } - - throw new LogicException( "Statement ID '$id' not found in patch result" ); - }; - foreach ( array_count_values( $patchedStatementsIds ) as $id => $occurrence ) { if ( $occurrence > 1 || !in_array( $id, $originalStatementsIds ) ) { - $path = "{$getStatementIdPath( $statementsSerialization, $id )}/id"; + $path = "{$this->getStatementIdPath( $statementsSerialization, $id )}/id"; throw UseCaseError::newPatchResultModifiedReadOnlyValue( $path ); } $originalPropertyId = $originalStatements->getFirstStatementWithGuid( $id )->getPropertyId(); if ( !$patchedStatements->getFirstStatementWithGuid( $id )->getPropertyId()->equals( $originalPropertyId ) ) { - $path = "{$getStatementIdPath( $statementsSerialization, $id )}/property/id"; + $path = "{$this->getStatementIdPath( $statementsSerialization, $id )}/property/id"; throw UseCaseError::newPatchResultModifiedReadOnlyValue( $path ); } } } + private function getStatementIdPath( array $serialization, string $id ): string { + foreach ( $serialization as $propertyId => $statementGroup ) { + foreach ( $statementGroup as $groupIndex => $statement ) { + if ( isset( $statement['id'] ) && $statement['id'] === $id ) { + return "/statements/$propertyId/$groupIndex"; + } + } + } + + throw new LogicException( "Statement ID '$id' not found in patch result" ); + } + } diff --git a/repo/rest-api/src/Application/UseCases/PatchStatement/PatchedStatementValidator.php b/repo/rest-api/src/Application/UseCases/PatchStatement/PatchedStatementValidator.php index 8bb17a0e4f5..474be40da9e 100644 --- a/repo/rest-api/src/Application/UseCases/PatchStatement/PatchedStatementValidator.php +++ b/repo/rest-api/src/Application/UseCases/PatchStatement/PatchedStatementValidator.php @@ -43,6 +43,12 @@ public function validateAndDeserializeStatement( $patchedStatement ): Statement $context[StatementValidator::CONTEXT_PATH], $context[StatementValidator::CONTEXT_VALUE] ); + case StatementValidator::CODE_PROPERTY_NOT_FOUND: + throw UseCaseError::newPatchResultReferencedResourceNotFound( + $context[StatementValidator::CONTEXT_PATH], + $context[StatementValidator::CONTEXT_VALUE] + ); + default: throw new LogicException( "Unexpected validation error code: {$validationError->getCode()}" ); } diff --git a/repo/rest-api/src/Application/Validation/StatementValidator.php b/repo/rest-api/src/Application/Validation/StatementValidator.php index 685242e6df0..6b60d65a6cf 100644 --- a/repo/rest-api/src/Application/Validation/StatementValidator.php +++ b/repo/rest-api/src/Application/Validation/StatementValidator.php @@ -7,6 +7,7 @@ use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\InvalidFieldException; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\InvalidFieldTypeException; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\MissingFieldException; +use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\PropertyNotFoundException; use Wikibase\Repo\RestApi\Application\Serialization\StatementDeserializer; /** @@ -17,6 +18,7 @@ class StatementValidator { public const CODE_INVALID_FIELD = 'statement-validator-code-invalid-statement-field'; public const CODE_MISSING_FIELD = 'statement-validator-code-missing-statement-field'; public const CODE_INVALID_FIELD_TYPE = 'statement-validator-code-invalid-statement-type'; + public const CODE_PROPERTY_NOT_FOUND = 'statement-validator-code-property-not-found'; public const CONTEXT_FIELD = 'statement-validator-context-field'; public const CONTEXT_PATH = 'statement-validator-context-path'; @@ -55,6 +57,14 @@ public function validate( array $statementSerialization, string $basePath = '' ) self::CONTEXT_VALUE => $e->getValue(), ] ); + } catch ( PropertyNotFoundException $e ) { + return new ValidationError( + self::CODE_PROPERTY_NOT_FOUND, + [ + self::CONTEXT_PATH => $e->getPath(), + self::CONTEXT_VALUE => $e->getValue(), + ] + ); } return null; diff --git a/repo/rest-api/src/RouteHandlers/openapi.json b/repo/rest-api/src/RouteHandlers/openapi.json index f161fb0a0e6..4587af56e77 100644 --- a/repo/rest-api/src/RouteHandlers/openapi.json +++ b/repo/rest-api/src/RouteHandlers/openapi.json @@ -5002,6 +5002,9 @@ "patch-result-invalid-value": { "$ref": "#/components/examples/PatchResultInvalidValueExample" }, + "patch-result-referenced-resource-not-found": { + "$ref": "#/components/examples/PatchResultResourceNotFoundExample" + }, "patch-result-value-too-long": { "$ref": "#/components/examples/PatchResultValueTooLongExample" }, @@ -5158,6 +5161,9 @@ }, "patch-result-modified-read-only-value": { "$ref": "#/components/examples/PatchResultModifiedReadOnlyValue" + }, + "patch-result-referenced-resource-not-found": { + "$ref": "#/components/examples/PatchResultResourceNotFoundExample" } } } diff --git a/repo/rest-api/tests/mocha/api-testing/PatchItemStatementTest.js b/repo/rest-api/tests/mocha/api-testing/PatchItemStatementTest.js index 1caffec92f2..bdb44309789 100644 --- a/repo/rest-api/tests/mocha/api-testing/PatchItemStatementTest.js +++ b/repo/rest-api/tests/mocha/api-testing/PatchItemStatementTest.js @@ -319,6 +319,44 @@ describe( 'PATCH statement tests', () => { } ); } ); + it( 'rejects qualifier with non-existent property', async () => { + const nonExistentProperty = 'P9999999'; + const patch = [ { + op: 'add', + path: '/qualifiers', + value: [ { property: { id: nonExistentProperty }, value: { type: 'novalue' } } ] + } ]; + const response = await newPatchRequestBuilder( testStatementId, patch ) + .assertValidRequest().makeRequest(); + + assertValidError( + response, + 422, + 'patch-result-referenced-resource-not-found', + { path: '/qualifiers/0/property/id', value: nonExistentProperty } + ); + assert.strictEqual( response.body.message, 'The referenced resource does not exist' ); + } ); + + it( 'rejects reference with non-existent property', async () => { + const nonExistentProperty = 'P9999999'; + const patch = [ { + op: 'add', + path: '/references/0', + value: { parts: [ { property: { id: nonExistentProperty }, value: { type: 'novalue' } } ] } + } ]; + const response = await newPatchRequestBuilder( testStatementId, patch ) + .assertValidRequest().makeRequest(); + + assertValidError( + response, + 422, + 'patch-result-referenced-resource-not-found', + { path: '/references/0/parts/0/property/id', value: nonExistentProperty } + ); + assert.strictEqual( response.body.message, 'The referenced resource does not exist' ); + } ); + } ); } ); diff --git a/repo/rest-api/tests/mocha/api-testing/PatchItemTest.js b/repo/rest-api/tests/mocha/api-testing/PatchItemTest.js index 87aa954f3a3..485662eaead 100644 --- a/repo/rest-api/tests/mocha/api-testing/PatchItemTest.js +++ b/repo/rest-api/tests/mocha/api-testing/PatchItemTest.js @@ -838,6 +838,74 @@ describe( newPatchItemRequestBuilder().getRouteDescription(), () => { assertValidError( response, 422, 'url-not-modifiable', { site_id: siteId } ); assert.equal( response.body.message, 'URL of sitelink cannot be modified' ); } ); + + it( 'rejects statement with non-existent property', async () => { + const nonExistentProperty = 'P9999999'; + const patch = [ { + op: 'add', + path: `/statements/${nonExistentProperty}`, + value: [ { property: { id: nonExistentProperty }, value: { type: 'novalue' } } ] + } ]; + const response = await newPatchItemRequestBuilder( testItemId, patch ) + .assertValidRequest().makeRequest(); + + assertValidError( + response, + 422, + 'patch-result-referenced-resource-not-found', + { path: `/statements/${nonExistentProperty}/0/property/id`, value: nonExistentProperty } + ); + assert.strictEqual( response.body.message, 'The referenced resource does not exist' ); + } ); + + it( 'rejects qualifier with non-existent property', async () => { + await newAddItemStatementRequestBuilder( testItemId, { + property: { id: predicatePropertyId }, + value: { type: 'novalue' } + } ).makeRequest(); + + const nonExistentProperty = 'P9999999'; + const patch = [ { + op: 'add', + path: `/statements/${predicatePropertyId}/0/qualifiers`, + value: [ { property: { id: nonExistentProperty }, value: { type: 'novalue' } } ] + } ]; + const response = await newPatchItemRequestBuilder( testItemId, patch ) + .assertValidRequest().makeRequest(); + + assertValidError( + response, + 422, + 'patch-result-referenced-resource-not-found', + { path: `/statements/${predicatePropertyId}/0/qualifiers/0/property/id`, value: nonExistentProperty } + ); + assert.strictEqual( response.body.message, 'The referenced resource does not exist' ); + } ); + + it( 'rejects reference with non-existent property', async () => { + await newAddItemStatementRequestBuilder( testItemId, { + property: { id: predicatePropertyId }, + value: { type: 'novalue' } + } ).makeRequest(); + + const nonExistentProperty = 'P9999999'; + const patch = [ { + op: 'add', + path: `/statements/${predicatePropertyId}/0/references/0`, + value: { parts: [ { property: { id: nonExistentProperty }, value: { type: 'novalue' } } ] } + } ]; + const response = await newPatchItemRequestBuilder( testItemId, patch ) + .assertValidRequest().makeRequest(); + + assertValidError( + response, + 422, + 'patch-result-referenced-resource-not-found', + { path: `/statements/${predicatePropertyId}/0/references/0/parts/0/property/id`, value: nonExistentProperty } + ); + assert.strictEqual( response.body.message, 'The referenced resource does not exist' ); + } ); + } ); } ); diff --git a/repo/rest-api/tests/mocha/api-testing/PatchPropertyStatementTest.js b/repo/rest-api/tests/mocha/api-testing/PatchPropertyStatementTest.js index bf749dce8ee..0e176f8908e 100644 --- a/repo/rest-api/tests/mocha/api-testing/PatchPropertyStatementTest.js +++ b/repo/rest-api/tests/mocha/api-testing/PatchPropertyStatementTest.js @@ -320,6 +320,45 @@ describe( 'PATCH property statement', () => { assert.strictEqual( response.body.message, 'Read only value in patch result cannot be modified' ); } ); } ); + + it( 'rejects qualifier with non-existent property', async () => { + const nonExistentProperty = 'P9999999'; + const patch = [ { + op: 'add', + path: '/qualifiers', + value: [ { property: { id: nonExistentProperty }, value: { type: 'novalue' } } ] + } ]; + const response = await newPatchRequestBuilder( testStatementId, patch ) + .assertValidRequest().makeRequest(); + + assertValidError( + response, + 422, + 'patch-result-referenced-resource-not-found', + { path: '/qualifiers/0/property/id', value: nonExistentProperty } + ); + assert.strictEqual( response.body.message, 'The referenced resource does not exist' ); + } ); + + it( 'rejects reference with non-existent property', async () => { + const nonExistentProperty = 'P9999999'; + const patch = [ { + op: 'add', + path: '/references/0', + value: { parts: [ { property: { id: nonExistentProperty }, value: { type: 'novalue' } } ] } + } ]; + const response = await newPatchRequestBuilder( testStatementId, patch ) + .assertValidRequest().makeRequest(); + + assertValidError( + response, + 422, + 'patch-result-referenced-resource-not-found', + { path: '/references/0/parts/0/property/id', value: nonExistentProperty } + ); + assert.strictEqual( response.body.message, 'The referenced resource does not exist' ); + } ); + } ); } ); diff --git a/repo/rest-api/tests/mocha/api-testing/PatchPropertyTest.js b/repo/rest-api/tests/mocha/api-testing/PatchPropertyTest.js index cd1f0d2f380..c8eea0ab2cc 100644 --- a/repo/rest-api/tests/mocha/api-testing/PatchPropertyTest.js +++ b/repo/rest-api/tests/mocha/api-testing/PatchPropertyTest.js @@ -694,5 +694,73 @@ describe( newPatchPropertyRequestBuilder().getRouteDescription(), () => { assertValidError( response, 422, 'patch-result-modified-read-only-value', context ); assert.strictEqual( response.body.message, 'Read only value in patch result cannot be modified' ); } ); + + it( 'rejects statement with non-existent property', async () => { + const nonExistentProperty = 'P9999999'; + const patch = [ { + op: 'add', + path: `/statements/${nonExistentProperty}`, + value: [ { property: { id: nonExistentProperty }, value: { type: 'novalue' } } ] + } ]; + const response = await newPatchPropertyRequestBuilder( testPropertyId, patch ) + .assertValidRequest().makeRequest(); + + assertValidError( + response, + 422, + 'patch-result-referenced-resource-not-found', + { path: `/statements/${nonExistentProperty}/0/property/id`, value: nonExistentProperty } + ); + assert.strictEqual( response.body.message, 'The referenced resource does not exist' ); + } ); + + it( 'rejects qualifier with non-existent property', async () => { + await newAddPropertyStatementRequestBuilder( testPropertyId, { + property: { id: predicatePropertyId }, + value: { type: 'novalue' } + } ).makeRequest(); + + const nonExistentProperty = 'P9999999'; + const patch = [ { + op: 'add', + path: `/statements/${predicatePropertyId}/0/qualifiers`, + value: [ { property: { id: nonExistentProperty }, value: { type: 'novalue' } } ] + } ]; + const response = await newPatchPropertyRequestBuilder( testPropertyId, patch ) + .assertValidRequest().makeRequest(); + + assertValidError( + response, + 422, + 'patch-result-referenced-resource-not-found', + { path: `/statements/${predicatePropertyId}/0/qualifiers/0/property/id`, value: nonExistentProperty } + ); + assert.strictEqual( response.body.message, 'The referenced resource does not exist' ); + } ); + + it( 'rejects reference with non-existent property', async () => { + await newAddPropertyStatementRequestBuilder( testPropertyId, { + property: { id: predicatePropertyId }, + value: { type: 'novalue' } + } ).makeRequest(); + + const nonExistentProperty = 'P9999999'; + const patch = [ { + op: 'add', + path: `/statements/${predicatePropertyId}/0/references/0`, + value: { parts: [ { property: { id: nonExistentProperty }, value: { type: 'novalue' } } ] } + } ]; + const response = await newPatchPropertyRequestBuilder( testPropertyId, patch ) + .assertValidRequest().makeRequest(); + + assertValidError( + response, + 422, + 'patch-result-referenced-resource-not-found', + { path: `/statements/${predicatePropertyId}/0/references/0/parts/0/property/id`, value: nonExistentProperty } + ); + assert.strictEqual( response.body.message, 'The referenced resource does not exist' ); + } ); + } ); } ); diff --git a/repo/rest-api/tests/phpunit/Application/Serialization/PropertyValuePairDeserializerTest.php b/repo/rest-api/tests/phpunit/Application/Serialization/PropertyValuePairDeserializerTest.php index 16e74f594ca..2de3b395033 100644 --- a/repo/rest-api/tests/phpunit/Application/Serialization/PropertyValuePairDeserializerTest.php +++ b/repo/rest-api/tests/phpunit/Application/Serialization/PropertyValuePairDeserializerTest.php @@ -15,6 +15,7 @@ use Wikibase\DataModel\Snak\Snak; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\InvalidFieldException; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\MissingFieldException; +use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\PropertyNotFoundException; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\SerializationException; use Wikibase\Repo\RestApi\Application\Serialization\PropertyValuePairDeserializer; use Wikibase\Repo\Tests\RestApi\Helpers\TestPropertyValuePairDeserializerFactory; @@ -184,15 +185,6 @@ public static function invalidSerializationProvider(): Generator { '/statements/P789/2/qualifiers/1', ]; - yield "invalid 'property/id' field - property does not exist" => [ - new InvalidFieldException( 'id', 'P666', '/statement/references/3/parts/1/property/id' ), - [ - 'property' => [ 'id' => 'P666' ], - 'value' => [ 'type' => 'novalue' ], - ], - '/statement/references/3/parts/1', - ]; - yield "invalid 'value' field - int" => [ new InvalidFieldException( 'value', 42, '/statements/P789/0/value' ), [ @@ -267,6 +259,15 @@ public static function invalidSerializationProvider(): Generator { ], '/statement', ]; + + yield 'referenced property does not exist' => [ + new PropertyNotFoundException( 'P666', '/statement/references/3/parts/1/property/id' ), + [ + 'property' => [ 'id' => 'P666' ], + 'value' => [ 'type' => 'novalue' ], + ], + '/statement/references/3/parts/1', + ]; } private function newDeserializer(): PropertyValuePairDeserializer { diff --git a/repo/rest-api/tests/phpunit/Application/Serialization/ReferenceDeserializerTest.php b/repo/rest-api/tests/phpunit/Application/Serialization/ReferenceDeserializerTest.php index 326a1b16d38..be614d5a3ff 100644 --- a/repo/rest-api/tests/phpunit/Application/Serialization/ReferenceDeserializerTest.php +++ b/repo/rest-api/tests/phpunit/Application/Serialization/ReferenceDeserializerTest.php @@ -13,6 +13,7 @@ use Wikibase\DataModel\Snak\PropertySomeValueSnak; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\InvalidFieldException; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\MissingFieldException; +use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\PropertyNotFoundException; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\SerializationException; use Wikibase\Repo\RestApi\Application\Serialization\PropertyValuePairDeserializer; use Wikibase\Repo\RestApi\Application\Serialization\ReferenceDeserializer; @@ -127,6 +128,12 @@ public static function invalidSerializationProvider(): Generator { [ 'parts' => [ [ 'not', 'an', 'associative', 'array' ] ] ], '/statement/references/5', ]; + + yield 'property does not exist' => [ + new PropertyNotFoundException( 'P9999999', '/statement/references/0/parts/0/property/id' ), + [ 'parts' => [ [ 'property' => [ 'id' => 'P9999999' ], 'value' => [ 'type' => 'somevalue' ] ] ] ], + '/statement/references/0', + ]; } private function newDeserializer(): ReferenceDeserializer { diff --git a/repo/rest-api/tests/phpunit/Application/Serialization/StatementDeserializerTest.php b/repo/rest-api/tests/phpunit/Application/Serialization/StatementDeserializerTest.php index 6b5ce242e47..7877c3c0375 100644 --- a/repo/rest-api/tests/phpunit/Application/Serialization/StatementDeserializerTest.php +++ b/repo/rest-api/tests/phpunit/Application/Serialization/StatementDeserializerTest.php @@ -13,6 +13,7 @@ use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\InvalidFieldException; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\InvalidFieldTypeException; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\MissingFieldException; +use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\PropertyNotFoundException; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\SerializationException; use Wikibase\Repo\RestApi\Application\Serialization\ReferenceDeserializer; use Wikibase\Repo\RestApi\Application\Serialization\StatementDeserializer; @@ -248,6 +249,15 @@ public static function invalidSerializationProvider(): Generator { [ 'property' => [ 'id' => self::EXISTING_STRING_PROPERTY_IDS[0] ], 'value' => [ 'type' => 'value' ] ], '/statement', ]; + + yield 'property does not exist' => [ + new PropertyNotFoundException( 'P9999999', '/statement/property/id' ), + [ + 'property' => [ 'id' => 'P9999999' ], + 'value' => [ 'type' => 'somevalue' ], + ], + '/statement', + ]; } private function newDeserializer(): StatementDeserializer { diff --git a/repo/rest-api/tests/phpunit/Application/UseCaseRequestValidation/ItemSerializationRequestValidatingDeserializerTest.php b/repo/rest-api/tests/phpunit/Application/UseCaseRequestValidation/ItemSerializationRequestValidatingDeserializerTest.php index 0304c9aa690..9589c874a40 100644 --- a/repo/rest-api/tests/phpunit/Application/UseCaseRequestValidation/ItemSerializationRequestValidatingDeserializerTest.php +++ b/repo/rest-api/tests/phpunit/Application/UseCaseRequestValidation/ItemSerializationRequestValidatingDeserializerTest.php @@ -482,6 +482,17 @@ public function itemStatementsValidationErrorProvider(): Generator { ), UseCaseError::newInvalidValue( '/item/statements/P1/0/value' ), ]; + + yield 'property does not exist' => [ + new ValidationError( + StatementValidator::CODE_PROPERTY_NOT_FOUND, + [ + StatementValidator::CONTEXT_PATH => '/some-path-to-non-existing-property', + StatementValidator::CONTEXT_VALUE => 'non-existing-property-id', + ] + ), + UseCaseError::newInvalidValue( '/some-path-to-non-existing-property' ), + ]; } public static function sitelinksValidationErrorProvider(): Generator { diff --git a/repo/rest-api/tests/phpunit/Application/UseCaseRequestValidation/StatementSerializationRequestValidatingDeserializerTest.php b/repo/rest-api/tests/phpunit/Application/UseCaseRequestValidation/StatementSerializationRequestValidatingDeserializerTest.php index 9eb64b9b302..12b2ef4d14b 100644 --- a/repo/rest-api/tests/phpunit/Application/UseCaseRequestValidation/StatementSerializationRequestValidatingDeserializerTest.php +++ b/repo/rest-api/tests/phpunit/Application/UseCaseRequestValidation/StatementSerializationRequestValidatingDeserializerTest.php @@ -258,6 +258,11 @@ public function provideInvalidStatementSerialization(): Generator { ], UseCaseError::newMissingField( '/statement/references/1', 'parts' ), ]; + + yield 'property does not exist' => [ + [ 'property' => [ 'id' => 'P9999999' ], 'value' => [ 'type' => 'novalue' ] ], + UseCaseError::newInvalidValue( '/statement/property/id' ), + ]; } private function newStatementSerializationRVD(): StatementSerializationRequestValidatingDeserializer { diff --git a/repo/rest-api/tests/phpunit/Application/UseCases/PatchItem/PatchedItemValidatorTest.php b/repo/rest-api/tests/phpunit/Application/UseCases/PatchItem/PatchedItemValidatorTest.php index c5aa98ac0bf..547f4823dae 100644 --- a/repo/rest-api/tests/phpunit/Application/UseCases/PatchItem/PatchedItemValidatorTest.php +++ b/repo/rest-api/tests/phpunit/Application/UseCases/PatchItem/PatchedItemValidatorTest.php @@ -776,6 +776,19 @@ public function providePatchInvalidStatements(): Generator { ] ), ]; + + $nonExistingPropertyId = 'P9999999'; + yield 'non-existing property' => [ + [ $nonExistingPropertyId => [ [ 'property' => [ 'id' => $nonExistingPropertyId ], 'value' => [ 'type' => 'somevalue' ] ] ] ], + new UseCaseError( + UseCaseError::PATCH_RESULT_REFERENCED_RESOURCE_NOT_FOUND, + 'The referenced resource does not exist', + [ + UseCaseError::CONTEXT_PATH => "/statements/$nonExistingPropertyId/0/property/id", + UseCaseError::CONTEXT_VALUE => $nonExistingPropertyId, + ] + ), + ]; } /** diff --git a/repo/rest-api/tests/phpunit/Application/UseCases/PatchProperty/PatchedPropertyValidatorTest.php b/repo/rest-api/tests/phpunit/Application/UseCases/PatchProperty/PatchedPropertyValidatorTest.php index d219e65cce3..7e65c609c4d 100644 --- a/repo/rest-api/tests/phpunit/Application/UseCases/PatchProperty/PatchedPropertyValidatorTest.php +++ b/repo/rest-api/tests/phpunit/Application/UseCases/PatchProperty/PatchedPropertyValidatorTest.php @@ -783,6 +783,19 @@ public function statementsProvider(): Generator { [ $propertyId => [ $statementWithExistingId ] ], UseCaseError::newPatchResultModifiedReadOnlyValue( "/statements/$propertyId/0/property/id" ), ]; + + $nonExistingPropertyId = 'P9999999'; + yield 'non-existing property' => [ + [ $nonExistingPropertyId => [ [ 'property' => [ 'id' => $nonExistingPropertyId ], 'value' => [ 'type' => 'somevalue' ] ] ] ], + new UseCaseError( + UseCaseError::PATCH_RESULT_REFERENCED_RESOURCE_NOT_FOUND, + 'The referenced resource does not exist', + [ + UseCaseError::CONTEXT_PATH => "/statements/$nonExistingPropertyId/0/property/id", + UseCaseError::CONTEXT_VALUE => $nonExistingPropertyId, + ] + ), + ]; } private function newValidator(): PatchedPropertyValidator { diff --git a/repo/rest-api/tests/phpunit/Application/UseCases/PatchStatement/PatchedStatementValidatorTest.php b/repo/rest-api/tests/phpunit/Application/UseCases/PatchStatement/PatchedStatementValidatorTest.php index bec31da47a7..14be3a737ba 100644 --- a/repo/rest-api/tests/phpunit/Application/UseCases/PatchStatement/PatchedStatementValidatorTest.php +++ b/repo/rest-api/tests/phpunit/Application/UseCases/PatchStatement/PatchedStatementValidatorTest.php @@ -118,6 +118,19 @@ public static function invalidPatchedStatementProvider(): Generator { UseCaseError::CONTEXT_VALUE => 'not-a-valid-rank', ], ]; + + yield 'from invalid patched statement (property not found)' => [ + new ValidationError( StatementValidator::CODE_PROPERTY_NOT_FOUND, [ + StatementValidator::CONTEXT_PATH => '/some-path-to-property-id', + StatementValidator::CONTEXT_VALUE => 'P9999999', + ] ), + UseCaseError::PATCH_RESULT_REFERENCED_RESOURCE_NOT_FOUND, + 'The referenced resource does not exist', + [ + UseCaseError::CONTEXT_PATH => '/some-path-to-property-id', + UseCaseError::CONTEXT_VALUE => 'P9999999', + ], + ]; } } diff --git a/repo/rest-api/tests/phpunit/Application/Validation/StatementValidatorTest.php b/repo/rest-api/tests/phpunit/Application/Validation/StatementValidatorTest.php index 8289df4dc8b..bc4fd40024e 100644 --- a/repo/rest-api/tests/phpunit/Application/Validation/StatementValidatorTest.php +++ b/repo/rest-api/tests/phpunit/Application/Validation/StatementValidatorTest.php @@ -9,6 +9,7 @@ use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\InvalidFieldException; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\InvalidFieldTypeException; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\MissingFieldException; +use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\PropertyNotFoundException; use Wikibase\Repo\RestApi\Application\Serialization\Exceptions\SerializationException; use Wikibase\Repo\RestApi\Application\Serialization\StatementDeserializer; use Wikibase\Repo\RestApi\Application\Validation\StatementValidator; @@ -73,6 +74,15 @@ public static function deserializationErrorProvider(): Generator { StatementValidator::CONTEXT_VALUE => 'some-value', ], ]; + + yield 'non-existent property' => [ + new PropertyNotFoundException( 'P9999999', '/path/to/non-existing-property' ), + StatementValidator::CODE_PROPERTY_NOT_FOUND, + [ + StatementValidator::CONTEXT_PATH => '/path/to/non-existing-property', + StatementValidator::CONTEXT_VALUE => 'P9999999', + ], + ]; } public function testGetValidatedStatement_calledAfterValidate(): void {