From ed46ec1e1fd5e3dbfd096f5d1a33b0acc2e8514d Mon Sep 17 00:00:00 2001 From: Jeremy Dormitzer Date: Sun, 20 Jan 2019 17:20:24 -0500 Subject: [PATCH] Extract objectFromArray to service; implement CollectionService; test --- src/Config/ActivityPubModule.php | 17 +++- src/Objects/CollectionsService.php | 92 +++++++++++++++++--- src/Objects/ContextProvider.php | 26 ++++++ test/Auth/SignatureListenerTest.php | 16 +--- test/Controllers/GetObjectControllerTest.php | 20 ++--- test/Entities/EntityTest.php | 15 ++-- test/Objects/CollectionsServiceTest.php | 83 +++++++++++++++++- test/Objects/ObjectsServiceTest.php | 7 +- test/TestUtils/TestUtils.php | 22 +++++ 9 files changed, 245 insertions(+), 53 deletions(-) create mode 100644 src/Objects/ContextProvider.php create mode 100644 test/TestUtils/TestUtils.php diff --git a/src/Config/ActivityPubModule.php b/src/Config/ActivityPubModule.php index 50ba5e1..23f6ec2 100644 --- a/src/Config/ActivityPubModule.php +++ b/src/Config/ActivityPubModule.php @@ -10,6 +10,7 @@ use ActivityPub\Controllers\OutboxController; use ActivityPub\Crypto\HttpSignatureService; use ActivityPub\Database\PrefixNamingStrategy; use ActivityPub\Http\ControllerResolver; +use ActivityPub\Objects\ContextProvider; use ActivityPub\Objects\CollectionsService; use ActivityPub\Objects\ObjectsService; use ActivityPub\Utils\SimpleDateTimeProvider; @@ -24,6 +25,8 @@ use Symfony\Component\DependencyInjection\Reference; */ class ActivityPubModule { + const COLLECTION_PAGE_SIZE = 20; + /** * @var ContainerBuilder */ @@ -37,6 +40,10 @@ class ActivityPubModule 'authFunction' => function() { return false; }, + 'context' => array( + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + ), ); $options = array_merge( $defaults, $options ); $this->validateOptions( $options ); @@ -76,10 +83,16 @@ class ActivityPubModule $this->injector->register( AuthListener::class, AuthListener::class ) ->addArgument( $options['authFunction'] ); - $this->injector->register( CollectionsService::class, CollectionsService::class ); - $this->injector->register( AuthService::class, AuthService::class ); + $this->injector->register( ContextProvider::class, ContextProvider::class ) + ->addArgument( $options['context'] ); + + $this->injector->register( CollectionsService::class, CollectionsService::class ) + ->addArgument( self::COLLECTION_PAGE_SIZE ) + ->addArgument( new Reference( AuthService::class ) ) + ->addArgument( new Reference( ContextProvider::class ) ); + $this->injector->register( GetObjectController::class, GetObjectController::class ) ->addArgument( new Reference( ObjectsService::class ) ) ->addArgument( new Reference( CollectionsService::class ) ) diff --git a/src/Objects/CollectionsService.php b/src/Objects/CollectionsService.php index 5bcd4c6..e8c3543 100644 --- a/src/Objects/CollectionsService.php +++ b/src/Objects/CollectionsService.php @@ -1,12 +1,35 @@ pageSize = $pageSize; + $this->authService = $authService; + $this->contextProvider = $contextProvider; + } /** * Returns an array representation of the $collection @@ -17,28 +40,77 @@ class CollectionsService public function pageAndFilterCollection( Request $request, ActivityPubObject $collection ) { - // expected behavior: - // - request with no 'offset' param returns the collection object, - // with the first page appended as with Pleroma - // - request with an 'offset' param returns the collection page starting - // at that offset with the next PAGE_SIZE items if ( $request->query->has( 'offset' ) ) { - // return a filtered collection page + return $this->getCollectionPage( + $collection, $request, $request->query->get( 'offset' ), $this->pageSize + ); } - // else return the collection itself with the first page + $colArr = array(); + foreach ( $collection->getFields() as $field ) { + if ( ! in_array( $field->getName(), array( 'items', 'orderedItems' ) ) ) { + if ( $field->hasValue() ) { + $colArr[$field->getName()] = $field->getValue(); + } else { + $colArr[$field->getName()] = $field->getTargetObject()->asArray( 1 ); + } + } + } + $firstPage = $this->getCollectionPage( + $collection, $request, 0, $this->pageSize + ); + $colArr['first'] = $firstPage; + return $colArr; } private function getCollectionPage( ActivityPubObject $collection, + Request $request, int $offset, int $pageSize ) { $itemsKey = 'items'; $pageType = 'CollectionPage'; - if ( $this->isOrdered( $collection ) ) { + $isOrdered = $this->isOrdered( $collection ); + if ( $isOrdered ) { $itemsKey = 'orderedItems'; $pageType = 'OrderedCollectionPage'; } - // Create and return the page as an array + if ( ! $collection->hasField( $itemsKey ) ) { + throw new InvalidArgumentException( + "Collection does not have an \"$field\" key" + ); + } + $collectionItems = $collection->getFieldValue( $itemsKey ); + $pageItems = array(); + $idx = $offset; + $count = 0; + while ( $count < $pageSize ) { + $item = $collectionItems->getFieldValue( $idx ); + if ( ! $item ) { + break; + } + if ( is_string( $item ) ) { + $pageItems[] = $item; + $count++; + } else if ( $this->authService->requestAuthorizedToView( $request, $item ) ) { + $pageItems[] = $item->asArray( 1 ); + $count++; + } + $idx++; + } + $page = array( + '@context' => $this->contextProvider->getContext(), + 'id' => $collection['id'] . "?offset=$offset", + 'type' => $pageType, + $itemsKey => $pageItems, + 'partOf' => $collection['id'], + ); + if ( $collectionItems->getFieldValue( $idx ) ) { + $page['next'] = $collection['id'] . "?offset=$idx"; + } + if ( $isOrdered ) { + $page['startIndex'] = $offset; + } + return $page; } private function isOrdered( ActivityPubObject $collection ) diff --git a/src/Objects/ContextProvider.php b/src/Objects/ContextProvider.php new file mode 100644 index 0000000..f8cc440 --- /dev/null +++ b/src/Objects/ContextProvider.php @@ -0,0 +1,26 @@ +ctx = $ctx; + } + + public function getContext() + { + return $this->ctx; + } +} +?> diff --git a/test/Auth/SignatureListenerTest.php b/test/Auth/SignatureListenerTest.php index 8e0e4ed..0ec3590 100644 --- a/test/Auth/SignatureListenerTest.php +++ b/test/Auth/SignatureListenerTest.php @@ -8,6 +8,7 @@ use ActivityPub\Entities\ActivityPubObject; use ActivityPub\Entities\Field; use ActivityPub\Objects\ObjectsService; use ActivityPub\Test\TestUtils\TestDateTimeProvider; +use ActivityPub\Test\TestUtils\TestUtils; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Event\GetResponseEvent; @@ -41,26 +42,13 @@ oYi+1hqp1fIekaxsyQIDAQAB $objectsService = $this->createMock( ObjectsService::class ); $objectsService->method( 'dereference' ) ->will( $this->returnValueMap( array( - array( self::KEY_ID, self::objectFromArray( self::KEY ) ) + array( self::KEY_ID, TestUtils::objectFromArray( self::KEY ) ) ) ) ); $this->signatureListener = new SignatureListener( $httpSignatureService, $objectsService ); } - private static function objectFromArray( $array ) { - $object = new ActivityPubObject(); - foreach ( $array as $name => $value ) { - if ( is_array( $value ) ) { - $child = $this->objectFromArray( $value ); - Field::withObject( $object, $name, $child ); - } else { - Field::withValue( $object, $name, $value ); - } - } - return $object; - } - private function getEvent() { $kernel = $this->createMock( HttpKernelInterface::class ); diff --git a/test/Controllers/GetObjectControllerTest.php b/test/Controllers/GetObjectControllerTest.php index 3cd573d..63776a9 100644 --- a/test/Controllers/GetObjectControllerTest.php +++ b/test/Controllers/GetObjectControllerTest.php @@ -5,8 +5,10 @@ use ActivityPub\Auth\AuthService; use ActivityPub\Controllers\GetObjectController; use ActivityPub\Entities\ActivityPubObject; use ActivityPub\Entities\Field; +use ActivityPub\Objects\ContextProvider; use ActivityPub\Objects\CollectionsService; use ActivityPub\Objects\ObjectsService; +use ActivityPub\Test\TestUtils\TestUtils; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -57,30 +59,18 @@ class GetObjectControllerTest extends TestCase $objectsService->method( 'dereference' )->will( $this->returnCallback( function( $uri ) { if ( array_key_exists( $uri, self::OBJECTS ) ) { - return $this->objectFromArray( self::OBJECTS[$uri] ); + return TestUtils::objectFromArray( self::OBJECTS[$uri] ); } }) ); - $collectionsService = new CollectionsService(); $authService = new AuthService(); + $contextProvider = new ContextProvider(); + $collectionsService = new CollectionsService( 4, $authService, $contextProvider ); $this->getObjectController = new GetObjectController( $objectsService, $collectionsService, $authService ); } - private function objectFromArray( $array ) { - $object = new ActivityPubObject(); - foreach ( $array as $name => $value ) { - if ( is_array( $value ) ) { - $child = $this->objectFromArray( $value ); - Field::withObject( $object, $name, $child ); - } else { - Field::withValue( $object, $name, $value ); - } - } - return $object; - } - public function testItRendersPersistedObject() { $request = Request::create( 'https://example.com/objects/1' ); diff --git a/test/Entities/EntityTest.php b/test/Entities/EntityTest.php index c56cf17..6bb5a2e 100644 --- a/test/Entities/EntityTest.php +++ b/test/Entities/EntityTest.php @@ -38,9 +38,10 @@ class EntityTest extends SQLiteTestCase 'path' => $this->getDbPath(), ); $this->entityManager = EntityManager::create( $dbParams, $dbConfig ); - $this->dateTimeProvider = new TestDateTimeProvider( - new DateTime( "12:00" ), new DateTime( "12:01" ) - ); + $this->dateTimeProvider = new TestDateTimeProvider( array( + 'objects-service.create' => new DateTime( "12:00" ), + 'objects-service.update' => new DateTime( "12:01" ), + ) ); } private function getTime( $context ) { @@ -51,12 +52,12 @@ class EntityTest extends SQLiteTestCase public function testItCreatesAnObjectWithAPrivateKey() { - $object = new ActivityPubObject( $this->dateTimeProvider->getTime( 'create' ) ); + $object = new ActivityPubObject( $this->dateTimeProvider->getTime( 'objects-service.create' ) ); $privateKey = 'a private key'; $object->setPrivateKey( $privateKey ); $this->entityManager->persist( $object ); $this->entityManager->flush(); - $now = $this->getTime( 'create' ); + $now = $this->getTime( 'objects-service.create' ); $expected = new ArrayDataSet( array( 'objects' => array( array( 'id' => 1, 'created' => $now, 'lastUpdated' => $now ), @@ -79,7 +80,7 @@ class EntityTest extends SQLiteTestCase public function itUpdatesAPrivateKey() { - $object = new ActivityPubObject( $this->dateTimeProvider->getTime( 'create' ) ); + $object = new ActivityPubObject( $this->dateTimeProvider->getTime( 'objects-service.create' ) ); $privateKey = 'a private key'; $object->setPrivateKey( $privateKey ); $this->entityManager->persist( $object ); @@ -88,7 +89,7 @@ class EntityTest extends SQLiteTestCase $object->setPrivateKey( $newPrivateKey ); $this->entityManager->persiste( $object ); $this->entityManager->flush(); - $now = $this->getTime( 'create' ); + $now = $this->getTime( 'objects-service.create' ); $expected = new ArrayDataSet( array( 'objects' => array( array( 'id' => 1, 'created' => $now, 'lastUpdated' => $now ), diff --git a/test/Objects/CollectionsServiceTest.php b/test/Objects/CollectionsServiceTest.php index eebfec3..aaf1c31 100644 --- a/test/Objects/CollectionsServiceTest.php +++ b/test/Objects/CollectionsServiceTest.php @@ -1,14 +1,93 @@ collectionsService = new CollectionsService( + 4, $authService, $contextProvider + ); + } + public function testCollectionsService() { - // TODO implement me - $this->assertTrue( false ); + $testCases = array( + array( + 'id' => 'lessThanOnePage', + 'collection' => array( + '@context' => array( + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + ), + 'id' => 'https://example.com/objects/1', + 'type' => 'OrderedCollection', + 'orderedItems' => array( + array( + 'id' => 'https://example.com/objects/2', + ), + array( + 'id' => 'https://example.com/objects/3', + ), + array( + 'id' => 'https://example.com/objects/4', + ), + ) + ), + 'request' => Request::create( + 'https://example.com/objects/1', + Request::METHOD_GET + ), + 'expectedResult' => array( + '@context' => array( + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + ), + 'id' => 'https://example.com/objects/1', + 'type' => 'OrderedCollection', + 'first' => array( + '@context' => array( + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + ), + 'id' => 'https://example.com/objects/1?offset=0', + 'type' => 'OrderedCollectionPage', + 'partOf' => 'https://example.com/objects/1', + 'startIndex' => 0, + 'orderedItems' => array( + array( + 'id' => 'https://example.com/objects/2', + ), + array( + 'id' => 'https://example.com/objects/3', + ), + array( + 'id' => 'https://example.com/objects/4', + ), + ), + ), + ), + ), + ); + foreach ( $testCases as $testCase ) { + $actual = $this->collectionsService->pageAndFilterCollection( + $testCase['request'], TestUtils::objectFromArray( $testCase['collection'] ) + ); + $this->assertEquals( + $testCase['expectedResult'], $actual, "Error on test $testCase[id]" + ); + } } } ?> diff --git a/test/Objects/ObjectsServiceTest.php b/test/Objects/ObjectsServiceTest.php index fd8440a..9712a8b 100644 --- a/test/Objects/ObjectsServiceTest.php +++ b/test/Objects/ObjectsServiceTest.php @@ -40,9 +40,10 @@ class ObjectsServiceTest extends SQLiteTestCase 'path' => $this->getDbPath(), ); $this->entityManager = EntityManager::create( $dbParams, $dbConfig ); - $this->dateTimeProvider = new TestDateTimeProvider( - array( 'objects-service.create' => new DateTime( "12:00" ), 'objects-service.update' => new DateTime( "12:01" ) ) - ); + $this->dateTimeProvider = new TestDateTimeProvider( array( + 'objects-service.create' => new DateTime( "12:00" ), + 'objects-service.update' => new DateTime( "12:01" ), + ) ); $this->httpClient = new Client( array( 'http_errors' => false ) ); $this->objectsService = new ObjectsService( $this->entityManager, $this->dateTimeProvider, $this->httpClient diff --git a/test/TestUtils/TestUtils.php b/test/TestUtils/TestUtils.php new file mode 100644 index 0000000..74a743e --- /dev/null +++ b/test/TestUtils/TestUtils.php @@ -0,0 +1,22 @@ + $value ) { + if ( is_array( $value ) ) { + $child = self::objectFromArray( $value ); + Field::withObject( $object, $name, $child ); + } else { + Field::withValue( $object, $name, $value ); + } + } + return $object; + } +} +?>