Extract objectFromArray to service; implement CollectionService; test

This commit is contained in:
Jeremy Dormitzer 2019-01-20 17:20:24 -05:00
parent 499e4d235d
commit ed46ec1e1f
9 changed files with 245 additions and 53 deletions

View File

@ -10,6 +10,7 @@ use ActivityPub\Controllers\OutboxController;
use ActivityPub\Crypto\HttpSignatureService; use ActivityPub\Crypto\HttpSignatureService;
use ActivityPub\Database\PrefixNamingStrategy; use ActivityPub\Database\PrefixNamingStrategy;
use ActivityPub\Http\ControllerResolver; use ActivityPub\Http\ControllerResolver;
use ActivityPub\Objects\ContextProvider;
use ActivityPub\Objects\CollectionsService; use ActivityPub\Objects\CollectionsService;
use ActivityPub\Objects\ObjectsService; use ActivityPub\Objects\ObjectsService;
use ActivityPub\Utils\SimpleDateTimeProvider; use ActivityPub\Utils\SimpleDateTimeProvider;
@ -24,6 +25,8 @@ use Symfony\Component\DependencyInjection\Reference;
*/ */
class ActivityPubModule class ActivityPubModule
{ {
const COLLECTION_PAGE_SIZE = 20;
/** /**
* @var ContainerBuilder * @var ContainerBuilder
*/ */
@ -37,6 +40,10 @@ class ActivityPubModule
'authFunction' => function() { 'authFunction' => function() {
return false; return false;
}, },
'context' => array(
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
),
); );
$options = array_merge( $defaults, $options ); $options = array_merge( $defaults, $options );
$this->validateOptions( $options ); $this->validateOptions( $options );
@ -76,10 +83,16 @@ class ActivityPubModule
$this->injector->register( AuthListener::class, AuthListener::class ) $this->injector->register( AuthListener::class, AuthListener::class )
->addArgument( $options['authFunction'] ); ->addArgument( $options['authFunction'] );
$this->injector->register( CollectionsService::class, CollectionsService::class );
$this->injector->register( AuthService::class, AuthService::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 ) $this->injector->register( GetObjectController::class, GetObjectController::class )
->addArgument( new Reference( ObjectsService::class ) ) ->addArgument( new Reference( ObjectsService::class ) )
->addArgument( new Reference( CollectionsService::class ) ) ->addArgument( new Reference( CollectionsService::class ) )

View File

