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\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 ) )

View File

@ -1,12 +1,35 @@
<?php
namespace ActivityPub\Objects;
use ActivityPub\Auth\AuthService;
use ActivityPub\Entities\ActivityPubObject;
use ActivityPub\Objects\ContextProvider;
use Symfony\Component\HttpFoundation\Request;
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
@ -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 )

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\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 );

View File

@ -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' );

View File

@ -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 ),

View File

@ -1,14 +1,93 @@
<?php
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 Symfony\Component\HttpFoundation\Request;
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()
{
// 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]"
);
}
}
}
?>

View File

@ -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

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;
}
}
?>