diff --git a/repo/rest-api/src/Domain/Services/Exceptions/ItemUpdateFailed.php b/repo/rest-api/src/Domain/Services/Exceptions/EntityUpdateFailed.php similarity index 76% rename from repo/rest-api/src/Domain/Services/Exceptions/ItemUpdateFailed.php rename to repo/rest-api/src/Domain/Services/Exceptions/EntityUpdateFailed.php index 700f57e5455..a77ef316a0e 100644 --- a/repo/rest-api/src/Domain/Services/Exceptions/ItemUpdateFailed.php +++ b/repo/rest-api/src/Domain/Services/Exceptions/EntityUpdateFailed.php @@ -7,5 +7,5 @@ /** * @license GPL-2.0-or-later */ -class ItemUpdateFailed extends Exception { +class EntityUpdateFailed extends Exception { } diff --git a/repo/rest-api/src/Domain/Services/Exceptions/EntityUpdatePrevented.php b/repo/rest-api/src/Domain/Services/Exceptions/EntityUpdatePrevented.php new file mode 100644 index 00000000000..9723fd63acb --- /dev/null +++ b/repo/rest-api/src/Domain/Services/Exceptions/EntityUpdatePrevented.php @@ -0,0 +1,15 @@ +context = $context; $this->editEntityFactory = $editEntityFactory; $this->logger = $logger; $this->summaryFormatter = $summaryFormatter; $this->permissionManager = $permissionManager; - $this->statementReadModelConverter = $statementReadModelConverter; } - public function update( DataModelItem $item, EditMetadata $editMetadata ): ItemRevision { + public function update( EntityDocument $entity, EditMetadata $editMetadata ): EntityRevision { $this->checkBotRightIfProvided( $this->context->getUser(), $editMetadata->isBot() ); - $editEntity = $this->editEntityFactory->newEditEntity( $this->context, $item->getId() ); + $editEntity = $this->editEntityFactory->newEditEntity( $this->context, $entity->getId() ); $status = $editEntity->attemptSave( - $item, + $entity, $this->summaryFormatter->format( $editMetadata->getSummary() ), EDIT_UPDATE | ( $editMetadata->isBot() ? EDIT_FORCE_BOT : 0 ), false, @@ -66,25 +56,15 @@ public function update( DataModelItem $item, EditMetadata $editMetadata ): ItemR if ( !$status->isOK() ) { if ( $this->isPreventedEdit( $status ) ) { - throw new ItemUpdatePrevented( (string)$status ); + throw new EntityUpdatePrevented( (string)$status ); } - throw new ItemUpdateFailed( (string)$status ); + throw new EntityUpdateFailed( (string)$status ); } elseif ( !$status->isGood() ) { $this->logger->warning( (string)$status ); } - /** @var EntityRevision $entityRevision */ - $entityRevision = $status->getValue()['revision']; - /** @var DataModelItem $savedItem */ - $savedItem = $entityRevision->getEntity(); - '@phan-var DataModelItem $savedItem'; - - return new ItemRevision( - $this->convertDataModelItemToReadModel( $savedItem ), - $entityRevision->getTimestamp(), - $entityRevision->getRevisionId() - ); + return $status->getValue()['revision']; } private function isPreventedEdit( \Status $status ): bool { @@ -103,17 +83,4 @@ private function checkBotRightIfProvided( User $user, bool $isBot ): void { } } - private function convertDataModelItemToReadModel( DataModelItem $item ): Item { - return new Item( - Labels::fromTermList( $item->getLabels() ), - Descriptions::fromTermList( $item->getDescriptions() ), - new StatementList( - ...array_map( - [ $this->statementReadModelConverter, 'convert' ], - iterator_to_array( $item->getStatements() ) - ) - ) - ); - } - } diff --git a/repo/rest-api/src/Infrastructure/DataAccess/EntityUpdaterItemUpdater.php b/repo/rest-api/src/Infrastructure/DataAccess/EntityUpdaterItemUpdater.php new file mode 100644 index 00000000000..77f48bcc0b3 --- /dev/null +++ b/repo/rest-api/src/Infrastructure/DataAccess/EntityUpdaterItemUpdater.php @@ -0,0 +1,55 @@ +entityUpdater = $entityUpdater; + $this->statementReadModelConverter = $statementReadModelConverter; + } + + public function update( DataModelItem $item, EditMetadata $editMetadata ): ItemRevision { + $entityRevision = $this->entityUpdater->update( $item, $editMetadata ); + + /** @var DataModelItem $savedItem */ + $savedItem = $entityRevision->getEntity(); + '@phan-var DataModelItem $savedItem'; + + return new ItemRevision( + $this->convertDataModelItemToReadModel( $savedItem ), + $entityRevision->getTimestamp(), + $entityRevision->getRevisionId() + ); + } + + private function convertDataModelItemToReadModel( DataModelItem $item ): Item { + return new Item( + Labels::fromTermList( $item->getLabels() ), + Descriptions::fromTermList( $item->getDescriptions() ), + new StatementList( + ...array_map( + [ $this->statementReadModelConverter, 'convert' ], + iterator_to_array( $item->getStatements() ) + ) + ) + ); + } + +} diff --git a/repo/rest-api/src/RouteHandlers/Middleware/UnexpectedErrorHandlerMiddleware.php b/repo/rest-api/src/RouteHandlers/Middleware/UnexpectedErrorHandlerMiddleware.php index 511113c89b7..c91fbe85ae2 100644 --- a/repo/rest-api/src/RouteHandlers/Middleware/UnexpectedErrorHandlerMiddleware.php +++ b/repo/rest-api/src/RouteHandlers/Middleware/UnexpectedErrorHandlerMiddleware.php @@ -8,7 +8,7 @@ use Psr\Log\LoggerInterface; use Throwable; use Wikibase\Repo\RestApi\Application\UseCases\UseCaseError; -use Wikibase\Repo\RestApi\Domain\Services\Exceptions\ItemUpdatePrevented; +use Wikibase\Repo\RestApi\Domain\Services\Exceptions\EntityUpdatePrevented; use Wikibase\Repo\RestApi\RouteHandlers\ResponseFactory; /** @@ -33,7 +33,7 @@ public function __construct( public function run( Handler $routeHandler, callable $runNext ): Response { try { return $runNext(); - } catch ( ItemUpdatePrevented $exception ) { // temporary fix for T329233 + } catch ( EntityUpdatePrevented $exception ) { // temporary fix for T329233 $this->logger->warning( $exception->getMessage(), [ 'exception' => $exception ] ); } catch ( Throwable $exception ) { $this->errorReporter->reportError( $exception, $routeHandler, $routeHandler->getRequest() ); diff --git a/repo/rest-api/src/WbRestApi.ServiceWiring.php b/repo/rest-api/src/WbRestApi.ServiceWiring.php index 3472c633cae..393e4931df8 100644 --- a/repo/rest-api/src/WbRestApi.ServiceWiring.php +++ b/repo/rest-api/src/WbRestApi.ServiceWiring.php @@ -67,7 +67,8 @@ use Wikibase\Repo\RestApi\Infrastructure\DataAccess\EntityRevisionLookupItemDataRetriever; use Wikibase\Repo\RestApi\Infrastructure\DataAccess\EntityRevisionLookupPropertyDataRetriever; use Wikibase\Repo\RestApi\Infrastructure\DataAccess\EntityRevisionLookupStatementRetriever; -use Wikibase\Repo\RestApi\Infrastructure\DataAccess\MediaWikiEditEntityFactoryItemUpdater; +use Wikibase\Repo\RestApi\Infrastructure\DataAccess\EntityUpdater; +use Wikibase\Repo\RestApi\Infrastructure\DataAccess\EntityUpdaterItemUpdater; use Wikibase\Repo\RestApi\Infrastructure\DataAccess\PrefetchingTermLookupAliasesRetriever; use Wikibase\Repo\RestApi\Infrastructure\DataAccess\StatementSubjectRetriever; use Wikibase\Repo\RestApi\Infrastructure\DataAccess\TermLookupItemDataRetriever; @@ -277,15 +278,17 @@ }, 'WbRestApi.ItemUpdater' => function( MediaWikiServices $services ): ItemUpdater { - return new MediaWikiEditEntityFactoryItemUpdater( - RequestContext::getMain(), - WikibaseRepo::getEditEntityFactory( $services ), - WikibaseRepo::getLogger( $services ), - new EditSummaryFormatter( - WikibaseRepo::getSummaryFormatter( $services ), - new LabelsEditSummaryToFormattableSummaryConverter() + return new EntityUpdaterItemUpdater( + new EntityUpdater( + RequestContext::getMain(), + WikibaseRepo::getEditEntityFactory( $services ), + WikibaseRepo::getLogger( $services ), + new EditSummaryFormatter( + WikibaseRepo::getSummaryFormatter( $services ), + new LabelsEditSummaryToFormattableSummaryConverter() + ), + $services->getPermissionManager(), ), - $services->getPermissionManager(), new StatementReadModelConverter( WikibaseRepo::getStatementGuidParser( $services ), WikibaseRepo::getPropertyDataTypeLookup( $services ) diff --git a/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/EntityUpdaterIntegrationTest.php b/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/EntityUpdaterIntegrationTest.php new file mode 100644 index 00000000000..dabc199cd90 --- /dev/null +++ b/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/EntityUpdaterIntegrationTest.php @@ -0,0 +1,110 @@ +saveNewEntity( $entityToUpdate ); + + $entityToUpdate->getStatements()->removeStatementsWithGuid( (string)$statementId ); + + $newRevision = $this->newEntityUpdater()->update( $entityToUpdate, $this->createStub( EditMetadata::class ) ); + + $this->assertCount( 0, $newRevision->getEntity()->getStatements() ); + } + + /** + * @dataProvider provideStatementIdAndEntityWithStatement + */ + public function testUpdate_replaceStatementOnEntity( StatementGuid $statementId, StatementListProvidingEntity $entityToUpdate ): void { + $newValue = 'new statement value'; + $newStatement = NewStatement::forProperty( 'P321' ) + ->withGuid( $statementId ) + ->withValue( $newValue ) + ->build(); + + $this->saveNewEntity( $entityToUpdate ); + + $entityToUpdate->getStatements()->replaceStatement( $statementId, $newStatement ); + + $newRevision = $this->newEntityUpdater()->update( $entityToUpdate, $this->createStub( EditMetadata::class ) ); + + $statementList = $newRevision->getEntity()->getStatements(); + $this->assertSame( + $newValue, + $statementList->getFirstStatementWithGuid( (string)$statementId )->getMainSnak()->getDataValue()->getValue() + ); + } + + public function provideStatementIdAndEntityWithStatement(): Generator { + $statementId = new StatementGuid( new ItemId( 'Q123' ), 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE' ); + $statement = NewStatement::forProperty( 'P321' ) + ->withGuid( $statementId ) + ->withValue( 'a statement value' ) + ->build(); + yield 'item with statement' => [ $statementId, NewItem::withStatement( $statement )->build() ]; + + $statementId = new StatementGuid( new NumericPropertyId( 'P123' ), 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE' ); + $statement = NewStatement::forProperty( 'P321' ) + ->withGuid( $statementId ) + ->withValue( 'a statement value' ) + ->build(); + $property = Property::newFromType( 'string' ); + $property->setStatements( new StatementList( $statement ) ); + yield 'property with statement' => [ $statementId, $property ]; + } + + private function saveNewEntity( EntityDocument $entity ): void { + WikibaseRepo::getEntityStore()->saveEntity( + $entity, + __METHOD__, + $this->getTestUser()->getUser(), + EDIT_NEW + ); + } + + private function newEntityUpdater(): EntityUpdater { + $permissionManager = $this->createStub( PermissionManager::class ); + $permissionManager->method( $this->anything() )->willReturn( true ); + + return new EntityUpdater( + RequestContext::getMain(), + WikibaseRepo::getEditEntityFactory(), + new NullLogger(), + $this->createStub( EditSummaryFormatter::class ), + $permissionManager + ); + } + +} diff --git a/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/EntityUpdaterItemUpdaterTest.php b/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/EntityUpdaterItemUpdaterTest.php new file mode 100644 index 00000000000..a061e31e525 --- /dev/null +++ b/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/EntityUpdaterItemUpdaterTest.php @@ -0,0 +1,96 @@ +statementReadModelConverter = $this->newStatementReadModelConverter(); + $this->entityUpdater = $this->createStub( EntityUpdater::class ); + } + + public function testUpdate(): void { + $expectedRevisionId = 234; + $expectedRevisionTimestamp = '20221111070707'; + $editMetaData = new EditMetadata( [], true, $this->createStub( EditSummary::class ) ); + [ $itemToUpdate, $expectedResultingItem ] = $this->newEquivalentWriteAndReadModelItem(); + + $this->entityUpdater = $this->createMock( EntityUpdater::class ); + + $this->entityUpdater->expects( $this->once() ) + ->method( 'update' ) + ->with( $itemToUpdate, $editMetaData ) + ->willReturn( new EntityRevision( + $itemToUpdate, + $expectedRevisionId, + $expectedRevisionTimestamp + ) ); + + $itemRevision = $this->newItemUpdater()->update( + $itemToUpdate, + $editMetaData + ); + + $this->assertEquals( $expectedResultingItem, $itemRevision->getItem() ); + $this->assertSame( $expectedRevisionId, $itemRevision->getRevisionId() ); + $this->assertSame( $expectedRevisionTimestamp, $itemRevision->getLastModified() ); + } + + private function newEquivalentWriteAndReadModelItem(): array { + $writeModelStatement = NewStatement::someValueFor( 'P123' ) + ->withGuid( 'Q123$AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE' ) + ->build(); + $readModelStatement = NewStatementReadModel::someValueFor( 'P123' ) + ->withGuid( 'Q123$AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE' ) + ->build(); + + return [ + NewItem::withId( 'Q123' ) + ->andLabel( 'en', 'English Label' ) + ->andDescription( 'en', 'English Description' ) + ->andStatement( $writeModelStatement ) + ->build(), + new Item( + new Labels( new Label( 'en', 'English Label' ) ), + new Descriptions( new Description( 'en', 'English Description' ) ), + new StatementList( $readModelStatement ) + ), + ]; + } + + private function newItemUpdater(): EntityUpdaterItemUpdater { + return new EntityUpdaterItemUpdater( $this->entityUpdater, $this->statementReadModelConverter ); + } + +} diff --git a/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/EntityUpdaterTest.php b/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/EntityUpdaterTest.php new file mode 100644 index 00000000000..a8408c6bf1c --- /dev/null +++ b/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/EntityUpdaterTest.php @@ -0,0 +1,241 @@ +context = $this->createStub( IContextSource::class ); + $this->context->method( 'getUser' )->willReturn( $this->createStub( User::class ) ); + $this->editEntityFactory = $this->createStub( MediaWikiEditEntityFactory::class ); + $this->logger = $this->createStub( LoggerInterface::class ); + $this->summaryFormatter = $this->createStub( EditSummaryFormatter::class ); + $this->permissionManager = $this->createStub( PermissionManager::class ); + $this->permissionManager->method( 'userHasRight' )->willReturn( true ); + } + + /** + * @dataProvider provideEntityAndEditMetadata + */ + public function testUpdate( EntityDocument $entityToUpdate, EditMetadata $editMetadata ): void { + $expectedRevisionId = 234; + $expectedRevisionTimestamp = '20221111070707'; + $expectedRevisionEntity = $entityToUpdate->copy(); + $expectedFormattedSummary = 'FORMATTED SUMMARY'; + + $this->summaryFormatter = $this->createMock( EditSummaryFormatter::class ); + $this->summaryFormatter->expects( $this->once() ) + ->method( 'format' ) + ->with( $editMetadata->getSummary() ) + ->willReturn( $expectedFormattedSummary ); + + $editEntity = $this->createMock( EditEntity::class ); + $editEntity->expects( $this->once() ) + ->method( 'attemptSave' ) + ->with( + $entityToUpdate, + $expectedFormattedSummary, + $editMetadata->isBot() ? EDIT_UPDATE | EDIT_FORCE_BOT : EDIT_UPDATE, + false, + false, + $editMetadata->getTags() + ) + ->willReturn( + Status::newGood( [ + 'revision' => new EntityRevision( $expectedRevisionEntity, $expectedRevisionId, $expectedRevisionTimestamp ), + ] ) + ); + + $this->editEntityFactory = $this->createMock( MediaWikiEditEntityFactory::class ); + $this->editEntityFactory->expects( $this->once() ) + ->method( 'newEditEntity' ) + ->with( $this->context, $entityToUpdate->getId() ) + ->willReturn( $editEntity ); + + $entityRevision = $this->newEntityUpdater()->update( $entityToUpdate, $editMetadata ); + + $this->assertEquals( $entityToUpdate, $entityRevision->getEntity() ); + $this->assertSame( $expectedRevisionId, $entityRevision->getRevisionId() ); + $this->assertSame( $expectedRevisionTimestamp, $entityRevision->getTimestamp() ); + } + + public function provideEntityAndEditMetadata(): array { + $editMetadata = [ + 'bot edit' => [ new EditMetadata( [], true, $this->createStub( EditSummary::class ) ) ], + 'user edit' => [ new EditMetadata( [], false, $this->createStub( EditSummary::class ) ) ], + ]; + + $dataSet = []; + foreach ( $this->provideEntity() as $entityType => $entity ) { + foreach ( $editMetadata as $metadataType => $metadata ) { + $dataSet["$entityType with $metadataType"] = array_merge( $entity, $metadata ); + } + } + + return $dataSet; + } + + /** + * @dataProvider provideEntity + */ + public function testGivenSavingFails_throwsGenericException( EntityDocument $entityToUpdate ): void { + $errorStatus = Status::newFatal( 'failed to save. sad times.' ); + + $editEntity = $this->createStub( EditEntity::class ); + $editEntity->method( 'attemptSave' )->willReturn( $errorStatus ); + $this->editEntityFactory = $this->createStub( MediaWikiEditEntityFactory::class ); + $this->editEntityFactory->method( 'newEditEntity' )->willReturn( $editEntity ); + + $this->expectExceptionObject( new EntityUpdateFailed( (string)$errorStatus ) ); + + $this->newEntityUpdater()->update( $entityToUpdate, $this->createStub( EditMetadata::class ) ); + } + + /** + * @dataProvider provideEntityAndErrorStatus + */ + public function testGivenEditPrevented_throwsCorrespondingException( EntityDocument $entityToUpdate, Status $errorStatus ): void { + $editEntity = $this->createStub( EditEntity::class ); + $editEntity->method( 'attemptSave' )->willReturn( $errorStatus ); + $this->editEntityFactory = $this->createStub( MediaWikiEditEntityFactory::class ); + $this->editEntityFactory->method( 'newEditEntity' )->willReturn( $editEntity ); + + $this->expectExceptionObject( new EntityUpdatePrevented( (string)$errorStatus ) ); + + $this->newEntityUpdater()->update( $entityToUpdate, $this->createStub( EditMetadata::class ) ); + } + + public function provideEntity(): Generator { + $itemId = new ItemId( 'Q123' ); + $statementId = new StatementGuid( $itemId, 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE' ); + $item = NewItem::withId( $itemId ) + ->andLabel( 'en', 'English Label' ) + ->andDescription( 'en', 'English Description' ) + ->andStatement( + NewStatement::someValueFor( 'P321' )->withGuid( $statementId )->build() + )->build(); + yield 'item' => [ $item ]; + + $propertyId = new NumericPropertyId( 'P123' ); + $statementId = new StatementGuid( $propertyId, 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE' ); + $statement = NewStatement::someValueFor( 'P321' )->withGuid( $statementId )->build(); + $property = Property::newFromType( 'string' ); + $property->setId( $propertyId ); + $property->setStatements( new StatementList( $statement ) ); + yield 'property' => [ $property ]; + } + + public function provideEntityAndErrorStatus(): array { + $errorStatuses = [ + "basic 'actionthrottledtext' error" => [ Status::newFatal( 'actionthrottledtext' ) ], + "wfMessage 'actionthrottledtext' error" => [ Status::newFatal( wfMessage( 'actionthrottledtext' ) ) ], + "'abusefilter-disallowed' error" => [ Status::newFatal( 'abusefilter-disallowed' ) ], + "'spam-blacklisted-link' error" => [ Status::newFatal( 'spam-blacklisted-link' ) ], + "'spam-blacklisted-email' error" => [ Status::newFatal( 'spam-blacklisted-email' ) ], + ]; + + $dataSet = []; + foreach ( $this->provideEntity() as $entityType => $entity ) { + foreach ( $errorStatuses as $errorStatusType => $errorStatus ) { + $dataSet["$entityType with $errorStatusType"] = array_merge( $entity, $errorStatus ); + } + } + + return $dataSet; + } + + public function testGivenSavingSucceedsWithErrors_logsErrors(): void { + $saveStatus = Status::newGood( [ + 'revision' => new EntityRevision( new FakeEntityDocument(), 123, '20221111070707' ), + ] ); + $saveStatus->merge( Status::newFatal( 'saving succeeded but something else went wrong' ) ); + $saveStatus->setOK( true ); + + $this->logger = $this->createMock( LoggerInterface::class ); + $this->logger->expects( $this->once() ) + ->method( 'warning' ) + ->with( (string)$saveStatus ); + + $editEntity = $this->createStub( EditEntity::class ); + $editEntity->method( 'attemptSave' )->willReturn( $saveStatus ); + + $this->editEntityFactory = $this->createStub( MediaWikiEditEntityFactory::class ); + $this->editEntityFactory->method( 'newEditEntity' )->willReturn( $editEntity ); + + $this->assertInstanceOf( + EntityRevision::class, + $this->newEntityUpdater()->update( + $this->createStub( EntityDocument::class ), + $this->createStub( EditMetadata::class ) + ) + ); + } + + public function testGivenUserWithoutBotRight_throwsForBotEdit(): void { + $this->permissionManager = $this->createMock( PermissionManager::class ); + $this->permissionManager->expects( $this->once() ) + ->method( 'userHasRight' ) + ->with( $this->context->getUser(), 'bot' ) + ->willReturn( false ); + + $this->expectException( RuntimeException::class ); + + $this->newEntityUpdater()->update( + $this->createStub( EntityDocument::class ), + new EditMetadata( [], true, $this->createStub( EditSummary::class ) ) + ); + } + + private function newEntityUpdater(): EntityUpdater { + return new EntityUpdater( + $this->context, + $this->editEntityFactory, + $this->logger, + $this->summaryFormatter, + $this->permissionManager + ); + } + +} diff --git a/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/MediaWikiEditEntityFactoryItemUpdaterIntegrationTest.php b/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/MediaWikiEditEntityFactoryItemUpdaterIntegrationTest.php deleted file mode 100644 index 777cd805087..00000000000 --- a/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/MediaWikiEditEntityFactoryItemUpdaterIntegrationTest.php +++ /dev/null @@ -1,108 +0,0 @@ -withGuid( $statementId ) - ->withValue( 'statement value' ) - ->build(); - $itemToUpdate = NewItem::withId( $itemId )->andStatement( $statementToRemove )->build(); - - $this->saveNewItem( $itemToUpdate ); - - $itemToUpdate->getStatements()->removeStatementsWithGuid( $statementId ); - - $newRevision = $this->newItemUpdater()->update( - $itemToUpdate, - new EditMetadata( [], false, StatementEditSummary::newRemoveSummary( null, $statementToRemove ) ) - ); - - $this->assertCount( 0, $newRevision->getItem()->getStatements() ); - } - - public function testUpdate_replaceStatementOnItem(): void { - $itemId = 'Q345'; - $statementGuid = new StatementGuid( new ItemId( $itemId ), 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE' ); - $oldStatement = NewStatement::forProperty( 'P123' ) - ->withGuid( $statementGuid ) - ->withValue( 'old statement value' ) - ->build(); - $newValue = 'new statement value'; - $newStatement = NewStatement::forProperty( 'P123' ) - ->withGuid( $statementGuid ) - ->withValue( $newValue ) - ->build(); - $itemToUpdate = NewItem::withId( $itemId )->andStatement( $oldStatement )->build(); - - $this->saveNewItem( $itemToUpdate ); - - $itemToUpdate->getStatements()->replaceStatement( $statementGuid, $newStatement ); - - $newRevision = $this->newItemUpdater()->update( - $itemToUpdate, - new EditMetadata( [], false, StatementEditSummary::newReplaceSummary( null, $newStatement ) ) - ); - - $statementList = $newRevision->getItem()->getStatements(); - $this->assertSame( - $newValue, - $statementList->getStatementById( $statementGuid )->getValue()->getContent()->getValue() - ); - } - - private function saveNewItem( Item $item ): void { - WikibaseRepo::getEntityStore()->saveEntity( - $item, - __METHOD__, - $this->getTestUser()->getUser(), - EDIT_NEW - ); - } - - private function newItemUpdater(): MediaWikiEditEntityFactoryItemUpdater { - $permissionManager = $this->createStub( PermissionManager::class ); - $permissionManager->method( $this->anything() )->willReturn( true ); - - return new MediaWikiEditEntityFactoryItemUpdater( - RequestContext::getMain(), - WikibaseRepo::getEditEntityFactory(), - new NullLogger(), - $this->createStub( EditSummaryFormatter::class ), - $permissionManager, - new StatementReadModelConverter( new StatementGuidParser( new ItemIdParser() ), new InMemoryDataTypeLookup() ) - ); - } - -} diff --git a/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/MediaWikiEditEntityFactoryItemUpdaterTest.php b/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/MediaWikiEditEntityFactoryItemUpdaterTest.php deleted file mode 100644 index c78e26574d5..00000000000 --- a/repo/rest-api/tests/phpunit/Infrastructure/DataAccess/MediaWikiEditEntityFactoryItemUpdaterTest.php +++ /dev/null @@ -1,243 +0,0 @@ -context = $this->createStub( IContextSource::class ); - $this->context->method( 'getUser' )->willReturn( $this->createStub( User::class ) ); - $this->editEntityFactory = $this->createStub( MediaWikiEditEntityFactory::class ); - $this->logger = $this->createStub( LoggerInterface::class ); - $this->summaryFormatter = $this->createStub( EditSummaryFormatter::class ); - $this->permissionManager = $this->createStub( PermissionManager::class ); - $this->permissionManager->method( 'userHasRight' )->willReturn( true ); - } - - /** - * @dataProvider editMetadataProvider - */ - public function testUpdate( EditMetadata $editMetadata ): void { - [ $itemToUpdate, $expectedResultingItem ] = $this->newEquivalentWriteAndReadModelItem(); - $expectedRevisionId = 234; - $expectedRevisionTimestamp = '20221111070707'; - $expectedRevisionItem = $itemToUpdate->copy(); - $expectedFormattedSummary = 'FORMATTED SUMMARY'; - - $this->summaryFormatter = $this->createMock( EditSummaryFormatter::class ); - $this->summaryFormatter->expects( $this->once() ) - ->method( 'format' ) - ->with( $editMetadata->getSummary() ) - ->willReturn( $expectedFormattedSummary ); - - $editEntity = $this->createMock( EditEntity::class ); - $editEntity->expects( $this->once() ) - ->method( 'attemptSave' ) - ->with( - $itemToUpdate, - $expectedFormattedSummary, - $editMetadata->isBot() ? EDIT_UPDATE | EDIT_FORCE_BOT : EDIT_UPDATE, - false, - false, - $editMetadata->getTags() - ) - ->willReturn( - Status::newGood( [ - 'revision' => new EntityRevision( $expectedRevisionItem, $expectedRevisionId, $expectedRevisionTimestamp ), - ] ) - ); - - $this->editEntityFactory = $this->createMock( MediaWikiEditEntityFactory::class ); - $this->editEntityFactory->expects( $this->once() ) - ->method( 'newEditEntity' ) - ->with( $this->context, $itemToUpdate->getId() ) - ->willReturn( $editEntity ); - - $itemRevision = $this->newItemUpdater()->update( $itemToUpdate, $editMetadata ); - - $this->assertEquals( $expectedResultingItem, $itemRevision->getItem() ); - $this->assertSame( $expectedRevisionId, $itemRevision->getRevisionId() ); - $this->assertSame( $expectedRevisionTimestamp, $itemRevision->getLastModified() ); - } - - public function editMetadataProvider(): Generator { - yield 'bot edit' => [ - new EditMetadata( [], true, $this->createStub( EditSummary::class ) ), - ]; - yield 'user edit' => [ - new EditMetadata( [], false, $this->createStub( EditSummary::class ) ), - ]; - } - - public function testGivenSavingFails_throwsGenericException(): void { - $itemToUpdate = NewItem::withId( 'Q123' )->build(); - $editMeta = new EditMetadata( [ 'tag', 'also a tag' ], false, $this->createStub( EditSummary::class ) ); - $errorStatus = Status::newFatal( 'failed to save. sad times.' ); - - $editEntity = $this->createStub( EditEntity::class ); - $editEntity->method( 'attemptSave' )->willReturn( $errorStatus ); - - $this->editEntityFactory = $this->createStub( MediaWikiEditEntityFactory::class ); - $this->editEntityFactory->method( 'newEditEntity' )->willReturn( $editEntity ); - - $updater = $this->newItemUpdater(); - - $this->expectException( ItemUpdateFailed::class ); - $this->expectExceptionMessage( (string)$errorStatus ); - - $updater->update( $itemToUpdate, $editMeta ); - } - - /** - * @dataProvider editPreventedStatusProvider - */ - public function testGivenEditPrevented_throwsCorrespondingException( Status $errorStatus ): void { - $itemToUpdate = NewItem::withId( 'Q123' )->build(); - $editMeta = new EditMetadata( [ 'tag', 'also a tag' ], false, $this->createStub( EditSummary::class ) ); - - $editEntity = $this->createStub( EditEntity::class ); - $editEntity->method( 'attemptSave' )->willReturn( $errorStatus ); - - $this->editEntityFactory = $this->createStub( MediaWikiEditEntityFactory::class ); - $this->editEntityFactory->method( 'newEditEntity' )->willReturn( $editEntity ); - - $updater = $this->newItemUpdater(); - - $this->expectException( ItemUpdatePrevented::class ); - $this->expectExceptionMessage( (string)$errorStatus ); - - $updater->update( $itemToUpdate, $editMeta ); - } - - public static function editPreventedStatusProvider(): Generator { - yield [ Status::newFatal( 'actionthrottledtext' ) ]; - yield [ Status::newFatal( wfMessage( 'actionthrottledtext' ) ) ]; - yield [ Status::newFatal( 'abusefilter-disallowed' ) ]; - yield [ Status::newFatal( 'spam-blacklisted-link' ) ]; - yield [ Status::newFatal( 'spam-blacklisted-email' ) ]; - } - - public function testGivenSavingSucceedsWithErrors_logsErrors(): void { - $saveStatus = Status::newGood( [ - 'revision' => new EntityRevision( new Item(), 123, '20221111070707' ), - ] ); - $saveStatus->merge( Status::newFatal( 'saving succeeded but something else went wrong' ) ); - $saveStatus->setOK( true ); - - $this->logger = $this->createMock( LoggerInterface::class ); - $this->logger->expects( $this->once() ) - ->method( 'warning' ) - ->with( (string)$saveStatus ); - - $editEntity = $this->createStub( EditEntity::class ); - $editEntity->method( 'attemptSave' )->willReturn( $saveStatus ); - - $this->editEntityFactory = $this->createStub( MediaWikiEditEntityFactory::class ); - $this->editEntityFactory->method( 'newEditEntity' )->willReturn( $editEntity ); - - $this->assertInstanceOf( - ItemRevision::class, - $this->newItemUpdater()->update( - $this->createStub( Item::class ), - $this->createStub( EditMetadata::class ) - ) - ); - } - - public function testGivenUserWithoutBotRight_throwsForBotEdit(): void { - $this->permissionManager = $this->createMock( PermissionManager::class ); - $this->permissionManager->expects( $this->once() ) - ->method( 'userHasRight' ) - ->with( $this->context->getUser(), 'bot' ) - ->willReturn( false ); - - $this->expectException( RuntimeException::class ); - - $this->newItemUpdater()->update( - $this->createStub( Item::class ), - new EditMetadata( [], true, $this->createStub( EditSummary::class ) ) - ); - } - - private function newItemUpdater(): MediaWikiEditEntityFactoryItemUpdater { - return new MediaWikiEditEntityFactoryItemUpdater( - $this->context, - $this->editEntityFactory, - $this->logger, - $this->summaryFormatter, - $this->permissionManager, - new StatementReadModelConverter( new StatementGuidParser( new ItemIdParser() ), new InMemoryDataTypeLookup() ) - ); - } - - private function newEquivalentWriteAndReadModelItem(): array { - $writeModelStatement = NewStatement::someValueFor( 'P123' ) - ->withGuid( 'Q123$AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE' ) - ->build(); - $readModelStatement = NewStatementReadModel::someValueFor( 'P123' ) - ->withGuid( 'Q123$AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE' ) - ->build(); - - return [ - NewItem::withId( 'Q123' ) - ->andLabel( 'en', 'English Label' ) - ->andDescription( 'en', 'English Description' ) - ->andStatement( $writeModelStatement ) - ->build(), - new ReadModelItem( - new Labels( new Label( 'en', 'English Label' ) ), - new Descriptions( new Description( 'en', 'English Description' ) ), - new StatementList( $readModelStatement ) - ), - ]; - } - -} diff --git a/repo/rest-api/tests/phpunit/RouteHandlers/Middleware/UnexpectedErrorHandlerMiddlewareTest.php b/repo/rest-api/tests/phpunit/RouteHandlers/Middleware/UnexpectedErrorHandlerMiddlewareTest.php index 547163a1f2d..6b62515e53c 100644 --- a/repo/rest-api/tests/phpunit/RouteHandlers/Middleware/UnexpectedErrorHandlerMiddlewareTest.php +++ b/repo/rest-api/tests/phpunit/RouteHandlers/Middleware/UnexpectedErrorHandlerMiddlewareTest.php @@ -13,7 +13,7 @@ use Throwable; use TypeError; use Wikibase\Repo\RestApi\Application\UseCases\UseCaseError; -use Wikibase\Repo\RestApi\Domain\Services\Exceptions\ItemUpdatePrevented; +use Wikibase\Repo\RestApi\Domain\Services\Exceptions\EntityUpdatePrevented; use Wikibase\Repo\RestApi\RouteHandlers\Middleware\UnexpectedErrorHandlerMiddleware; use Wikibase\Repo\RestApi\RouteHandlers\ResponseFactory; @@ -93,7 +93,7 @@ public static function throwableProvider(): Generator { public function testGivenEditPrevented_logsWarning(): void { $routeHandler = $this->createStub( Handler::class ); - $exception = new ItemUpdatePrevented( 'bad things happened' ); + $exception = new EntityUpdatePrevented( 'bad things happened' ); $this->logger = $this->createMock( LoggerInterface::class ); $this->logger->expects( $this->once() )