@ -1,12 +1,35 @@
<?php <?php
namespace ActivityPub\Objects; namespace ActivityPub\Objects;
use ActivityPub\Auth\AuthService;
use ActivityPub\Entities\ActivityPubObject; use ActivityPub\Entities\ActivityPubObject;
use ActivityPub\Objects\ContextProvider;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
class CollectionsService class CollectionsService
{ {
const PAGE_SIZE = 20; /**
* @var int
*/
private $pageSize;
/**
* @var AuthService
*/
private $authService;
/**
* @var ContextProvider
*/
private $contextProvider;
public function __construct( int $pageSize, AuthService $authService,
ContextProvider $contextProvider )
{
$this->pageSize = $pageSize;
$this->authService = $authService;
$this->contextProvider = $contextProvider;
}
/** /**
* Returns an array representation of the $collection * Returns an array representation of the $collection
@ -17,28 +40,77 @@ class CollectionsService
public function pageAndFilterCollection( Request $request, public function pageAndFilterCollection( Request $request,
ActivityPubObject $collection ) 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' ) ) { 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, private function getCollectionPage( ActivityPubObject $collection,
Request $request,
int $offset, int $offset,
int $pageSize ) int $pageSize )
{ {
$itemsKey = 'items'; $itemsKey = 'items';
$pageType = 'CollectionPage'; $pageType = 'CollectionPage';
if ( $this->isOrdered( $collection ) ) { $isOrdered = $this->isOrdered( $collection );
if ( $isOrdered ) {
$itemsKey = 'orderedItems'; $itemsKey = 'orderedItems';
$pageType = 'OrderedCollectionPage'; $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 ) private function isOrdered( ActivityPubObject $collection )

View File

@ -0,0 +1,26 @@
<?php
namespace ActivityPub\Objects;
class ContextProvider
{
const DEFAULT_CONTEXT = array(
'https://www.w3.org/ns/activitystreams',
'https://w3id.org/security/v1',
);
private $ctx;
public function __construct( $ctx = null )
{
if ( ! $ctx ) {
$ctx = self::DEFAULT_CONTEXT;
}
$this->ctx = $ctx;
}
public function getContext()
{
return $this->ctx;
}
}
?>

View File

@ -8,6 +8,7 @@ use ActivityPub\Entities\ActivityPubObject;
use ActivityPub\Entities\Field; use ActivityPub\Entities\Field;
use ActivityPub\Objects\ObjectsService; use ActivityPub\Objects\ObjectsService;
use ActivityPub\Test\TestUtils\TestDateTimeProvider; use ActivityPub\Test\TestUtils\TestDateTimeProvider;
use ActivityPub\Test\TestUtils\TestUtils;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent; use Symfony\Component\HttpKernel\Event\GetResponseEvent;
@ -41,26 +42,13 @@ oYi+1hqp1fIekaxsyQIDAQAB
$objectsService = $this->createMock( ObjectsService::class ); $objectsService = $this->createMock( ObjectsService::class );
$objectsService->method( 'dereference' ) $objectsService->method( 'dereference' )
->will( $this->returnValueMap( array( ->will( $this->returnValueMap( array(
array( self::KEY_ID, self::objectFromArray( self::KEY ) ) array( self::KEY_ID, TestUtils::objectFromArray( self::KEY ) )
) ) ); ) ) );
$this->signatureListener = new SignatureListener( $this->signatureListener = new SignatureListener(
$httpSignatureService, $objectsService $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() private function getEvent()
{ {
$kernel = $this->createMock( HttpKernelInterface::class ); $kernel = $this->createMock( HttpKernelInterface::class );

View File

@ -5,8 +5,10 @@ use ActivityPub\Auth\AuthService;
use ActivityPub\Controllers\GetObjectController; use ActivityPub\Controllers\GetObjectController;
use ActivityPub\Entities\ActivityPubObject; use ActivityPub\Entities\ActivityPubObject;
use ActivityPub\Entities\Field; use ActivityPub\Entities\Field;
use ActivityPub\Objects\ContextProvider;
use ActivityPub\Objects\CollectionsService; use ActivityPub\Objects\CollectionsService;
use ActivityPub\Objects\ObjectsService; use ActivityPub\Objects\ObjectsService;
use ActivityPub\Test\TestUtils\TestUtils;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@ -57,30 +59,18 @@ class GetObjectControllerTest extends TestCase
$objectsService->method( 'dereference' )->will( $objectsService->method( 'dereference' )->will(
$this->returnCallback( function( $uri ) { $this->returnCallback( function( $uri ) {
if ( array_key_exists( $uri, self::OBJECTS ) ) { 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(); $authService = new AuthService();
$contextProvider = new ContextProvider();
$collectionsService = new CollectionsService( 4, $authService, $contextProvider );
$this->getObjectController = new GetObjectController( $this->getObjectController = new GetObjectController(
$objectsService, $collectionsService, $authService $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() public function testItRendersPersistedObject()
{ {
$request = Request::create( 'https://example.com/objects/1' ); $request = Request::create( 'https://example.com/objects/1' );

View File

@ -38,9 +38,10 @@ class EntityTest extends SQLiteTestCase
'path' => $this->getDbPath(), 'path' => $this->getDbPath(),
); );
$this->entityManager = EntityManager::create( $dbParams, $dbConfig ); $this->entityManager = EntityManager::create( $dbParams, $dbConfig );
$this->dateTimeProvider = new TestDateTimeProvider( $this->dateTimeProvider = new TestDateTimeProvider( array(
new DateTime( "12:00" ), new DateTime( "12:01" ) 'objects-service.create' => new DateTime( "12:00" ),
); 'objects-service.update' => new DateTime( "12:01" ),
) );
} }
private function getTime( $context ) { private function getTime( $context ) {
@ -51,12 +52,12 @@ class EntityTest extends SQLiteTestCase
public function testItCreatesAnObjectWithAPrivateKey() public function testItCreatesAnObjectWithAPrivateKey()
{ {
$object = new ActivityPubObject( $this->dateTimeProvider->getTime( 'create' ) ); $object = new ActivityPubObject( $this->dateTimeProvider->getTime( 'objects-service.create' ) );
$privateKey = 'a private key'; $privateKey = 'a private key';
$object->setPrivateKey( $privateKey ); $object->setPrivateKey( $privateKey );
$this->entityManager->persist( $object ); $this->entityManager->persist( $object );
$this->entityManager->flush(); $this->entityManager->flush();
$now = $this->getTime( 'create' ); $now = $this->getTime( 'objects-service.create' );
$expected = new ArrayDataSet( array( $expected = new ArrayDataSet( array(
'objects' => array( 'objects' => array(
array( 'id' => 1, 'created' => $now, 'lastUpdated' => $now ), array( 'id' => 1, 'created' => $now, 'lastUpdated' => $now ),
@ -79,7 +80,7 @@ class EntityTest extends SQLiteTestCase
public function itUpdatesAPrivateKey() public function itUpdatesAPrivateKey()
{ {
$object = new ActivityPubObject( $this->dateTimeProvider->getTime( 'create' ) ); $object = new ActivityPubObject( $this->dateTimeProvider->getTime( 'objects-service.create' ) );
$privateKey = 'a private key'; $privateKey = 'a private key';
$object->setPrivateKey( $privateKey ); $object->setPrivateKey( $privateKey );
$this->entityManager->persist( $object ); $this->entityManager->persist( $object );
@ -88,7 +89,7 @@ class EntityTest extends SQLiteTestCase
$object->setPrivateKey( $newPrivateKey ); $object->setPrivateKey( $newPrivateKey );
$this->entityManager->persiste( $object ); $this->entityManager->persiste( $object );
$this->entityManager->flush(); $this->entityManager->flush();
$now = $this->getTime( 'create' ); $now = $this->getTime( 'objects-service.create' );
$expected = new ArrayDataSet( array( $expected = new ArrayDataSet( array(
'objects' => array( 'objects' => array(
array( 'id' => 1, 'created' => $now, 'lastUpdated' => $now ), array( 'id' => 1, 'created' => $now, 'lastUpdated' => $now ),

View File

@ -1,14 +1,93 @@
<?php <?php
namespace ActivityPub\Test\Objects; namespace ActivityPub\Test\Objects;
use ActivityPub\Auth\AuthService;
use ActivityPub\Objects\ContextProvider;
use ActivityPub\Objects\CollectionsService;
use ActivityPub\Test\TestUtils\TestUtils;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
class CollectionsServiceTest extends TestCase class CollectionsServiceTest extends TestCase
{ {
private $collectionsService;
public function setUp()
{
$authService = new AuthService();
$contextProvider = new ContextProvider();
$this->collectionsService = new CollectionsService(
4, $authService, $contextProvider
);
}
public function testCollectionsService() public function testCollectionsService()
{ {
// TODO implement me $testCases = array(
$this->assertTrue( false ); 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]"
);
}
} }
} }
?> ?>

View File

@ -40,9 +40,10 @@ class ObjectsServiceTest extends SQLiteTestCase
'path' => $this->getDbPath(), 'path' => $this->getDbPath(),
); );
$this->entityManager = EntityManager::create( $dbParams, $dbConfig ); $this->entityManager = EntityManager::create( $dbParams, $dbConfig );
$this->dateTimeProvider = new TestDateTimeProvider( $this->dateTimeProvider = new TestDateTimeProvider( array(
array( 'objects-service.create' => new DateTime( "12:00" ), 'objects-service.update' => new DateTime( "12:01" ) ) 'objects-service.create' => new DateTime( "12:00" ),
); 'objects-service.update' => new DateTime( "12:01" ),
) );
$this->httpClient = new Client( array( 'http_errors' => false ) ); $this->httpClient = new Client( array( 'http_errors' => false ) );
$this->objectsService = new ObjectsService( $this->objectsService = new ObjectsService(
$this->entityManager, $this->dateTimeProvider, $this->httpClient $this->entityManager, $this->dateTimeProvider, $this->httpClient

View File

@ -0,0 +1,22 @@
<?php
namespace ActivityPub\Test\TestUtils;
use ActivityPub\Entities\ActivityPubObject;
use ActivityPub\Entities\Field;
class TestUtils
{
public static function objectFromArray( $array ) {
$object = new ActivityPubObject();
foreach ( $array as $name => $value ) {
if ( is_array( $value ) ) {
$child = self::objectFromArray( $value );
Field::withObject( $object, $name, $child );
} else {
Field::withValue( $object, $name, $value );
}
}
return $object;
}
}
?>