diff --git a/app/commands/StatementMigrateCommand.php b/app/commands/StatementMigrateCommand.php index fc421d49ec..2af1bab5fb 100644 --- a/app/commands/StatementMigrateCommand.php +++ b/app/commands/StatementMigrateCommand.php @@ -55,7 +55,7 @@ public function fire() { }); // Uses the repository to migrate the statements. - $repo = App::make('Locker\Repository\Statement\EloquentStatementRepository'); + $repo = App::make('Locker\Repository\Statement\EloquentVoider'); $repo->updateReferences($statements_array, $lrs); $repo->voidStatements($statements_array, $lrs); diff --git a/app/controllers/LrsController.php b/app/controllers/LrsController.php index 3f7bb07034..9281f93318 100644 --- a/app/controllers/LrsController.php +++ b/app/controllers/LrsController.php @@ -1,7 +1,9 @@ <?php use \Locker\Repository\Lrs\Repository as LrsRepo; -use \Locker\Repository\Statement\StatementRepository as StatementRepo; +use \Locker\Repository\Statement\Repository as StatementRepo; +use \Locker\Repository\Statement\EloquentIndexer as StatementIndexer; +use \Locker\Repository\Statement\IndexOptions as IndexOptions; class LrsController extends BaseController { @@ -66,7 +68,7 @@ public function store() { //lrs input validation $rules['title'] = 'required'; - $rules['description'] = ''; + $rules['description'] = ''; $validator = \Validator::make($data, $rules); if ($validator->fails()) return \Redirect::back()->withErrors($validator); @@ -103,7 +105,7 @@ public function update($lrs_id){ $data = \Input::all(); //lrs input validation - $rules['title'] = 'required'; + $rules['title'] = 'required'; $validator = \Validator::make($data, $rules); if ($validator->fails()) { return \Redirect::back()->withErrors($validator); @@ -123,7 +125,7 @@ public function update($lrs_id){ /** * Display the specified resource. - * This is a temp hack until the single page app for + * This is a temp hack until the single page app for * analytics is ready. v1.0 stable. * @param String $lrs_id * @return View @@ -187,10 +189,13 @@ public function destroy($lrs_id){ * @return View */ public function statements($lrs_id){ - $statements = $this->statement->index($lrs_id, [], [ + $statements = (new StatementIndexer)->index(new IndexOptions([ + 'lrs_id' => $lrs_id, 'ascending' => false, - 'limit' => $this->statement->count($lrs_id) - ])->paginate(15); + 'limit' => $this->statement->count([ + 'lrs_id' => $lrs_id + ]) + ]))->paginate(15); return View::make('partials.statements.list', array_merge($this->getLrs($lrs_id), [ 'statements' => $statements, @@ -240,7 +245,7 @@ public function editCredentials( $lrs_id ){ $message_type = 'error'; $message = trans('update_key_error'); } - + return Redirect::back()->with($message_type, $message); } @@ -251,7 +256,7 @@ public function editCredentials( $lrs_id ){ */ public function users($lrs_id) { $opts = $this->getLrs($lrs_id); - return View::make('partials.users.list', array_merge($opts, [ + return View::make('partials.users.list', array_merge($opts, [ 'users' => $opts['lrs']->users, 'user_nav' => true ])); @@ -260,7 +265,7 @@ public function users($lrs_id) { public function inviteUsersForm($lrs_id) { $opts = $this->getLrs($lrs_id); - return View::make('partials.lrs.invite', array_merge($opts, [ + return View::make('partials.lrs.invite', array_merge($opts, [ 'users' => $opts['lrs']->users, 'user_nav' => true ])); diff --git a/app/controllers/SiteController.php b/app/controllers/SiteController.php index bf0ef74a4b..3d74c06e51 100644 --- a/app/controllers/SiteController.php +++ b/app/controllers/SiteController.php @@ -2,7 +2,7 @@ use Locker\Repository\Site\SiteRepository as SiteRepo; use Locker\Repository\Lrs\Repository as LrsRepo; -use Locker\Repository\Statement\StatementRepository as StatementRepo; +use Locker\Repository\Statement\Repository as StatementRepo; use Locker\Repository\User\UserRepository as UserRepo; class SiteController extends BaseController { @@ -20,7 +20,7 @@ public function __construct(SiteRepo $site, LrsRepo $lrs, UserRepo $user, Statem $this->beforeFilter('auth'); $this->beforeFilter('auth.super', array('except' => array('inviteUsers'))); - $this->beforeFilter('csrf', array('only' => array('update', 'verifyUser', 'inviteUsers'))); + $this->beforeFilter('csrf', array('only' => array('update', 'verifyUser', 'inviteUsers'))); } /** @@ -35,7 +35,7 @@ public function index(){ $admin_dashboard = new \app\locker\data\dashboards\AdminDashboard(); return View::make('partials.site.dashboard', [ - 'site' => $site, + 'site' => $site, 'list' => $list, 'stats' => $admin_dashboard->getFullStats(), 'graph_data' => $admin_dashboard->getGraphData() @@ -51,7 +51,7 @@ public function index(){ public function edit($id){ $site = $this->site->find($id); return View::make('partials.site.edit', [ - 'site' => $site, + 'site' => $site, 'settings_nav' => true ]); } @@ -122,7 +122,7 @@ public function lrs(){ $lrss = $this->lrs->index($opts); return Response::json(array_map(function ($lrs) { - $lrs->statement_total = $this->statement->count($lrs->_id); + $lrs->statement_total = $this->statement->count(['lrs_id' => $lrs->_id]); return $lrs; }, $lrss)); } @@ -148,7 +148,7 @@ public function users() { */ public function inviteUsersForm() { return View::make('partials.site.invite', [ - 'users_nav' => true, + 'users_nav' => true, 'admin_dash' => true ]); } diff --git a/app/controllers/StatementController.php b/app/controllers/StatementController.php index a6fff286bf..2d9d59a4de 100644 --- a/app/controllers/StatementController.php +++ b/app/controllers/StatementController.php @@ -1,6 +1,6 @@ <?php -use Locker\Repository\Statement\StatementRepository as StatementRepo; +use Locker\Repository\Statement\Repository as StatementRepo; use Locker\Repository\Lrs\Repository as LrsRepo; class StatementController extends BaseController { diff --git a/app/controllers/xapi/ActivityController.php b/app/controllers/xapi/ActivityController.php index 0969a37102..439a835ab7 100644 --- a/app/controllers/xapi/ActivityController.php +++ b/app/controllers/xapi/ActivityController.php @@ -1,7 +1,6 @@ <?php namespace Controllers\xAPI; use \Locker\Repository\Document\DocumentRepository as Document; -use \Locker\Repository\Activity\ActivityRepository as Activity; use Locker\Repository\Document\DocumentType as DocumentType; class ActivityController extends DocumentController { @@ -22,9 +21,8 @@ class ActivityController extends DocumentController { * @param Document $document * @param Activity $activity */ - public function __construct(Document $document, Activity $activity){ + public function __construct(Document $document){ parent::__construct($document); - $this->activity = $activity; } /** diff --git a/app/controllers/xapi/BaseController.php b/app/controllers/xapi/BaseController.php index b7e99ebe41..28bcea9ee6 100644 --- a/app/controllers/xapi/BaseController.php +++ b/app/controllers/xapi/BaseController.php @@ -38,17 +38,12 @@ public function setParameters(){ * @return mixed Result of the method. */ public function selectMethod() { - try { - switch ($this->method) { - case 'HEAD': - case 'GET': return $this->get(); - case 'PUT': return $this->update(); - case 'POST': return $this->store(); - case 'DELETE': return $this->destroy(); - } - } catch (\Exception $ex) { - $code = method_exists($ex, 'getStatusCode') ? $ex->getStatusCode() : 400; - throw new Exceptions\Exception($ex->getMessage(), $code, $ex); + switch ($this->method) { + case 'HEAD': + case 'GET': return $this->get(); + case 'PUT': return $this->update(); + case 'POST': return $this->store(); + case 'DELETE': return $this->destroy(); } } diff --git a/app/controllers/xapi/StatementController.php b/app/controllers/xapi/StatementController.php index fcfd1ecce2..70b7bc9be7 100644 --- a/app/controllers/xapi/StatementController.php +++ b/app/controllers/xapi/StatementController.php @@ -1,9 +1,9 @@ <?php namespace Controllers\xAPI; -use \Locker\Repository\Statement\StatementRepository as Statement; -use \Locker\Repository\Query\QueryRepository as Query; +use \Locker\Repository\Statement\Repository as Statement; use \Locker\Helpers\Attachments as Attachments; use \Locker\Helpers\Exceptions as Exceptions; +use \Locker\Helpers\Helpers as Helpers; class StatementController extends BaseController { @@ -22,10 +22,11 @@ class StatementController extends BaseController { * Constructs a new StatementController. * @param StatementRepository $statement */ - public function __construct(Statement $statement, Query $query) { + public function __construct(Statement $statement) { parent::__construct(); - $this->statement = $statement; - $this->query = $query; + $this->statements = $statement; + $this->index_controller = new StatementIndexController($statement); + $this->store_controller = new StatementStoreController($statement); } /** @@ -53,286 +54,49 @@ public function get() { } /** - * Deals with multipart requests. - * @return ['content' => $content, 'attachments' => $attachments]. - */ - private function getParts() { - $content = \LockerRequest::getContent(); - $contentType = \LockerRequest::header('content-type'); - $types = explode(';', $contentType, 2); - $mimeType = count($types) >= 1 ? $types[0] : $types; - - if ($mimeType == 'multipart/mixed') { - $components = Attachments::setAttachments($contentType, $content); - - // Returns 'formatting' error. - if (empty($components)) { - throw new Exceptions\Exception('There is a problem with the formatting of your submitted content.'); - } - - // Returns 'no attachment' error. - if (!isset($components['attachments'])) { - throw new Exceptions\Exception('There were no attachments.'); - } - - $content = $components['body']; - $attachments = $components['attachments']; - } else { - $attachments = ''; - } - - return [ - 'content' => $content, - 'attachments' => $attachments - ]; - } - - private function checkContentType() { - $contentType = \LockerRequest::header('Content-Type'); - if ($contentType === null) { - throw new Exceptions\Exception('Missing Content-Type.'); - } - - $validator = new \app\locker\statements\xAPIValidation(); - $validator->checkTypes('Content-Type', $contentType, 'contentType', 'headers'); - if ($validator->getStatus() !== 'passed') { - throw new Exceptions\Exception(implode(',', $validator->getErrors())); - } - } - - /** - * Stores (POSTs) a newly created statement in storage. + * Updates (PUTs) Statement with the given id. * @return Response */ - public function store() { - // Validates request. + public function update() { + // Runs filters. if ($result = $this->checkVersion()) return $result; - if ($result = $this->checkContentType()) return $result; - if (\LockerRequest::hasParam(self::STATEMENT_ID)) { - throw new Exceptions\Exception('Statement ID parameter is invalid.'); - } - - $parts = $this->getParts(); - $content = $parts['content']; - $attachments = $parts['attachments']; - - $statements = json_decode($content); - - if ($statements === null && $content != 'null' && $content != '') { - throw new Exceptions\Exception('Invalid JSON'); - } - - // Ensures that $statements is an array. - if (!is_array($statements)) { - $statements = [$statements]; - } - - // Saves $statements with $attachments. - return $this->statement->create( - $statements, - $this->lrs, - $attachments - ); + return $this->store_controller->update($this->lrs->_id); } /** * Updates (PUTs) Statement with the given id. * @return Response */ - public function update() { + public function store() { // Runs filters. if ($result = $this->checkVersion()) return $result; - if ($result = $this->checkContentType()) return $result; - - $parts = $this->getParts(); - $content = $parts['content']; - $attachments = $parts['attachments']; - - // Decodes the statement. - $statement = json_decode($content); - - if ($statement === null && $content != 'null' && $content != '') { - throw new Exceptions\Exception('Invalid JSON'); - } - - $statementId = \LockerRequest::getParam(self::STATEMENT_ID); - - // Returns a error if identifier is not present. - if (!$statementId) { - throw new Exceptions\Exception('A statement ID is required to PUT.'); - } - - // Attempts to create the statement if `statementId` is present. - $statement->id = $statementId; - $this->statement->create([$statement], $this->lrs, $attachments); - return \Response::make('', 204); + return $this->store_controller->store($this->lrs->_id); } /** * Gets an array of statements. - * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#723-getstatements - * @return StatementResult + * @return Response */ public function index() { - // Gets the filters from the request. - $filters = [ - 'agent' => $this->validatedParam('agent', 'agent'), - 'activity' => $this->validatedParam('irl', 'activity'), - 'verb' => $this->validatedParam('irl', 'verb'), - 'registration' => $this->validatedParam('uuid', 'registration'), - 'since' => $this->validatedParam('isoTimestamp', 'since'), - 'until' => $this->validatedParam('isoTimestamp', 'until'), - 'active' => $this->validatedParam('boolean', 'active', true), - 'voided' => $this->validatedParam('boolean', 'voided', false) - ]; - - - // Gets the options/flags from the request. - $options = [ - 'related_activities' => $this->validatedParam('boolean', 'related_activities', false), - 'related_agents' => $this->validatedParam('boolean', 'related_agents', false), - 'ascending' => $this->validatedParam('boolean', 'ascending', false), - 'format' => $this->validatedParam('string', 'format', 'exact'), - 'offset' => $this->validatedParam('int', 'offset', 0), - 'limit' => $this->validatedParam('int', 'limit', 100), - 'attachments' => $this->validatedParam('boolean', 'attachments', false) - ]; - - // Gets the $statements from the LRS (with the $lrsId) that match the $filters with the $options. - $statements = $this->statement->index( - $this->lrs->_id, - $filters, - $options - ); - - $total = $statements->count(); - - // Gets the statements and uses offset and limit options. - $statements->skip((int) $options['offset']); - $statements->take((int) $options['limit']); - $statements = $statements->get()->toArray(); - - // Selects an output format. - if ($options['format'] === 'ids') { - $statements = $this->statement->toIds($statements); - } else if ($options['format'] === 'canonical') { - $langs = \Request::header('Accept-Language'); - $langs = $langs !== '' ? explode(',', $langs) : []; - $statements = $this->statement->toCanonical($statements, $langs); - } - - // Returns the StatementResult object. - $statement_result = json_encode($this->makeStatementObject($statements, [ - 'total' => $total, - 'offset' => $options['offset'], - 'limit' => $options['limit'] - ])); - - if ($options['attachments'] === true) { - $boundary = 'abcABC0123\'()+_,-./:=?'; - $content_type = 'multipart/mixed; boundary='.$boundary; - $statement_result = "Content-Type:application/json\r\n\r\n".$statement_result; - $body = "--$boundary\r\n".implode( - "\r\n--$boundary\r\n", - array_merge([$statement_result], $this->statement->getAttachments($statements, $this->lrs->_id)) - )."\r\n--$boundary--"; - } else { - $content_type = 'application/json;'; - $body = $statement_result; - } - - // Creates the response. - return \Response::make($body, BaseController::OK, [ - 'Content-Type' => $content_type, - 'X-Experience-API-Consistent-Through' => $this->statement->getCurrentDate() - ]);; + return $this->index_controller->index($this->lrs->_id); } /** * Gets the statement with the given $id. - * @param UUID $id + * @param String $id Statement's UUID. * @param boolean $voided determines if the statement is voided. - * @return Statement + * @return Response */ public function show($id, $voided = false) { // Runs filters. if ($result = $this->checkVersion()) return $result; - $statement = $this->statement->show($this->lrs->_id, $id, $voided)->first(); - if ($statement) { - $dotted_statement = \Locker\Helpers\Helpers::replaceHtmlEntity( - $statement->statement - ); - return \Response::json($dotted_statement, 200); - } else { - throw new Exceptions\NotFound($id, 'Statement'); - } - } - - /** - * Constructs a response for $statements. - * @param array $statements Statements to return. - * @param array $params Filter. - * @param array $debug Log for debgging information. - * @return response - **/ - private function makeStatementObject(array $statements, array $options) { - // Merges options with default options. - $options = array_merge([ - 'total' => count($statements), - 'offset' => null, - 'limit' => null - ], $options); - - // Replaces '&46;' in keys with '.' in statements. - // http://docs.learninglocker.net/docs/statements#quirks - $statements = $statements ?: []; - $statements = \Locker\Helpers\Helpers::replaceHtmlEntity($statements); - foreach ($statements as &$s) { - $s = $s->statement; - } + $statement = $this->statements->show($id, [ + 'lrs_id' => $this->lrs->_id, + 'voided' => $voided + ]); - // Creates the statement result. - $statement_result = [ - 'more' => $this->getMoreLink($options['total'], $options['limit'], $options['offset']), - 'statements' => $statements - ]; - - return $statement_result; - } - - /** - * Constructs the "more link" for a statement response. - * @param Integer $total Number of statements that can be returned for the given request parameters. - * @param Integer $limit Number of statements to be outputted in the response. - * @param Integer $offset Number of statements being skipped. - * @return String A URL that can be used to get more statements for the given request parameters. - */ - private function getMoreLink($total, $limit, $offset) { - // Uses defaults. - $total = $total ?: 0; - $limit = $limit ?: 100; - $offset = $offset ?: 0; - - // Calculates the $next_offset. - $next_offset = $offset + $limit; - if ($total <= $next_offset) return ''; - - // Changes (when defined) or appends (when undefined) offset. - $query = \Request::getQueryString(); - $statement_route = \URL::route('xapi.statement', [], false); - $current_url = $query ? $statement_route.'?'.$query : $statement_route; - - if (strpos($query, "offset=$offset") !== false) { - return str_replace( - 'offset=' . $offset, - 'offset=' . $next_offset, - $current_url - ); - } else { - $separator = strpos($current_url, '?') !== False ? '&' : '?'; - return $current_url . $separator . 'offset=' . $next_offset; - } + return \Response::json(Helpers::replaceHtmlEntity($statement), 200); } /** diff --git a/app/controllers/xapi/StatementIndexController.php b/app/controllers/xapi/StatementIndexController.php new file mode 100644 index 0000000000..4d3f8c0439 --- /dev/null +++ b/app/controllers/xapi/StatementIndexController.php @@ -0,0 +1,138 @@ +<?php namespace Controllers\xAPI; + +use \Locker\Repository\Statement\Repository as StatementRepo; +use \Locker\Helpers\Exceptions as Exceptions; +use \Locker\Helpers\Helpers as Helpers; +use \LockerRequest as LockerRequest; + +class StatementIndexController { + + const BOUNDARY = 'abcABC0123\'()+_,-./:=?'; + const EOL = "\r\n"; + + /** + * Constructs a new StatementIndexController. + * @param StatementRepo $statement_repo + */ + public function __construct(StatementRepo $statement_repo) { + $this->statements = $statement_repo; + } + + /** + * Gets an array of statements. + * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#723-getstatements + * @param String $lrs_id + * @return Response + */ + public function index($lrs_id) { + // Gets the acceptable languages. + $langs = LockerRequest::header('Accept-Language', []); + $langs = is_array($langs) ? $langs : explode(',', $langs); + $langs = array_map(function ($lang) { + return explode(';', $lang)[0]; + }, $langs); + + // Gets the params. + $params = LockerRequest::all(); + if (isset($params['agent'])) { + $decoded_agent = json_decode($params['agent']); + if ($decoded_agent !== null) { + $params['agent'] = $decoded_agent; + } + } + + // Gets an index of the statements with the given options. + list($statements, $count, $opts) = $this->statements->index(array_merge([ + 'lrs_id' => $lrs_id, + 'langs' => $langs + ], $params)); + + // Defines the content type and body of the response. + if ($opts['attachments'] === true) { + $content_type = 'multipart/mixed; boundary='.static::BOUNDARY; + $body = $this->makeAttachmentsResult($statements, $count, $opts); + } else { + $content_type = 'application/json;'; + $body = $this->makeStatementsResult($statements, $count, $opts); + } + + // Creates the response. + return \Response::make($body, 200, [ + 'Content-Type' => $content_type, + 'X-Experience-API-Consistent-Through' => Helpers::getCurrentDate() + ]);; + } + + /** + * Makes a statements result. + * @param [\stdClass] $statements + * @param Int $count + * @param [String => Mixed] $opts + * @return \stdClass + */ + private function makeStatementsResult(array $statements, $count, array $opts) { + // Defaults to empty array of statements. + $statements = $statements ?: []; + + // Replaces '&46;' in keys with '.' in statements. + // http://docs.learninglocker.net/docs/installation#quirks + $statements = Helpers::replaceHtmlEntity($statements); + + // Creates the statement result. + $statement_result = (object) [ + 'more' => $this->getMoreLink($count, $opts['limit'], $opts['offset']), + 'statements' => $statements + ]; + + return json_encode($statement_result); + } + + /** + * Makes an attachments result. + * @param [\stdClass] $statements + * @param [String => Mixed] $opts + * @return \stdClass + */ + private function makeAttachmentsResult(array $statements, $count, array $opts) { + $content_type = 'multipart/mixed; boundary='.static::BOUNDARY; + $statement_result = "Content-Type:application/json{static::EOL}{static::EOL}".$this->makeStatementsResult( + $statements, + $count, + $opts + ); + $body = "--{static::BOUNDARY}{static::EOL}".implode( + "{static::EOL}--{static::BOUNDARY}{static::EOL}", + array_merge([$statement_result], array_map(function ($attachment) { + return ( + 'Content-Type:'.$attachment->content_type.static::EOL. + 'Content-Transfer-Encoding:binary'.static::EOL. + 'X-Experience-API-Hash:'.$attachment->hash. + static::EOL.static::EOL. + $attachment->content + ); + }, $this->statements->getAttachments($statements, $opts))) + )."{static::EOL}--{static::BOUNDARY}--"; + } + + private function getMoreLink($count, $limit, $offset) { + // Calculates the $next_offset. + $next_offset = $offset + $limit; + if ($count <= $next_offset) return ''; + + // Changes (when defined) or appends (when undefined) offset. + $query = \Request::getQueryString(); + $statement_route = \URL::route('xapi.statement', [], false); + $current_url = $query ? $statement_route.'?'.$query : $statement_route; + + if (strpos($query, "offset=$offset") !== false) { + return str_replace( + 'offset=' . $offset, + 'offset=' . $next_offset, + $current_url + ); + } else { + $separator = strpos($current_url, '?') !== False ? '&' : '?'; + return $current_url . $separator . 'offset=' . $next_offset; + } + } +} diff --git a/app/controllers/xapi/StatementStoreController.php b/app/controllers/xapi/StatementStoreController.php new file mode 100644 index 0000000000..34f94a544b --- /dev/null +++ b/app/controllers/xapi/StatementStoreController.php @@ -0,0 +1,150 @@ +<?php namespace Controllers\xAPI; + +use \Locker\Repository\Statement\Repository as StatementRepo; +use \Locker\Helpers\Attachments as Attachments; +use \Locker\Helpers\Exceptions as Exceptions; +use \Locker\Helpers\Helpers as Helpers; +use \Locker\XApi\IMT as XApiImt; +use \LockerRequest as LockerRequest; +use \Response as IlluminateResponse; + +class StatementStoreController { + + /** + * Constructs a new StatementStoreController. + * @param StatementRepo $statement_repo + */ + public function __construct(StatementRepo $statement_repo) { + $this->statements = $statement_repo; + } + + /** + * Deals with multipart requests. + * @return ['content' => $content, 'attachments' => $attachments]. + */ + private function getParts() { + $content = \LockerRequest::getContent(); + $contentType = \LockerRequest::header('content-type'); + $types = explode(';', $contentType, 2); + $mimeType = count($types) >= 1 ? $types[0] : $types; + + if ($mimeType == 'multipart/mixed') { + $components = Attachments::setAttachments($contentType, $content); + + // Returns 'formatting' error. + if (empty($components)) { + throw new Exceptions\Exception('There is a problem with the formatting of your submitted content.'); + } + + // Returns 'no attachment' error. + if (!isset($components['attachments'])) { + throw new Exceptions\Exception('There were no attachments.'); + } + + $content = $components['body']; + $attachments = $components['attachments']; + } else { + $attachments = []; + } + + return [ + 'content' => $content, + 'attachments' => $attachments + ]; + } + + /** + * Stores (POSTs) a newly created statement in storage. + * @param String $lrs_id + * @return Response + */ + public function store($lrs_id) { + if (LockerRequest::hasParam(StatementController::STATEMENT_ID)) { + throw new Exceptions\Exception('Statement ID parameter is invalid.'); + } + + return IlluminateResponse::json($this->createStatements($lrs_id), 200, Helpers::getCORSHeaders()); + } + + /** + * Updates (PUTs) Statement with the given id. + * @param String $lrs_id + * @return Response + */ + public function update($lrs_id) { + $this->createStatements($lrs_id, function ($statements) { + $statement_id = \LockerRequest::getParam(StatementController::STATEMENT_ID); + + // Returns a error if identifier is not present. + if (!$statement_id) { + throw new Exceptions\Exception('A statement ID is required to PUT.'); + } + + // Adds the ID to the statement. + $statements[0]->id = $statement_id; + return $statements; + }); + + return IlluminateResponse::make('', 204, Helpers::getCORSHeaders()); + } + + /** + * Creates statements from the content of the request. + * @param String $lrs_id + * @param Callable|null $modifier A function that modifies the statements before storing them. + * @return AssocArray Result of storing the statements. + */ + private function createStatements($lrs_id, Callable $modifier = null) { + Helpers::validateAtom(new XApiImt(LockerRequest::header('Content-Type'))); + + // Gets parts of the request. + $parts = $this->getParts(); + $content = $parts['content']; + + // Decodes $statements from $content. + $statements = json_decode($content); + if ($statements === null && $content !== '') { + throw new Exceptions\Exception('Invalid JSON'); + } else if ($statements === null) { + $statements = []; + } + + // Ensures that $statements is an array. + if (!is_array($statements)) { + $statements = [$statements]; + } + + // Runs the modifier if there is one and there are statements. + if (count($statements) > 0 && $modifier !== null) { + $statements = $modifier($statements); + } + + // Saves $statements with attachments. + return $this->statements->store( + $statements, + is_array($parts['attachments']) ? $parts['attachments'] : [], + [ + 'lrs_id' => $lrs_id, + 'authority' => $this->getAuthority() + ] + ); + } + + private function getAuthority() { + $client = (new \Client) + ->where('api.basic_key', \LockerRequest::getUser()) + ->where('api.basic_secret', \LockerRequest::getPassword()) + ->first(); + + if ($client != null && isset($client['authority'])) { + return json_decode(json_encode($client['authority'])); + } else { + $site = \Site::first(); + return (object) [ + 'name' => $site->name, + 'mbox' => 'mailto:' . $site->email, + 'objectType' => 'Agent' + ]; + } + } +} diff --git a/app/locker/helpers/Attachments.php b/app/locker/helpers/Attachments.php index bfb331b633..394ab6e841 100644 --- a/app/locker/helpers/Attachments.php +++ b/app/locker/helpers/Attachments.php @@ -1,5 +1,6 @@ <?php namespace Locker\Helpers; - +use \Locker\Helpers\Exceptions as Exceptions; +use \Locker\Repository\Document\FileTypes as FileTypes; class Attachments { @@ -7,111 +8,99 @@ class Attachments { * Get statements and attachments from submitted content * * @param $content_type - * @param $incoming_statement + * @param $content * * @return array * **/ - static function setAttachments( $content_type, $incoming_statement ){ - - $return = array(); - $sha_hashes = array(); - - //grab boundary from content_type header - @todo not sure which way is better? - preg_match('/boundary=(.*)$/', $content_type, $matches); - //if no boundary, abort - if( !isset($matches[1]) ){ - \App::abort(400, 'You need to set a boundary if submitting attachments.'); - } - $boundary = '--' . $matches[1]; + static function setAttachments($content_type, $content) { + $return = ['body' => '', 'attachments' => []]; + $sha_hashes = []; + $boundary = static::getBoundary($content_type); // Fetch each part of the multipart document - $parts = array_slice(explode($boundary, $incoming_statement), 1); - $data = array(); + $parts = array_slice(explode($boundary, $content), 1); $raw_headers = $body = ''; - //loop through all parts on the body foreach ($parts as $count => $part) { - // At the end of the file, break + // Stops at the end of the file. if ($part == "--") break; // Determines the delimiter. - $delim = "\n"; - if (strpos($part, "\r".$delim) !== false) $delim = "\r".$delim; + $delim = strpos($part, "\r\n") !== false ? "\r\n" : "\n"; // Separate body contents from headers $part = ltrim($part, $delim); list($raw_headers, $body) = explode($delim.$delim, $part, 2); + $headers = static::getHeaders($raw_headers, $delim); - // Parse headers and separate so we can access - $raw_headers = explode($delim, $raw_headers); - $headers = array(); - foreach ($raw_headers as $header) { - list($name, $value) = explode(':', $header); - $headers[strtolower($name)] = ltrim($value, ' '); - } - - //the first part must be statements - if( $count == 0 ){ - //this is part one, which must be statements - if( $headers['content-type'] !== 'application/json' ){ - \App::abort(400, 'Statements must make up the first part of the body.'); - } - - //get sha2 hash from each statement - $set_body = json_decode($body, true); - if( is_array(json_decode($body)) ){ - foreach($set_body as $a){ - foreach($a['attachments'] as $attach){ - $sha_hashes[] = $attach['sha2']; - } - } - }else{ - foreach($set_body['attachments'] as $attach){ - $sha_hashes[] = $attach['sha2']; - } + if ($count == 0) { + if (!isset($headers['content-type']) || $headers['content-type'] !== 'application/json') { + throw new Exceptions\Exception('Statements must make up the first part of the body.'); } - //set body which will = statements $return['body'] = $body; + } else { + static::validateHeaders($headers, $sha_hashes); + $return['attachments'][$count] = (object) [ + 'hash' => $headers['x-experience-api-hash'], + 'content_type' => $headers['content-type'], + 'content' => $body + ]; + } + } - }else{ - - //get the attachment type (Should this be required? @todo) - if( !isset($headers['content-type']) ){ - \App::abort(400, 'You need to set a content type for your attachments.'); - } - - //get the correct ext if valid - $fileTypes = new \Locker\Repository\Document\FileTypes; - $ext = array_search( $headers['content-type'], $fileTypes::getMap() ); - if( $ext === false ){ - \App::abort(400, 'This file type cannot be supported'); - } - - //if content-transfer-encoding is not binary, reject attachment @todo - // if( !isset($headers['content-transfer-encoding']) || $headers['content-transfer-encoding'] !== 'binary' ){ - // \App::abort(400, 'This is the wrong encoding type'); - // } - - //check X-Experience-API-Hash is set, otherwise reject @todo - if( !isset($headers['x-experience-api-hash']) || $headers['x-experience-api-hash'] == ''){ - \App::abort(400, 'Attachments require an api hash.'); - } - - //check x-experience-api-hash is contained within a statement - if( !in_array($headers['x-experience-api-hash'], $sha_hashes)){ - \App::abort(400, 'Attachments need to contain x-experience-api-hash that is declared in statement.'); - } + return $return; + } - $return['attachments'][$count] = $part; + /** + * Gets the boundary from the content type. + * @param String $raw_headers + * @param String $delim + * @return [String => Mixed] + */ + private static function getHeaders($raw_headers, $delim) { + $raw_headers = explode($delim, $raw_headers); + $headers = []; + foreach ($raw_headers as $header) { + list($name, $value) = explode(':', $header); + $headers[strtolower($name)] = ltrim($value, ' '); + } + return $headers; + } - } + /** + * Gets the boundary from the content type. + * @param String $content_type + * @return String + */ + private static function getBoundary($content_type) { + preg_match('/boundary=(.*)$/', $content_type, $matches); + if (!isset($matches[1])) throw new Exceptions\Exception( + 'You need to set a boundary if submitting attachments.' + ); + return '--'.$matches[1]; + } + /** + * Validates the attachment headers. + * @param [String => Mixed] $headers + */ + private static function validateHeaders(array $headers, array $sha_hashes) { + if (!isset($headers['content-type'])) { + throw new Exceptions\Exception('You need to set a content type for your attachments.'); } - return $return; + //get the correct ext if valid + $ext = array_search($headers['content-type'], FileTypes::getMap()); + if ($ext === false) { + throw new Exceptions\Exception('This file type cannot be supported'); + } + //check X-Experience-API-Hash is set, otherwise reject @todo + if (!isset($headers['x-experience-api-hash']) || $headers['x-experience-api-hash'] == '') { + throw new Exceptions\Exception('Attachments require an api hash.'); + } } } diff --git a/app/locker/helpers/Helpers.php b/app/locker/helpers/Helpers.php index c7e4203eae..f52f5485b9 100644 --- a/app/locker/helpers/Helpers.php +++ b/app/locker/helpers/Helpers.php @@ -127,10 +127,10 @@ static function getEnvVar($var) { $defaults = include base_path() . '/.env.php'; $value = $defaults[$var]; } - + return $value; } - + /** * Determines which identifier is currently in use in the given actor. * @param \stdClass $actor. @@ -154,7 +154,49 @@ static function validateAtom(XAPIAtom $atom, $trace = null) { if (count($errors) > 0) { throw new Exceptions\Validation(array_map(function (XAPIError $error) use ($trace) { return (string) ($trace === null ? $error : $error->addTrace($trace)); - }, $errors))); + }, $errors)); } } + + /** + * Makes a new UUID. + * @return String Generated UUID. + */ + static function makeUUID() { + $remote_addr = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : 'LL'; + mt_srand(crc32(serialize([microtime(true), $remote_addr, 'ETC']))); + + return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + } + + /** + * Gets the current date and time in ISO format using the current timezone. + * @return String Current ISO date and time. + */ + static function getCurrentDate() { + $current_date = \DateTime::createFromFormat('U.u', sprintf('%.4f', microtime(true))); + $current_date->setTimezone(new \DateTimeZone(\Config::get('app.timezone'))); + return $current_date->format('Y-m-d\TH:i:s.uP'); + } + + /** + * Gets the CORS headers. + * @return [String => Mixed] CORS headers. + */ + static function getCORSHeaders() { + return [ + 'Access-Control-Allow-Origin' => \Request::root(), + 'Access-Control-Allow-Methods' => 'GET, PUT, POST, DELETE, OPTIONS', + 'Access-Control-Allow-Headers' => 'Origin, Content-Type, Accept, Authorization, X-Requested-With, X-Experience-API-Version, X-Experience-API-Consistent-Through, Updated', + 'Access-Control-Allow-Credentials' => 'true', + 'X-Experience-API-Consistent-Through' => Helpers::getCurrentDate(), + 'X-Experience-API-Version' => '1.0.1' + ]; + } } diff --git a/app/locker/repository/Activity/ActivityRepository.php b/app/locker/repository/Activity/ActivityRepository.php deleted file mode 100644 index cf276d5165..0000000000 --- a/app/locker/repository/Activity/ActivityRepository.php +++ /dev/null @@ -1,9 +0,0 @@ -<?php namespace Locker\Repository\Activity; - -interface ActivityRepository { - - public function saveActivity( $activity_id, $activity_def ); - - public function getActivity( $activity_id ); - -} \ No newline at end of file diff --git a/app/locker/repository/Activity/EloquentActivityRepository.php b/app/locker/repository/Activity/EloquentActivityRepository.php deleted file mode 100644 index 150702b890..0000000000 --- a/app/locker/repository/Activity/EloquentActivityRepository.php +++ /dev/null @@ -1,49 +0,0 @@ -<?php namespace Locker\Repository\Activity; - -use Activity; - -class EloquentActivityRepository implements ActivityRepository { - - /** - * Activity - */ - protected $activity; - - /** - * Construct - * - * @param Activity $activity - */ - public function __construct( Activity $activity ){ - - $this->activity = $activity; - - } - - /** - * This is a temp solution, we need something better depending - * on authority to update activity stored. - * - **/ - public function saveActivity( $activity_id, $activity_def ){ - - $exists = \Activity::find( $activity_id ); - - //if the object activity exists, remove and update with recent - if( $exists ){ - \Activity::where('_id', $activity_id)->delete(); - } - - //save record - \Activity::insert( - array('_id' => $activity_id, - 'definition' => $activity_def) - ); - - } - - public function getActivity( $activity_id ){ - return \Activity::where('_id', $activity_id)->first(); - } - -} \ No newline at end of file diff --git a/app/locker/repository/RepositoryServiceProvider.php b/app/locker/repository/RepositoryServiceProvider.php index 1cfbe5638b..6adfd48a5b 100644 --- a/app/locker/repository/RepositoryServiceProvider.php +++ b/app/locker/repository/RepositoryServiceProvider.php @@ -11,8 +11,8 @@ public function register(){ 'Locker\Repository\User\EloquentUserRepository' ); $this->app->bind( - 'Locker\Repository\Statement\StatementRepository', - 'Locker\Repository\Statement\EloquentStatementRepository' + 'Locker\Repository\Statement\Repository', + 'Locker\Repository\Statement\EloquentRepository' ); $this->app->bind( 'Locker\Repository\Lrs\Repository', @@ -34,10 +34,6 @@ public function register(){ 'Locker\Repository\Document\DocumentRepository', 'Locker\Repository\Document\EloquentDocumentRepository' ); - $this->app->bind( - 'Locker\Repository\Activity\ActivityRepository', - 'Locker\Repository\Activity\EloquentActivityRepository' - ); $this->app->bind( 'Locker\Repository\OAuthApp\OAuthAppRepository', 'Locker\Repository\OAuthApp\EloquentOAuthAppRepository' diff --git a/app/locker/repository/Statement/EloquentIndexer.php b/app/locker/repository/Statement/EloquentIndexer.php index c97d1d33fc..a6a3f4a222 100644 --- a/app/locker/repository/Statement/EloquentIndexer.php +++ b/app/locker/repository/Statement/EloquentIndexer.php @@ -1,68 +1,202 @@ <?php namespace Locker\Repository\Statement; use \Locker\Helpers\Exceptions as Exceptions; +use \Locker\Helpers\Helpers as Helpers; use \Locker\XApi\Helpers as XApiHelpers; use \Jenssegers\Mongodb\Eloquent\Builder as Builder; use \Illuminate\Database\Eloquent\Model as Model; -class EloquentIndexer extends EloquentReader { +interface IndexerInterface { + public function index(IndexOptions $opts); + public function format(Builder $builder, IndexOptions $opts); + public function count(Builder $builder, IndexOptions $opts); +} + +class EloquentIndexer extends EloquentReader implements IndexerInterface { + + protected $formatter; + + public function __construct() { + $this->formatter = new Formatter(); + } /** * Gets all of the available models with the options. - * @param [String => Mixed] $opts + * @param IndexOptions $opts * @return [Model] */ - public function index(array $opts) { - $opts = new IndexOptions($opts); - return $this->where($opts); - } - - protected function where(IndexOptions $opts) { - $builder = $this->where($opts->options); + public function index(IndexOptions $opts) { + $builder = $this->where($opts); return $this->constructFilterOpts($builder, $opts, [ - 'agent' => function ($agent, IndexOptions $opts) { - Helpers::validateAtom(\Locker\XApi\Agent::createFromJSON($agent)); - return $this->matchAgent($agent, $options); + 'agent' => function ($value, $builder, IndexOptions $opts) { + return $this->matchAgent($value, $builder, $opts); + }, + 'activity' => function ($value, $builder, IndexOptions $opts) { + return $this->matchActivity($value, $builder, $opts); + }, + 'verb' => function ($value, $builder, IndexOptions $opts) { + return $this->addWhere($builder, 'verb.id', $value); + }, + 'registration' => function ($value, $builder, IndexOptions $opts) { + return $this->addWhere($builder, 'context.registration', $value); + }, + 'since' => function ($value, $builder, IndexOptions $opts) { + return $this->addWhere($builder, 'stored', $value, '>'); + }, + 'until' => function ($value, $builder, IndexOptions $opts) { + return $this->addWhere($builder, 'stored', $value, '<='); + }, + 'active' => function ($value, $builder, IndexOptions $opts) { + return $builder->where('active', $value); + }, + 'voided' => function ($value, $builder, IndexOptions $opts) { + return $builder->where('voided', $value); } ]); } + /** + * Adds where to builder. + * @param Builder $builder + * @param String $key + * @param Mixed $value + * @return Builder + */ + private function addWhere(Builder $builder, $key, $value, $op = '=') { + return $builder->where(function ($query) use ($key, $value, $op) { + return $query + ->orWhere('statement.'.$key, $op, $value) + ->orWhere('refs.'.$key, $op, $value); + }); + } + + /** + * Adds wheres to builder. + * @param Builder $builder + * @param [String] $keys + * @param Mixed $value + * @return Builder + */ + private function addWheres(Builder $builder, array $keys, $value) { + return $builder->where(function ($query) use ($keys, $value) { + foreach ($keys as $key) { + $query->orWhere(function ($query) use ($key, $value) { + return $query + ->orWhere('statement.'.$key, $value) + ->orWhere('refs.'.$key, $value); + }); + } + return $query; + }); + } + + /** + * Extends a given Builder using the given options and option builders. + * @param Builder $builder + * @param IndexOptions $opts. + * @param [String => Callable] $builders Option builders. + * @return Builder + */ + private function constructFilterOpts(Builder $builder, IndexOptions $opts, array $builders) { + foreach ($builders as $opt => $opt_builder) { + $opt_value = $opts->getOpt($opt); + $builder = $opt_value === null ? $builder : $opt_builder($opt_value, $builder, $opts); + } + return $builder; + } + /** * Formats statements. * @param Builder $builder - * @param [String => Mixed] $opts + * @param IndexOptions $opts * @return [Model] Formatted statements. */ - public function format(Builder $builder, array $opts) { - if ($opts['format'] === 'exact') { - $formatter = function ($model, $opts) { - return $model; + public function format(Builder $builder, IndexOptions $opts) { + // Determines the formatter to be used. + $format = $opts->getOpt('format'); + if ($format === 'exact') { + $formatter = function ($statement, $opts) { + return $statement; }; - } else if ($opts['format'] === 'ids') { - $formatter = function ($model, $opts) { - return $this->formatter->identityStatement($model); + } else if ($format === 'ids') { + $formatter = function ($statement, $opts) { + return $this->formatter->identityStatement($statement); }; - } else if ($opts['format'] === 'canonical') { - $formatter = function ($model, $opts) { - return $this->formatter->canonicalStatement($model, $opts['langs']); + } else if ($format === 'canonical') { + $formatter = function ($statement, $opts) { + return $this->formatter->canonicalStatement($statement, $opts->getOpt('langs')); }; } else { - throw new Exceptions\Exception("`$opts['format']` is not a valid format."); + throw new Exceptions\Exception("`$format` is not a valid format."); } - return $builder->get()->each(function (Model $model) use ($opts) { - return $formatter($model, $opts); + // Returns the models. + return json_decode($builder + ->orderBy('statement.stored', $opts->getOpt('ascending') ? 'ASC' : 'DESC') + ->skip($opts->getOpt('offset')) + ->take($opts->getOpt('limit')) + ->get() + ->map(function (Model $model) use ($opts, $formatter) { + return $formatter($this->formatModel($model), $opts); + })); + } + + /** + * Constructs a Mongo match using the given agent and options. + * @param String $agent Agent to be matched. + * @param IndexOptions $opts Index options. + * @return Builder + */ + private function matchAgent($agent, Builder $builder, IndexOptions $opts) { + $id_key = Helpers::getAgentIdentifier($agent); + $id_val = $agent->{$id_key}; + + return $builder->where(function ($query) use ($id_key, $id_val, $builder, $opts) { + $keys = ["actor.$id_key", "actor.members.$id_key", "object.$id_key"]; + + if ($opts->getOpt('related_agents') === true) { + $keys = array_merge($keys, [ + "authority.$id_key", + "context.instructor.$id_key", + "context.team.$id_key" + ]); + } + + $query = $this->addWheres($builder, $keys, $id_val); + }); + } + + /** + * Constructs a Mongo match using the given activity and options. + * @param String $activity Activity to be matched. + * @param IndexOptions $opts Index options. + * @return Builder + */ + private function matchActivity($activity, Builder $builder, IndexOptions $opts) { + return $builder->where(function ($query) use ($activity, $builder, $opts) { + $keys = ['object.id']; + + if ($opts->getOpt('related_activities') === true) { + $keys = array_merge($keys, [ + 'context.contextActivities.parent.id', + 'context.contextActivities.grouping.id', + 'context.contextActivities.category.id', + 'context.contextActivities.other.id' + ]); + } + + $query = $this->addWheres($builder, $keys, $activity); }); } /** * Counts statements. * @param Builder $builder - * @param [String => Mixed] $opts + * @param IndexOptions $opts * @return Int Number of statements in Builder. */ - public function count(Builder $builder, array $opts) { + public function count(Builder $builder, IndexOptions $opts) { return $builder->count(); } } diff --git a/app/locker/repository/Statement/EloquentInserter.php b/app/locker/repository/Statement/EloquentInserter.php new file mode 100644 index 0000000000..bd4084bc06 --- /dev/null +++ b/app/locker/repository/Statement/EloquentInserter.php @@ -0,0 +1,97 @@ +<?php namespace Locker\Repository\Statement; + +use \Locker\Helpers\Exceptions as Exceptions; +use \Locker\Helpers\Helpers as Helpers; + +interface Inserter { + public function insert(array $statements, StoreOptions $opts); +} + +class EloquentInserter extends EloquentReader implements Inserter { + + /** + * Inserts statements with the given options. + * @param [\stdClass] $statements + * @param StoreOptions $opts + * @throws Exceptions\Conflict + */ + public function insert(array $statements, StoreOptions $opts) { + $models = array_map(function (\stdClass $statement) use ($opts) { + $this->checkForConflict($statement, $opts); + return $this->constructModel($statement, $opts); + }, $statements); + + return $this->insertModels($models, $opts); + } + + /** + * Checks for a duplicate statement with the given options. + * @param [\stdClass] $statements + * @param StoreOptions $opts + * @throws Exceptions\Conflict + */ + private function checkForConflict(\stdClass $statement, StoreOptions $opts) { + $duplicate = $this->where($opts) + ->where('statement.id', $statement->id) + ->where('active', true) + ->first(); + + if ($duplicate === null) return; + $this->compareForConflict($statement, $this->formatModel($duplicate)); + } + + /** + * Compares two statements. + * Throws Exceptions\Conflict if the two statements match. + * @param \stdClass $statement_x + * @param \stdClass $statement_y + * @throws Exceptions\Conflict + */ + private function compareForConflict(\stdClass $statement_x, \stdClass $statement_y) { + $matchable_x = $this->matchableStatement($statement_x); + $matchable_y = $this->matchableStatement($statement_y); + if ($matchable_x != $matchable_y) { + throw new Exceptions\Conflict( + "Conflicts\r\n`{json_encode($statement_x)}`\r\n`{json_encode($statement_y)}`." + ); + }; + } + + /** + * Decodes the encoded statement. + * Removes properties not necessary for matching. + * @param \stdClass $statement + * @return \stdClass $statement + */ + private function matchableStatement(\stdClass $statement) { + $statement = json_decode(json_encode($statement)); + unset($statement->stored); + unset($statement->authority); + return $statement; + } + + /** + * Constructs a model from the given statement and options. + * @param \stdClass $statement + * @param StoreOptions $opts + * @return [String => Mixed] $model + */ + private function constructModel(\stdClass $statement, StoreOptions $opts) { + return [ + 'lrs' => ['_id' => $opts->getOpt('lrs_id')], + 'statement' => Helpers::replaceFullStop(json_decode(json_encode($statement), true)), + 'active' => false, + 'voided' => false, + 'timestamp' => new \MongoDate(strtotime($statement->timestamp)) + ]; + } + + /** + * Inserts models with the given options. + * @param [[String => Mixed]] $models + * @param StoreOptions $opts + */ + private function insertModels(array $models, StoreOptions $opts) { + return $this->where($opts)->insert($models); + } +} diff --git a/app/locker/repository/Statement/EloquentLinker.php b/app/locker/repository/Statement/EloquentLinker.php new file mode 100644 index 0000000000..29efe3c866 --- /dev/null +++ b/app/locker/repository/Statement/EloquentLinker.php @@ -0,0 +1,157 @@ +<?php namespace Locker\Repository\Statement; + +use \Illuminate\Database\Eloquent\Model as Model; +use \Illuminate\Database\Eloquent\Collection as Collection; +use \Locker\Helpers\Helpers as Helpers; + +interface LinkerInterface { + public function updateReferences(array $statements, StoreOptions $opts); +} + +class EloquentLinker extends EloquentReader implements LinkerInterface { + + private $to_update = []; + private $downed = null; + + /** + * Updates statement references. + * @param [\stdClass] $statements + * @param StoreOptions $opts + */ + public function updateReferences(array $statements, StoreOptions $opts) { + $this->voider = strpos(json_encode($statements), 'voided') !== false; + $this->downed = new Collection(); + $this->to_update = array_map(function (\stdClass $statement) use ($opts) { + return $this->getModel($statement->id, $opts); + }, $statements); + + while (count($this->to_update) > 0) { + $this->upLink($this->to_update[0], [], $opts); + } + } + + /** + * Determines if a statement is a referencing statement. + * @param \stdClass $statement + * @return Boolean + */ + protected function isReferencing(\stdClass $statement) { + return ( + isset($statement->object->objectType) && + $statement->object->objectType === 'StatementRef' + ); + } + + /** + * Gets the statement as an associative array from the database. + * @param String $statement_id Statement's UUID. + * @param StoreOptions $opts + * @return [Model] + */ + protected function getModel($statement_id, StoreOptions $opts) { + $model = $this->where($opts) + ->where('statement.id', $statement_id) + ->first(); + + return $model; + } + + /** + * Goes up the reference chain until it reaches the top then goes down setting references. + * @param Model $model + * @param [String] $visited IDs of statements visisted in the current chain (avoids infinite loop). + * @param StoreOptions $opts + * @return [Model] + */ + private function upLink(Model $model, array $visited, StoreOptions $opts) { + $statement = $this->formatModel($model); + if (in_array($statement->id, $visited)) return []; + $visited[] = $statement->id; + $up_refs = $this->upRefs($statement, $opts); + if ($up_refs->count() > 0) { + return $up_refs->each(function ($up_ref) use ($opts, $visited) { + if ($this->downed->has($up_ref->_id)) return; + $this->downed->merge($this->upLink($up_ref, $visited, $opts)); + return $up_ref; + })->values(); + } else { + return $this->downLink($model, [], $opts); + } + } + + /** + * Goes down the reference chain setting references (refs). + * @param Model $model + * @param [String] $visited IDs of statements visisted in the current chain (avoids infinite loop). + * @param StoreOptions $opts + * @return [Model] + */ + private function downLink(Model $model, array $visited, StoreOptions $opts) { + $statement = $this->formatModel($model); + if (in_array($model, $visited)) { + return array_slice($visited, array_search($model, $visited)); + } + $visited[] = $model; + $down_ref = $this->downRef($statement, $opts); + if ($down_ref !== null) { + $refs = $this->downLink($down_ref, $visited, $opts); + $this->setRefs($statement, $refs, $opts); + $this->unQueue($model); + return array_merge([$model], $refs); + } else { + $this->unQueue($model); + return [$model]; + } + } + + /** + * Gets the statements referencing the given statement. + * @param \stdClass $statement + * @param StoreOptions $opts + * @return [\stdClass] + */ + private function upRefs(\stdClass $statement, StoreOptions $opts) { + return $this->where($opts) + ->where('statement.object.id', $statement->id) + ->where('statement.object.objectType', 'StatementRef') + ->get(); + } + + /** + * Gets the statement referred to by the given statement. + * @param \stdClass $statement + * @param StoreOptions $opts + * @return Model + */ + private function downRef(\stdClass $statement, StoreOptions $opts) { + if (!$this->isReferencing($statement)) return null; + return $this->getModel($statement->object->id, $opts); + } + + /** + * Updates the refs for the given statement. + * @param \stdClass $statement + * @param [\stdClass] $refs Statements that are referenced by the given statement. + * @param StoreOptions $opts + */ + private function setRefs(\stdClass $statement, array $refs, StoreOptions $opts) { + $this->where($opts) + ->where('statement.id', $statement->id) + ->update([ + 'refs' => array_map(function ($ref) { + return Helpers::replaceFullStop(json_decode(json_encode($ref->statement), true)); + }, $refs) + ]); + } + + /** + * Unqueues the statement so that it doesn't get relinked. + * @param Model $model + */ + private function unQueue(Model $model) { + $updated_index = array_search($model, $this->to_update); + if ($updated_index !== false) { + array_splice($this->to_update, $updated_index, 1); + } + } +} diff --git a/app/locker/repository/Statement/EloquentReader.php b/app/locker/repository/Statement/EloquentReader.php index 5a8be9675d..5d9742043f 100644 --- a/app/locker/repository/Statement/EloquentReader.php +++ b/app/locker/repository/Statement/EloquentReader.php @@ -1,5 +1,8 @@ <?php namespace Locker\Repository\Statement; +use \Illuminate\Database\Eloquent\Model as Model; +use \Locker\Helpers\Helpers as Helpers; + abstract class EloquentReader { protected $model = '\Statement'; @@ -8,7 +11,16 @@ abstract class EloquentReader { * @param [String => Mixed] $opts * @return \Jenssegers\Mongodb\Eloquent\Builder */ - protected function where(array $opts) { - return (new $this->model)->where('lrs', $opts['lrs_id']); + protected function where(Options $opts) { + return (new $this->model)->where('lrs._id', $opts->getOpt('lrs_id')); + } + + /** + * Gets the statement from the model as an Object. + * @param Model $model + * @return \stdClass + */ + protected function formatModel(Model $model) { + return Helpers::replaceHTMLEntity($model->statement); } } diff --git a/app/locker/repository/Statement/EloquentRepository.php b/app/locker/repository/Statement/EloquentRepository.php new file mode 100644 index 0000000000..39868a4f17 --- /dev/null +++ b/app/locker/repository/Statement/EloquentRepository.php @@ -0,0 +1,79 @@ +<?php namespace Locker\Repository\Statement; + +interface Repository { + public function store(array $statements, array $attachments, array $opts); + public function index(array $opts); + public function show($id, array $opts); + public function getAttachments(array $statements, array $opts); + public function count(array $opts); +} + +class EloquentRepository implements Repository { + + /** + * Constructs a new EloquentRepository for statements. + */ + public function __construct() { + $this->storer = new EloquentStorer(); + $this->indexer = new EloquentIndexer(); + $this->shower = new EloquentShower(); + $this->attacher = new FileAttacher(); + } + + /** + * Stores statements and attachments with the given options. + * @param [\stdClass] $statements + * @param [\stdClass] $attachments + * @param [String => Mixed] $opts + * @return [String] UUIDs of the stored statements. + */ + public function store(array $statements, array $attachments, array $opts) { + return $this->storer->store($statements, $attachments, new StoreOptions($opts)); + } + + /** + * Gets all of the available models with the options. + * @param [String => Mixed] $opts + * @return [[\stdClass], Int, [String => Mixed]] Array containing the statements, count, and opts. + */ + public function index(array $opts) { + $opts = new IndexOptions($opts); + $builder = $this->indexer->index($opts); + return [ + $this->indexer->format($builder, $opts), + $this->indexer->count($builder, $opts), + $opts->options + ]; + } + + /** + * Gets the model with the given ID and options. + * @param String $id ID to match. + * @param [String => Mixed] $opts + * @return [\stdClass] + */ + public function show($id, array $opts) { + return $this->shower->show($id, new ShowOptions($opts)); + } + + /** + * Gets the attachments for the given statements and options. + * @param [\stdClass] $statements + * @param [String => Mixed] $opts + * @return [\stdClass] + */ + public function getAttachments(array $statements, array $opts) { + return $this->attacher->index($statements, new IndexOptions($opts)); + } + + /** + * Gets a count of all the statements available with the given options. + * @param [String => Mixed] $opts + * @return Int + */ + public function count(array $opts) { + $opts = new IndexOptions($opts); + $builder = $this->indexer->index($opts); + return $this->indexer->count($builder, $opts); + } +} diff --git a/app/locker/repository/Statement/EloquentShower.php b/app/locker/repository/Statement/EloquentShower.php new file mode 100644 index 0000000000..4086044e5e --- /dev/null +++ b/app/locker/repository/Statement/EloquentShower.php @@ -0,0 +1,30 @@ +<?php namespace Locker\Repository\Statement; + +use \Locker\Helpers\Exceptions as Exceptions; +use \Locker\XApi\Statement as XApiStatement; +use \Illuminate\Database\Eloquent\Model as Model; + +interface ShowerInterface { + public function show($id, ShowOptions $opts); +} + +class EloquentShower extends EloquentReader implements ShowerInterface { + + /** + * Gets the model with the given ID and options. + * @param String $id ID to match. + * @param ShowOptions $opts + * @return Model + */ + public function show($id, ShowOptions $opts) { + $model = $this->where($opts) + ->where('statement.id', $id) + ->where('voided', $opts->getOpt('voided')) + ->where('active', $opts->getOpt('active')) + ->first(); + + if ($model === null) throw new Exceptions\NotFound($id, $this->model); + + return $this->formatModel($model); + } +} diff --git a/app/locker/repository/Statement/EloquentStatementRepository.php b/app/locker/repository/Statement/EloquentStatementRepository.php deleted file mode 100644 index 4dd4a49706..0000000000 --- a/app/locker/repository/Statement/EloquentStatementRepository.php +++ /dev/null @@ -1,970 +0,0 @@ -<?php namespace Locker\Repository\Statement; - -use \DateTime; -use \Statement; -use \Locker\Repository\Activity\ActivityRepository as Activity; -use \Locker\Repository\Query\QueryRepository as Query; -use \Locker\Repository\Document\FileTypes; -use \Illuminate\Database\Eloquent\Builder as Builder; -use \Locker\Helpers\Exceptions as Exceptions; -use \Locker\Helpers\Helpers as Helpers; - -class EloquentStatementRepository implements StatementRepository { - - // Defines properties to be set to construtor parameters. - protected $statement, $activity, $query; - protected $sent_ids = array(); - - // Number of statements to return by default. - const DEFAULT_LIMIT = 100; - - /** - * Constructs a new EloquentStatementRepository. - * @param Statement $statement - * @param Activity $activity - * @param Query $query - */ - public function __construct(Statement $statement, Activity $activity, Query $query) { - $this->statement = $statement; - $this->activity = $activity; - $this->query = $query; - } - - /** - * Gets the statement with the given $id from the lrs (with the $lrsId). - * @param UUID $lrsId - * @param UUID $id - * @param boolean $voided determines if the statement is voided. - * @param boolean $active determines if the statement is active. - * @return Builder - */ - public function show($lrsId, $id, $voided = false, $active = true) { - return $this->query->where($lrsId, [ - ['statement.id', '=', $id], - ['voided', '=', $voided], - ['active', '=', $active] - ]); - } - - /** - * Gets statements from the lrs (with the $lrsId) that match the $filters. - * @param UUID $lrsId - * @param [StatementFilter] $filters - * @param [StatementFilter] $options - * @return Builder - */ - public function index($lrsId, array $filters, array $options) { - $where = []; - - // Defaults filters. - $filters = array_merge([ - 'agent' => null, - 'activity' => null, - 'verb' => null, - 'registration' => null, - 'since' => null, - 'until' => null, - 'active' => true, - 'voided' => false - ], $filters); - - // Defaults options. - $options = array_merge([ - 'related_activities' => false, - 'related_agents' => false, - 'ascending' => false, - 'format' => 'exact', - 'offset' => 0, - 'limit' => self::DEFAULT_LIMIT - ], $options); - - // Checks params. - if ($options['offset'] < 0) throw new Exceptions\Exception('`offset` must be a positive interger.'); - if ($options['limit'] < 0) throw new Exceptions\Exception('`limit` must be a positive interger.'); - if (!in_array($options['format'], ['ids', 'exact', 'canonical'])) { - throw new Exceptions\Exception('`format` must be `ids`, `exact` or `canonical`.'); - } - - // Filters by date. - if (isset($filters['since'])) $where[] = ['statement.stored', '>', $filters['since']]; - if (isset($filters['until'])) $where[] = ['statement.stored', '<', $filters['until']]; - if (isset($filters['active'])) $where[] = ['active', '=', $filters['active']]; - if (isset($filters['voided'])) $where[] = ['voided', '=', $filters['voided']]; - $statements = $this->query->where($lrsId, $where); - - // Adds filters that don't have options. - $statements = $this->addFilter($statements, $filters['verb'], [ - 'statement.verb.id' - ]); - $statements = $this->addFilter($statements, $filters['registration'], [ - 'statement.context.registration' - ]); - - // Filters by activity. - $statements = $this->addOptionFilter($statements, $filters['activity'], $options['related_activities'], [ - 'statement.object.id' - ], [ - 'statement.context.contextActivities.parent.id', - 'statement.context.contextActivities.grouping.id', - 'statement.context.contextActivities.category.id', - 'statement.context.contextActivities.other.id' - ]); - - // Filters by agent. - $agent = $filters['agent']; - $identifier = $this->getIdentifier($agent); - if (isset($agent) && !is_array($agent)) throw new Exceptions\Exception('Invalid agent'); - $agent = isset($agent) && isset($agent[$identifier]) ? $agent[$identifier] : null; - - // Fixes https://github.com/LearningLocker/learninglocker/issues/519. - if ($identifier === 'account') { - $statements = $this->addOptionFilter($statements, $agent['name'], $options['related_agents'], [ - 'statement.actor.'.$identifier.'.name', - 'statement.object.'.$identifier.'.name' - ], [ - 'statement.authority.'.$identifier.'.name', - 'statement.context.instructor.'.$identifier.'.name', - 'statement.context.team.'.$identifier.'.name' - ]); - $statements = $this->addOptionFilter($statements, $agent['homePage'], $options['related_agents'], [ - 'statement.actor.'.$identifier.'.homePage', - 'statement.object.'.$identifier.'.homePage' - ], [ - 'statement.authority.'.$identifier.'.homePage', - 'statement.context.instructor.'.$identifier.'.homePage', - 'statement.context.team.'.$identifier.'.homePage' - ]); - } else { - $statements = $this->addOptionFilter($statements, $agent, $options['related_agents'], [ - 'statement.actor.'.$identifier, - 'statement.object.'.$identifier - ], [ - 'statement.authority.'.$identifier, - 'statement.context.instructor.'.$identifier, - 'statement.context.team.'.$identifier - ]); - } - - // Uses ordering. - if (isset($options['ascending']) && $options['ascending'] === true) { - $statements = $statements->orderBy('statement.stored', 'ASC'); - } else { - $statements = $statements->orderBy('statement.stored', 'DESC'); - } - - return $statements; - } - - /** - * Gets the identifier of the agent. - * @param array $agent - * @return string identifier (mbox, openid, account). - */ - private function getIdentifier($agent) { - if (isset($agent)) { - if (isset($agent['mbox'])) return 'mbox'; - if (isset($agent['openid'])) return 'openid'; - if (isset($agent['account'])) return 'account'; - if (isset($agent['mbox_sha1sum'])) return 'mbox_sha1sum'; - } else { - return 'actor'; - } - } - - /** - * Returns $statements where the $value matches any of the $keys. - * @param Builder $statements - * @param mixed $value - * @param array $keys - * @return Builder - */ - private function addFilter(Builder $statements, $value, array $keys) { - if (!isset($value)) return $statements; - - // Adds keys for sub statement and statement references. - foreach ($keys as $key) { - $keys[] = 'refs.'.substr($key, 10); - } - - return $this->orWhere($statements, $value, $keys); - } - - /** - * Filters $statements with an options. - * @param Builder $statements Statements to be filtered. - * @param mixed $value Value to match against $keys. - * @param boolean $use_broad - * @param array $specific Keys to be search regardless of $use_broad. - * @param array $broad Addtional keys to be searched when $use_broad is true. - * @return Builder - */ - private function addOptionFilter(Builder $statements, $value, $use_broad, array $specific, array $broad) { - $keys = $specific; - - if ($use_broad === true) { - $keys = array_merge($keys, $broad); - - // Adds keys for sub statement. - foreach ($keys as $key) { - $keys[] = 'statement.object.'.substr($key, 10); - } - } - - return $this->addFilter($statements, $value, $keys); - } - - /** - * Returns $statements where the $value matches any of the $keys. - * @param Builder $statements - * @param mixed $value - * @param array $keys - * @return Builder - */ - private function orWhere(Builder $statements, $value, array $keys) { - return $statements->where(function (Builder $query) use ($keys, $value) { - foreach ($keys as $key) { - $query->orWhere($key, $value); - } - return $query; - }); - } - - /** - * Converts statements in the "canonical" format as defined by the spec. - * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#723-getstatements - * @param [statements] - * @return [statements] - */ - public function toCanonical(array $statements, array $langs) { - foreach ($statements as $index => $statement) { - $statements[$index]['statement'] = $this->getStatementCanonical($statement['statement'], $langs); - } - return $statements; - } - - /** - * Converts statements in the "ids" format as defined by the spec. - * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#723-getstatements - * @param [statements] - * @return [statements] - */ - public function toIds(array $statements) { - foreach ($statements as $index => $statement) { - $statements[$index]['statement'] = $this->getStatementIds($statement['statement']); - } - return $statements; - } - - /** - * Attempts to convert a $langMap to a single string using a relevant language from $langs. - * @param [LanguageMap] $langMap - * @param [Language] $langs - * @return String/[LanguageMap] - */ - private function canonicalise(array $langMap, array $langs) { - foreach ($langs as $lang) { - if (isset($langMap[$lang])) { - return $langMap[$lang]; - } - } - return $langMap; - } - - /** - * Canonicalises some parts of the $statement as defined by the spec using $langs. - * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#723-getstatements - * @param Statement $statement - * @param [Language] $langs - * @return Statement - */ - private function getStatementCanonical(array $statement, array $langs) { - if (isset($statement['object']['definition']['name'])) { - $statement['object']['definition']['name'] = $this->canonicalise( - $statement['object']['definition']['name'], - $langs - ); - } - if (isset($statement['object']['definition']['description'])) { - $statement['object']['definition']['description'] = $this->canonicalise( - $statement['object']['definition']['description'], - $langs - ); - } - return $statement; - } - - /** - * Gets the identifier key of an $agent. - * @param Agent $actor - * @return string - */ - private function getAgentIdentifier($actor) { - if (isset($actor['mbox'])) return 'mbox'; - if (isset($actor['account'])) return 'account'; - if (isset($actor['openid'])) return 'openid'; - if (isset($actor['mbox_sha1sum'])) return 'mbox_sha1sum'; - return null; - } - - /** - * Ids some parts of the $statement as defined by the spec. - * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#723-getstatements - * @param Statement $statement - * @return Statement - */ - private function getStatementIds(array $statement) { - $actor = $statement['actor']; - - // Processes an anonymous group or actor. - if (isset($actor['objectType']) && $actor['objectType'] === 'Group' && $this->getAgentIdentifier($actor) === null) { - $members = []; - foreach ($actor['members'] as $member) { - $identifier = $this->getAgentIdentifier($member); - $members[] = [ - $identifier => $member[$identifier] - ]; - } - $actor['members'] = $members; - } else { - $identifier = $this->getAgentIdentifier($actor); - $actor = [ - $identifier => $actor[$identifier], - 'objectType' => isset($actor['objectType']) ? $actor['objectType'] : 'Agent' - ]; - } - - // Replace parts of the statements. - $statement['actor'] = $actor; - $identifier = $this->getAgentIdentifier($statement['object']) ?: 'id'; - $statement['object'] = [ - $identifier => $statement['object'][$identifier], - 'objectType' => isset($statement['object']['objectType']) ? $statement['object']['objectType'] : 'Activity' - ]; - - return $statement; - } - - /** - * Constructs the authority. - * https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#authority - * @return Authority - */ - private function constructAuthority() { - $client = (new \Client) - ->where('api.basic_key', \LockerRequest::getUser()) - ->where('api.basic_secret', \LockerRequest::getPassword()) - ->first(); - - if ($client != null && isset($client['authority'])) { - return json_decode(json_encode($client['authority'])); - } else { - $site = \Site::first(); - return (object) [ - 'name' => $site->name, - 'mbox' => 'mailto:' . $site->email, - 'objectType' => 'Agent' - ]; - } - } - - /** - * Validates $statements. - * @param [Statement] $statements - * @return [Statement] Valid statements. - */ - private function validateStatements(array $statements, \Lrs $lrs) { - $statements = $this->removeDuplicateStatements($statements, $lrs); - $authority = $this->constructAuthority(); - $void_statements = []; - - foreach ($statements as $index => $statement) { - $statement->setProp('authority', $authority); - $errors = array_map(function ($error) { - return (string) $error->addTrace('statement'); - }, $statement->validate()); - - if (!empty($errors)) { - throw new Exceptions\Validation($errors); - } else { - if ($this->isVoiding($statement->getValue())) { - $void_statements[] = $statement->getPropValue('object.id'); - } - } - } - if ($void_statements) { - $this->validateVoid($statements, $lrs, $void_statements); - } - return $statements; - } - - /** - * Check that all void reference ids exist in the database and are not themselves void statements - * @param array $statements - * @param Lrs $lrs - * @param array $references - * @throws \Exception - */ - private function validateVoid(array $statements, \Lrs $lrs, array $references) { - $count = count($references); - $reference_count = $this->statement - ->where('lrs._id', $lrs->_id) - ->whereIn('statement.id', $references) - ->where('statement.verb.id', '<>', "http://adlnet.gov/expapi/verbs/voided") - ->count(); - if ($reference_count != $count) { - throw new Exceptions\Exception('Voiding invalid or nonexistant statement'); - } - } - - /** - * Remove duplicate statements and generate ids - * - * @param array $statements - * @param \Lrs $lrs - * @return array - */ - private function removeDuplicateStatements(array $statements, \Lrs $lrs) { - $new_id_count = 0; - $new_statements = []; - $indexed_statements = []; - foreach($statements as $index => $statement) { - $statement_id = $statement->getPropValue('id'); - if ($statement_id !== null) { - if (isset($this->sent_ids[$statement_id])) { - $sent_statement = json_encode($this->sent_ids[$statement_id]); - $current_statement = json_encode($statement); - $this->checkMatch($new_statement, $current_statement); - unset($statements[$index]); - } else { - $this->sent_ids[$statement_id] = $statement; - $indexed_statements[$statement_id] = $statement; - } - } else { - $new_statements[] = $statement; - } - } - - if (count($new_statements)) { - $new_statements = $this->assignIds($new_statements, $lrs); - $indexed_statements = array_merge($indexed_statements, $new_statements); - } - - return $indexed_statements; - } - - /** - * @param array $statements - * @param \Lrs $lrs - * @return array List of statements with assigned id - */ - private function assignIds(array $statements, \Lrs $lrs) { - $indexed_statements = []; - $count = count($statements); - $uuids = $this->generateIds($count + 1); - $duplicates = $this->checkIdsExist($uuids, $lrs); - if ($duplicates) { - $uuids = array_diff($uuids, $duplicates); - } - while(count($uuids) < $count) { - $new_uuids = $this->generateIds($count - count($uuids)); - $duplicates = $this->checkIdsExist($new_uuids, $lrs); - if ($duplicates) { - $new_uuids = array_diff($uuids, $duplicates); - $uuids = array_merge($new_uuids); - } - } - - foreach($statements as $statement) { - $uuid = array_pop($uuids); - $statement->setProp('id', $uuid); - $indexed_statements[$uuid] = $statement; - } - return $indexed_statements; - } - - private function checkMatch($new_statement, $old_statement) { - $new_statement_obj = \Locker\XApi\Statement::createFromJson($new_statement); - $old_statement_obj = \Locker\XApi\Statement::createFromJson($old_statement); - $new_statement = json_decode($new_statement_obj->toJson(), true); - $old_statement = json_decode($old_statement_obj->toJson(), true); - array_multisort($new_statement); - array_multisort($old_statement); - ksort($new_statement); - ksort($old_statement); - unset($new_statement['stored']); - unset($old_statement['stored']); - if ($new_statement !== $old_statement) { - $new_statement = $new_statement_obj->toJson(); - $old_statement = $old_statement_obj->toJson(); - throw new Exceptions\Conflict( - "Conflicts\r\n`$new_statement`\r\n`$old_statement`." - ); - }; - } - - /** - * Check lrs for list of statement ids, optional list of statements by id for comparison - * - * @param array $uuids - * @param \Lrs $lrs - * @param array $statements - * @return array List of duplicate ids - */ - private function checkIdsExist(array $uuids, \Lrs $lrs, array $statements=null) { - $duplicates = array(); - - if ($uuids) { - $existingModels = $this->statement - ->where('lrs._id', $lrs->_id) - ->whereIn('statement.id', $uuids) - ->get(); - - if(!$existingModels->isEmpty()) { - foreach($existingModels as $existingModel) { - $existingStatement = $existingModel->statement; - $id = $existingStatement['id']; - $duplicates[] = $id; - if ($statements && isset($statements[$id])) { - $statement = $statements[$id]; - $this->checkMatch($statement->toJson(), json_encode($existingStatement)); - } - } - } - } - return $duplicates; - } - - /** - * Generate an array of uuids of size $count - * - * @param integer $count - * @return array List of uuids - */ - private function generateIds($count) { - $uuids = array(); - $validator = new \app\locker\statements\xAPIValidation(); - $i = 1; - while ($i <= $count) { - $uuid = $validator->makeUUID(); - if (isset($this->sent_ids[$uuid])) { - continue; - } - $i++; - $uuids[] = $uuid; - } - - return $uuids; - } - - /** - * Create statements. - * @param [Statement] $statements - * @param Lrs $lrs - * @return array list of statements - */ - private function createStatements(array $statements, \Lrs $lrs) { - if (count($this->sent_ids)) { - // check for duplicates from statements with pre-assigned ids - $this->checkIdsExist(array_keys($this->sent_ids), $lrs, $statements); - } - - // Replaces '.' in keys with '&46;'. - $statements = array_map(function (\Locker\XApi\Statement $statement) use ($lrs) { - $replaceFullStop = function ($object, $replaceFullStop) { - if ($object instanceof \Locker\XApi\Element) { - $prop_keys = array_keys(get_object_vars($object->getValue())); - foreach ($prop_keys as $prop_key) { - $new_prop_key = str_replace('.', '&46;', $prop_key); - $prop_value = $object->getProp($prop_key); - $new_value = $replaceFullStop($prop_value, $replaceFullStop); - $object->unsetProp($prop_key); - $object->setProp($new_prop_key, $new_value); - } - return $object; - } else { - return $object; - } - }; - $replaceFullStop($statement, $replaceFullStop); - - return $this->makeStatement($statement, $lrs); - }, $statements); - - $this->statement->where('lrs._id', $lrs->id)->insert(array_values($statements)); - return $statements; - } - - /** - * Sets references - * @param array $statements - * @param \Lrs $lrs - * @return array list of statements with references - */ - public function updateReferences(array $statements, \Lrs $lrs) { - foreach($statements as $id => $statement) { - if ($this->isReferencing($statement['statement'])) { - // Finds the statement that it references. - $refs = []; - $this->recursiveCheckReferences($statements, $lrs, $refs, $statement['statement']->object->id); - // Updates the refs. - if ($refs) { - $refs = array_values($refs); - $statements[$id]['refs'] = $refs; - $this->statement - ->where('lrs._id', $lrs->id) - ->where('statement.id', $id)->update([ - 'refs' => $refs - ]); - } - } - } - $this->updateReferrers($statements, $lrs); - return $statements; - } - - private function recursiveCheckReferences(array $statements, \Lrs $lrs, array &$refs, $id) { - // check if $id refers to a statement being inserted - if (isset($refs[$id])) { - return $refs; - } - - if (isset($statements[$id])) { - $s = $statements[$id]; - $refs[$id] = $s->statement; - if ($this->isReferencing($s->statement)) { - $s_id = $s->statement->getPropValue('object.id'); - $this->recursiveCheckReferences($statements, $lrs, $refs, $s_id); - } - } else { - $reference = $this->query->where($lrs->_id, [ - ['statement.id', '=', $id] - ])->first(); - if ($reference) { - $refs[$id] = $reference->statement; - if ($this->isReferencing((object) $reference->statement)) { - $s_id = $reference->statement['object']['id']; - $this->recursiveCheckReferences($statements, $lrs, $refs, $s_id); - } - } - } - return $refs; - } - - /** - * Adds statement to refs in a existing referrer. - * @param [Statement] $statements - * @return [Statement] - */ - private function updateReferrers(array $statements, \Lrs $lrs) { - if (count($this->sent_ids)) { - $referrers = $this->query->where($lrs->_id, [ - ['statement.object.id', 'in', array_keys($statements)], - ['statement.object.objectType', '=', 'StatementRef'], - ])->get(); - - // Updates the refs $referrers. - foreach ($referrers as $referrer) { - $statement_id = $referrer['statement']['object']['id']; - $statement = $statements[$statement_id]; - if (isset($statement['refs'])) { - $referrer->refs = array_merge([$statement['statement']], $statement['refs']); - } else { - $referrer->refs = [$statement['statement']]; - } - if (!$referrer->save()) throw new Exceptions\Exception('Failed to save referrer.'); - } - } - return $statements; - } - - private function isReferencing(\stdClass $statement) { - return ( - isset($statement->object->id) && - isset($statement->object->objectType) && - $statement->object->objectType === 'StatementRef' - ); - } - - /** - * Determines if a $statement voids another. - * @param Statement $statement - * @return boolean - */ - private function isVoiding(\stdClass $statement) { - if (($statement->verb->id === 'http://adlnet.gov/expapi/verbs/voided') && $this->isReferencing($statement)) { - return true; - } - return false; - } - - private function voidStatement($statement, $lrs) { - if (!$this->isVoiding($statement['statement'])) return $statement; - $reference = $this->query->where($lrs->_id, [ - ['statement.id', '=', $statement['statement']->object->id] - ])->first(); - $ref_statement = json_decode(json_encode($reference->statement)); - if ($this->isVoiding($ref_statement)) { - throw new Exceptions\Exception('Cannot void a voiding statement'); - } - $reference->voided = true; - if (!$reference->save()) throw new Exceptions\Exception('Failed to void statement.'); - return $statement; - } - - public function voidStatements(array $statements, \Lrs $lrs) { - return array_map(function (array $statement) use ($lrs) { - return $this->voidStatement($statement, $lrs); - }, $statements); - } - - public function activateStatements(array $statements, \Lrs $lrs) { - $updated = $this->statement->where('lrs._id', $lrs->id)->whereIn('statement.id', array_keys($statements))->update(array('active' => true)); - } - - /** - * Creates $statements in the $lrs with $attachments. - * @param [Statement] $statements - * @param \LRS $lrs - * @param string $attachments - * @return array create result (see makeCreateResult function) - */ - public function create(array $statements, \Lrs $lrs, $attachments = '') { - $statements = array_map(function (\stdClass $statement) { - return new \Locker\XApi\Statement($statement); - }, $statements); - $statements = $this->validateStatements($statements, $lrs); - $statements = $this->createStatements($statements, $lrs); - $statements = $this->updateReferences($statements, $lrs); - $statements = $this->voidStatements($statements, $lrs); - $this->activateStatements($statements, $lrs); - - // Stores the $attachments. - if ($attachments != '') { - $this->storeAttachments($attachments, $lrs->_id); - } - return array_keys($statements); - } - - /** - * Validates a $statement with an $authority. - * @param Statement $statement - * @param Authority $Authority - * @return Validator - */ - private function validateStatement(array $statement, array $authority) { - return (new \app\locker\statements\xAPIValidation())->runValidation( - $statement, - $authority - ); - } - - /** - * Makes a $statement for the current $lrs. - * @param Statement $statement - * @param LRS $lrs - * @return Statement - */ - private function makeStatement(\Locker\XApi\Statement $statement, \Lrs $lrs) { - // Uses defaults where possible. - $currentDate = $this->getCurrentDate(); - $statement->setProp('stored', $currentDate); - if ($statement->getPropValue('timestamp') === null) { - $statement->setProp('timestamp', $currentDate); - } - - // For now we store the latest submitted definition. - // @todo this will change when we have a way to determine authority to edit. - if ($statement->getPropValue('object.definition') !== null) { - $this->activity->saveActivity( - $statement->getPropValue('object.id'), - $statement->getPropValue('object.definition') - ); - } - // Create a new statement model - return [ - 'lrs' => [ - '_id' => $lrs->_id, - 'name' => $lrs->title - ], - 'statement' => $statement->getValue(), - 'active' => false, - 'voided' => false, - 'timestamp' => new \MongoDate(strtotime($statement->getPropValue('timestamp'))) - ]; - } - - /** - * Make an associative array that represents the result of creating statements. - * @param [StatementId] $ids Array of IDs of successfully created statements. - * @param boolean $success - * @param string $description Description of the result. - * @return array create result. - */ - private function makeCreateResult(array $ids, $success = false, $description = '') { - return [ - 'success' => $success, - 'ids' => $ids, - 'message' => $description - ]; - } - - /** - * Calculates the current date(consistent through xAPI header). - * @return string - */ - public function getCurrentDate() { - $current_date = \DateTime::createFromFormat('U.u', sprintf('%.4f', microtime(true))); - $current_date->setTimezone(new \DateTimeZone(\Config::get('app.timezone'))); - return $current_date->format('Y-m-d\TH:i:s.uP'); - } - - /** - * Replace `.` with `&46;` in keys of a $statement. - * @param Statement $statement - * @return Statement - */ - private function replaceFullStop(array $statement){ - return \Locker\Helpers\Helpers::replaceFullStop($statement); - } - - /** - * Check to see if a submitted statementId already exist and if so - * are the two statements idntical? If not, return true. - * - * @param uuid $id - * @param string $lrs - * @return boolean - * - **/ - private function doesStatementIdExist($lrsId, array $statement) { - $existingModel = $this->statement - ->where('lrs._id', $lrsId) - ->where('statement.id', $statement['id']) - ->first(); - - if ($existingModel) { - $existingStatement = json_encode($existingModel->statement); - $this->checkMatch($existingStatement, json_encode($statement)); - return $existingModel; - } - - return null; - } - - public function getAttachments($statements, $lrs) { - $destination_path = Helpers::getEnvVar('LOCAL_FILESTORE').'/'.$lrs.'/attachments/'; - $attachments = []; - - foreach ($statements as $statement) { - $statement = $statement['statement']; - $attachments = array_merge($attachments, array_map(function ($attachment) use ($destination_path) { - $ext = array_search($attachment['contentType'], FileTypes::getMap()); - $filename = $attachment['sha2'].'.'.$ext; - return ( - 'Content-Type:'.$attachment['contentType']."\r\n". - 'Content-Transfer-Encoding:binary'."\r\n". - 'X-Experience-API-Hash:'.$attachment['sha2']. - "\r\n\r\n". - file_get_contents($destination_path.$filename) - ); - }, isset($statement['attachments']) ? $statement['attachments'] : [])); - } - - return $attachments; - } - - /** - * Store any attachments - * - **/ - private function storeAttachments( $attachments, $lrs ){ - foreach ($attachments as $attachment) { - // Determines the delimiter. - $delim = "\n"; - if (strpos($attachment, "\r".$delim) !== false) $delim = "\r".$delim; - - // Separate body contents from headers - $attachment = ltrim($attachment, $delim); - list($raw_headers, $body) = explode($delim.$delim, $attachment, 2); - - // Parse headers and separate so we can access - $raw_headers = explode($delim, $raw_headers); - $headers = []; - foreach ($raw_headers as $header) { - list($name, $value) = explode(':', $header); - $headers[strtolower($name)] = ltrim($value, ' '); - } - - //get the correct ext if valid - $ext = array_search($headers['content-type'], FileTypes::getMap()); - if ($ext === false) throw new Exceptions\Exception( - 'This file type cannot be supported' - ); - - $destination_path = Helpers::getEnvVar('LOCAL_FILESTORE').'/'.$lrs.'/attachments/'; - $filename = $headers['x-experience-api-hash']; - - //create directory if it doesn't exist - if (!\File::exists($destination_path)) { - \File::makeDirectory($destination_path, 0775, true); - } - - $filename = $destination_path.$filename.'.'.$ext; - $file = fopen($filename, 'wb'); //opens the file for writing with a BINARY (b) fla - $size = fwrite($file, $body); //write the data to the file - fclose($file); - - if($size === false) throw new Exceptions\Exception( - 'There was an issue saving the attachment' - ); - } - - } - - /** - * Count statements for any give lrs - * @param string Lrs - * @param array parameters Any parameters for filtering - * @return count - **/ - public function count( $lrs, $parameters=null ){ - $query = $this->statement->where('lrs._id', $lrs); - if(!is_null($parameters)){ - $this->addParameters( $query, $parameters, true ); - } - $count = $query->count(); - $query->remember(5); - return $count; - } - - public function grouped($id, $parameters){ - $type = isset($parameters['grouping']) ? strtolower($parameters['grouping']) : ''; - - switch ($type) { - case "time": - $interval = isset($parameters['interval']) ? $parameters['interval'] : "day"; - $filters = isset($parameters['filters']) ? json_decode($parameters['filters'], true) : array(); - $filters['lrs._id'] = $id; - $results = $this->query->timedGrouping( $filters, $interval ); - break; - } - - return $results; - } - - public function timeGrouping($query, $interval ){ - return $query; - } - - public function actorGrouping($query){ - $query->aggregate( - array('$match' => array()), - array( - '$group' => array( - '_id' => 'statement.actor.mbox' - ) - ) - ); - return $query; - } -} diff --git a/app/locker/repository/Statement/EloquentStorer.php b/app/locker/repository/Statement/EloquentStorer.php new file mode 100644 index 0000000000..e025a1af30 --- /dev/null +++ b/app/locker/repository/Statement/EloquentStorer.php @@ -0,0 +1,96 @@ +<?php namespace Locker\Repository\Statement; + +use \Locker\Helpers\Helpers as Helpers; +use \Locker\XApi\Statement as XAPIStatement; + +interface Storer { + public function store(array $statements, array $attachments, StoreOptions $opts); +} + +class EloquentStorer extends EloquentReader implements Storer { + + protected $inserter, $linker, $voider, $attacher, $hashes; + + public function __construct() { + $this->inserter = new EloquentInserter(); + $this->linker = new EloquentLinker(); + $this->voider = new EloquentVoider(); + $this->attacher = new FileAttacher(); + } + + /** + * Stores statements and attachments with the given options + * @param [\stdClass] $statements + * @param [String => Mixed] $attachments + * @param StoreOptions $opts + * @return [String] UUIDs of the statements stored. + */ + public function store(array $statements, array $attachments, StoreOptions $opts) { + $id_statements = $this->constructValidStatements($statements, $opts); + $ids = array_keys($id_statements); + $statements = array_values($id_statements); + + $this->inserter->insert($statements, $opts); + $this->linker->updateReferences($statements, $opts); + $this->voider->voidStatements($statements, $opts); + $this->attacher->store($attachments, $this->hashes, $opts); + $this->activateStatements($ids, $opts); + + return $ids; + } + + /** + * Constructs valid statements. + * @param [\stdClass] $statements + * @param StoreOptions $opts + * @return [String => \stdClass] Array of statements mapped to their UUIDs. + */ + private function constructValidStatements(array $statements, StoreOptions $opts) { + $constructed = []; + $this->hashes = []; + + foreach ($statements as $statement) { + $statement->authority = $opts->getOpt('authority'); + $statement->stored = Helpers::getCurrentDate(); + + if (!isset($statement->timestamp)) { + $statement->timestamp = $statement->stored; + } + + if (!isset($statement->id)) { + $statement->id = Helpers::makeUUID(); + } + + // Validates statement. + $constructed_statement = new XAPIStatement($statement); + Helpers::validateAtom($constructed_statement, 'statement'); + $statement = $constructed_statement->getValue(); + + // Gets attachment hashes. + $attachments = !isset($statement->attachments) ? [] : $statement->attachments; + foreach ($attachments as $attachment) { + $this->hashes[] = $attachment->sha2; + } + + // Adds $statement to $constructed. + if (isset($constructed[$statement->id])) { + $this->inserter->compareForConflict($statement, $constructed[$statement->id]); + } else { + $constructed[$statement->id] = $statement; + } + } + + return $constructed; + } + + /** + * Activates the statements using their UUIDs. + * @param [String] $ids UUIDs of the statements to be activated. + * @param StoreOptions $opts + */ + private function activateStatements(array $ids, StoreOptions $opts) { + return $this->where($opts) + ->whereIn('statement.id', $ids) + ->update(['active' => true]); + } +} diff --git a/app/locker/repository/Statement/EloquentVoider.php b/app/locker/repository/Statement/EloquentVoider.php new file mode 100644 index 0000000000..3f08b11fd5 --- /dev/null +++ b/app/locker/repository/Statement/EloquentVoider.php @@ -0,0 +1,59 @@ +<?php namespace Locker\Repository\Statement; + +use \Locker\Helpers\Exceptions as Exceptions; +use \Illuminate\Database\Eloquent\Model as Model; + +interface VoiderInterface { + public function voidStatements(array $statements, StoreOptions $opts); +} + +class EloquentVoider extends EloquentLinker implements VoiderInterface { + + /** + * Voids statements that need to be voided. + * @param [\stdClass] $statements + * @param StoreOptions $opts + */ + public function voidStatements(array $statements, StoreOptions $opts) { + return array_map(function (\stdClass $voider) use ($opts) { + return $this->voidStatement($voider, $opts); + }, $statements); + } + + /** + * Voids a statement if it needs to be voided. + * @param \stdClass $voider + * @param StoreOptions $opts + */ + private function voidStatement(\stdClass $voider, StoreOptions $opts) { + if (!$this->isVoiding($voider)) return; + + $voided = $this->getModel($voider->object->id, $opts); + + if ($voided !== null) { + if ($this->isVoiding($this->formatModel($voided))) throw new Exceptions\Exception(trans( + 'xapi.errors.void_voider' + )); + + $voided->voided = true; + $voided->save(); + } else { + throw new Exceptions\Exception(trans( + 'xapi.errors.void_null' + )); + } + } + + /** + * Determines if a statement is a voiding statement. + * @param \stdClass $statement + * @return Boolean + */ + private function isVoiding(\stdClass $statement) { + return ( + isset($statement->verb->id) && + $statement->verb->id === 'http://adlnet.gov/expapi/verbs/voided' && + $this->isReferencing($statement) + ); + } +} diff --git a/app/locker/repository/Statement/FileAttacher.php b/app/locker/repository/Statement/FileAttacher.php new file mode 100644 index 0000000000..3e8edb15b0 --- /dev/null +++ b/app/locker/repository/Statement/FileAttacher.php @@ -0,0 +1,78 @@ +<?php namespace Locker\Repository\Statement; + +use \Locker\Repository\Document\FileTypes as FileTypes; +use \Locker\Helpers\Helpers as Helpers; +use \Locker\Helpers\Exceptions as Exceptions; + +class FileAttacher { + + /** + * Stores attachments with the given options. + * @param [\stdClass] $attachments + * @param [String] $hashes + * @param StoreOptions $opts + */ + public function store(array $attachments, array $hashes, StoreOptions $opts) { + $dir = $this->getDir($opts); + if (!is_dir($dir) && count($attachments > 0) && !empty($attachments)) { + mkdir($dir, null, true); + } + + foreach ($attachments as $attachment) { + if (!in_array($attachment->hash, $hashes)) throw new Exceptions\Exception( + 'Attachment hash does not exist in given statements' + ); + + $ext = $this->getExt($attachment->content_type); + if ($ext === false) throw new Exceptions\Exception( + 'This file type cannot be supported' + ); + + $file = $attachment->hash.$ext; + file_put_contents($dir.'.'.$file, $attachment->content); + } + } + + /** + * Gets all of the attachments for the given statements. + * @param [\stdClass] $statements + * @param IndexOptions $opts + * @return [\stdClass] + */ + public function index(array $statements, IndexOptions $opts) { + $dir = $this->getDir($opts); + + $attachments = []; + foreach ($statements as $statement) { + $attachments = array_merge($attachments, array_map(function ($attachment) use ($dir) { + $ext = $this->getExt($attachment->contentType); + $filename = $attachment->sha2.'.'.$ext; + return (object) [ + 'content_type' => $attachment['contentType'], + 'hash' => $attachment->sha2, + 'content' => file_get_contents($dir.$filename) + ]; + }, isset($statement->attachments) ? $statement->attachments : [])); + } + + return $attachments; + } + + /** + * Gets the extension from the given content type. + * @param String $content_type + * @return String + */ + private function getExt($content_type) { + return array_search($content_type, FileTypes::getMap()); + } + + /** + * Gets the directory for attachments with the given options. + * @param Options $opts + * @return String + */ + private function getDir(Options $opts) { + return Helpers::getEnvVar('LOCAL_FILESTORE').'/'.$opts->getOpt('lrs_id').'/attachments/'; + } +} diff --git a/app/locker/repository/Statement/IndexOptions.php b/app/locker/repository/Statement/IndexOptions.php index 78194ed6cb..c9ca3e8311 100644 --- a/app/locker/repository/Statement/IndexOptions.php +++ b/app/locker/repository/Statement/IndexOptions.php @@ -1,30 +1,64 @@ <?php namespace Locker\Repository\Statement; use \Locker\Helpers\Helpers as Helpers; +use \Locker\Helpers\Exceptions as Exceptions; -class IndexOptions { - public $options = []; - - public function __construct(array $opts) { - $this->options = $this->mergeDefaults($opts); - $this->validate(); - } - - public function getOpt($opt) { - return $options[$opt]; - } +class IndexOptions extends Options { + protected $defaults = [ + 'agent' => null, + 'activity' => null, + 'verb' => null, + 'registration' => null, + 'since' => null, + 'until' => null, + 'active' => true, + 'voided' => false, + 'related_activities' => false, + 'related_agents' => false, + 'ascending' => false, + 'format' => 'exact', + 'offset' => 0, + 'limit' => 0, + 'langs' => [], + 'attachments' => false + ]; + protected $types = [ + 'agent' => 'Actor', + 'activity' => 'IRI', + 'verb' => 'IRI', + 'registration' => 'UUID', + 'since' => 'Timestamp', + 'until' => 'Timestamp', + 'active' => 'Boolean', + 'voided' => 'Boolean', + 'related_activities' => 'Boolean', + 'related_agents' => 'Boolean', + 'ascending' => 'Boolean', + 'format' => 'String', + 'offset' => 'Integer', + 'limit' => 'Integer', + 'langs' => ['Language'], + 'attachments' => 'Boolean', + 'lrs_id' => 'String' + ]; /** * Validates the given options as index options. + * @param [String => Mixed] $opts + * @return [String => Mixed] */ - private function validate() { - $opts = $this->options; + protected function validate($opts) { + $opts = parent::validate($opts); + + // Validates values. + if (!isset($opts['lrs_id'])) throw new Exceptions\Exception('`lrs_id` must be set.'); if ($opts['offset'] < 0) throw new Exceptions\Exception('`offset` must be a positive interger.'); if ($opts['limit'] < 1) throw new Exceptions\Exception('`limit` must be a positive interger.'); - XApiHelpers::checkType('related_activities', 'boolean', $opts['related_agents'])); - XApiHelpers::checkType('related_activities', 'boolean', $opts['related_activities'])); - XApiHelpers::checkType('attachments', 'boolean', $opts['attachments'])); - XApiHelpers::checkType('ascending', 'boolean', $opts['ascending'])); + if (!in_array($opts['format'], ['exact', 'canonical', 'ids'])) { + throw new Exceptions\Exception('`format` must be "exact", "canonical", or "ids".'); + } + + return $opts; } /** @@ -32,60 +66,21 @@ private function validate() { * @param [String => mixed] $opts Index options. * @return [String => mixed] */ - private function mergeDefaults(array $opts) { + protected function mergeDefaults(array $opts) { // Merges with defaults. - $options = array_merge([ - 'agent' => null, - 'activity' => null, - 'verb' => null, - 'registration' => null, - 'since' => null, - 'until' => null, - 'active' => true, - 'voided' => false, - 'related_activities' => false, - 'related_agents' => false, - 'ascending' => false, - 'format' => 'exact', - 'offset' => 0, - 'limit' => 0, - 'langs' => [], - 'attachments' => false - ], $opts); + $opts = parent::mergeDefaults($opts); // Converts types. - $options['active'] = $this->convertToBoolean($options['active']); - $options['voided'] = $this->convertToBoolean($options['voided']); - $options['related_agents'] = $this->convertToBoolean($options['related_agents']); - $options['related_activities'] = $this->convertToBoolean($options['related_activities']); - $options['attachments'] = $this->convertToBoolean($options['attachments']); - $options['ascending'] = $this->convertToBoolean($options['ascending']); - $options['limit'] = $this->convertToInt($options['limit']); - $options['offset'] = $this->convertToInt($options['offset']); - - if ($options['limit'] === 0) $options['limit'] = 100; - return $options; - } + $opts['active'] = $this->convertToBoolean($opts['active']); + $opts['voided'] = $this->convertToBoolean($opts['voided']); + $opts['related_agents'] = $this->convertToBoolean($opts['related_agents']); + $opts['related_activities'] = $this->convertToBoolean($opts['related_activities']); + $opts['attachments'] = $this->convertToBoolean($opts['attachments']); + $opts['ascending'] = $this->convertToBoolean($opts['ascending']); + $opts['limit'] = $this->convertToInt($opts['limit']); + $opts['offset'] = $this->convertToInt($opts['offset']); - /** - * Converts the given value to a Boolean if it can be. - * @param mixed $value - * @return Boolean|mixed Returns the value unchanged if it can't be converted. - */ - private function convertToBoolean($value) { - if (is_string($value)) $value = strtolower($value); - if ($value === 'true') return true; - if ($value === 'false') return false; - return $value; - } - - /** - * Converts the given value to a Integer if it can be. - * @param mixed $value - * @return Integer|mixed Returns the value unchanged if it can't be converted. - */ - private function convertToInt($value) { - $converted_value = (int) $value; - return ($value !== (string) $converted_value) ? $value : $converted_value; + if ($opts['limit'] === 0) $opts['limit'] = 100; + return $opts; } } diff --git a/app/locker/repository/Statement/Options.php b/app/locker/repository/Statement/Options.php new file mode 100644 index 0000000000..547caff34b --- /dev/null +++ b/app/locker/repository/Statement/Options.php @@ -0,0 +1,81 @@ +<?php namespace Locker\Repository\Statement; + +use \Locker\Helpers\Helpers as Helpers; +use \Locker\Helpers\Exceptions as Exceptions; + +abstract class Options { + public $options = []; + protected $defaults = []; + protected $types = []; + + public function __construct(array $opts) { + $this->options = $this->mergeDefaults($opts); + $this->options = $this->validate($this->options); + } + + /** + * Gets an options. + * @param String $opt Option name. + * @return Mixed + */ + public function getOpt($opt) { + return $this->options[$opt]; + } + + /** + * Validates the given options as index options. + * @param [String => Mixed] $opts + * @return [String => Mixed] + */ + protected function validate($opts) { + foreach ($opts as $key => $value) { + if ($value !== null) { + if (is_array($this->types[$key])) { + $class = '\Locker\XApi\\'.$this->types[$key][0]; + if (!is_array($value)) { + throw new Exceptions\Exception("$key must be an array."); + } + foreach ($value as $item) { + Helpers::validateAtom(new $class($item)); + } + } else { + $class = '\Locker\XApi\\'.$this->types[$key]; + Helpers::validateAtom(new $class($value)); + } + } + } + + return $opts; + } + + /** + * Returns all of the index options set to their default or given value (using the given options). + * @param [String => mixed] $opts Index options. + * @return [String => mixed] + */ + protected function mergeDefaults(array $opts) { + return array_merge($this->defaults, $opts); + } + + /** + * Converts the given value to a Boolean if it can be. + * @param mixed $value + * @return Boolean|mixed Returns the value unchanged if it can't be converted. + */ + protected function convertToBoolean($value) { + if (is_string($value)) $value = strtolower($value); + if ($value === 'true') return true; + if ($value === 'false') return false; + return $value; + } + + /** + * Converts the given value to a Integer if it can be. + * @param mixed $value + * @return Integer|mixed Returns the value unchanged if it can't be converted. + */ + protected function convertToInt($value) { + $converted_value = (int) $value; + return ($value !== (string) $converted_value) ? $value : $converted_value; + } +} diff --git a/app/locker/repository/Statement/ShowOptions.php b/app/locker/repository/Statement/ShowOptions.php new file mode 100644 index 0000000000..04e78a374a --- /dev/null +++ b/app/locker/repository/Statement/ShowOptions.php @@ -0,0 +1,13 @@ +<?php namespace Locker\Repository\Statement; + +class ShowOptions extends Options { + protected $defaults = [ + 'voided' => false, + 'active' => true + ]; + protected $types = [ + 'lrs_id' => 'String', + 'voided' => 'Boolean', + 'active' => 'Boolean' + ]; +} diff --git a/app/locker/repository/Statement/StatementRepository.php b/app/locker/repository/Statement/StatementRepository.php deleted file mode 100644 index 548b0fe7ed..0000000000 --- a/app/locker/repository/Statement/StatementRepository.php +++ /dev/null @@ -1,12 +0,0 @@ -<?php namespace Locker\Repository\Statement; - -interface StatementRepository { - - public function index($lrsId, array $filters, array $options); - public function show($lrsId, $id, $voided); - public function toCanonical(array $statements, array $langs); - public function toIds(array $statements); - public function getCurrentDate(); - public function create(array $statements, \Lrs $lrs, $attachment); - -} \ No newline at end of file diff --git a/app/locker/repository/Statement/StoreOptions.php b/app/locker/repository/Statement/StoreOptions.php new file mode 100644 index 0000000000..7ca41f642b --- /dev/null +++ b/app/locker/repository/Statement/StoreOptions.php @@ -0,0 +1,9 @@ +<?php namespace Locker\Repository\Statement; + +class StoreOptions extends Options { + protected $defaults = []; + protected $types = [ + 'lrs_id' => 'String', + 'authority' => 'Authority' + ]; +} diff --git a/app/models/Statement.php b/app/models/Statement.php index 687f41c022..9a7ece1ba1 100644 --- a/app/models/Statement.php +++ b/app/models/Statement.php @@ -4,38 +4,12 @@ class Statement extends Eloquent { - /** - * Our MongoDB collection used by the model. - * - * @var string - */ protected $collection = 'statements'; + protected $hidden = ['_id', 'created_at', 'updated_at']; + protected $fillable = ['statement', 'active', 'voided', 'refs', 'lrs', 'timestamp']; - /** - * We don't need default Laravel created_at - **/ - //public $timestamps = false; - - /** - * The attributes excluded from the model's JSON form. - * - * @var array - */ - protected $hidden = array('_id', 'created_at', 'updated_at'); - - /** - * For mass assigning which we use for TC statements, - * set the fillable fields. - **/ - // protected $fillable = array('actor', 'verb', 'result', 'object', 'context', - // 'authority', 'stored', 'timestamp', 'id', 'attachments', 'version'); - - /** - * All statements belong to an LRS - * - **/ public function lrs(){ return $this->belongsTo('Lrs'); } -} \ No newline at end of file +} diff --git a/app/tests/API/StatementsTest.php b/app/tests/API/StatementsTest.php deleted file mode 100644 index 199f6964f7..0000000000 --- a/app/tests/API/StatementsTest.php +++ /dev/null @@ -1,155 +0,0 @@ -<?php namespace Tests\API; -use \Route as Route; - -class StatementsTest extends TestCase { - static protected $endpoint = '/api/v1/statements'; - protected $pipeline = null; - - public function setup() { - parent::setup(); - } - - protected function getPipeline() { - return $this->pipeline ?: file_get_contents(__DIR__ . '/../Fixtures/pipeline.json'); - } - - protected function requestStatementsAPI($method = 'GET', $url = '', $params = []) { - $server = $this->getHeaders($this->lrs->api); - Route::enableFilters(); - return $this->call($method, $url, $params, [], $server, ''); - } - - public function testAggregate() { - $response = $this->requestStatementsAPI('GET', static::$endpoint.'/aggregate', [ - 'pipeline' => $this->getPipeline() - ]); - - // Checks that the response is correct. - $this->assertEquals(200, $response->getStatusCode(), 'Incorrect status code.'); - $this->assertEquals(true, method_exists($response, 'getContent'), 'Incorrect response.'); - - // Checks that the content is correct. - $content = json_decode($response->getContent()); - $this->assertEquals(true, is_object($content), 'Incorrect content type.'); - $this->assertEquals(true, isset($content->result), 'No result.'); - $this->assertEquals(true, is_array($content->result), 'Incorrect result type.'); - $this->assertEquals(static::$statements, count($content->result), 'Incorrect number of results.'); - $this->assertEquals(true, is_object($content->result[0]), 'Incorrect projection type.'); - $this->assertEquals(true, isset($content->result[0]->statement), 'No statement.'); - $this->assertEquals(true, is_object($content->result[0]->statement), 'Incorrect statement type.'); - $this->assertEquals(true, isset($content->result[0]->statement->actor), 'No actor.'); - $this->assertEquals(true, isset($content->ok), 'No ok.'); - $this->assertEquals(true, is_numeric($content->ok), 'Incorrect ok type.'); - $this->assertEquals(1, $content->ok, 'Incorrect ok.'); - } - - public function testAggregateTime() { - $response = $this->requestStatementsAPI('GET', static::$endpoint.'/aggregate/time', [ - 'match' => '{"active": true}' - ]); - - // Checks that the response is correct. - $this->assertEquals(200, $response->getStatusCode(), 'Incorrect status code.'); - $this->assertEquals(true, method_exists($response, 'getContent'), 'Incorrect response.'); - - // Checks that the content is correct. - $content = json_decode($response->getContent()); - $this->assertEquals(true, is_object($content), 'Incorrect content type.'); - $this->assertEquals(true, isset($content->result), 'No result.'); - $this->assertEquals(true, is_array($content->result), 'Incorrect result type.'); - $this->assertEquals(1, count($content->result), 'Incorrect number of results.'); - $this->assertEquals(true, is_object($content->result[0]), 'Incorrect projection type.'); - $this->assertEquals(true, isset($content->result[0]->count), 'No count.'); - $this->assertEquals(true, is_numeric($content->result[0]->count), 'Incorrect count type.'); - $this->assertEquals(static::$statements, is_numeric($content->result[0]->count), 'Incorrect count.'); - $this->assertEquals(true, isset($content->result[0]->date), 'No date.'); - $this->assertEquals(true, is_array($content->result[0]->date), 'Incorrect date type.'); - $this->assertEquals(true, isset($content->ok), 'No ok.'); - $this->assertEquals(true, is_numeric($content->ok), 'Incorrect ok type.'); - $this->assertEquals(1, $content->ok, 'Incorrect ok.'); - } - - public function testAggregateObject() { - $response = $this->requestStatementsAPI('GET', static::$endpoint.'/aggregate/object', [ - 'match' => '{"active": true}' - ]); - - // Checks that the response is correct. - $this->assertEquals(200, $response->getStatusCode(), 'Incorrect status code.'); - $this->assertEquals(true, method_exists($response, 'getContent'), 'Incorrect response.'); - - // Checks that the content is correct. - $content = json_decode($response->getContent()); - $this->assertEquals(true, is_object($content), 'Incorrect content type.'); - $this->assertEquals(true, isset($content->result), 'No result.'); - $this->assertEquals(true, is_array($content->result), 'Incorrect result type.'); - $this->assertEquals(1, count($content->result), 'Incorrect number of results.'); - $this->assertEquals(true, is_object($content->result[0]), 'Incorrect projection type.'); - $this->assertEquals(true, isset($content->result[0]->count), 'No count.'); - $this->assertEquals(true, is_numeric($content->result[0]->count), 'Incorrect count type.'); - $this->assertEquals(static::$statements, is_numeric($content->result[0]->count), 'Incorrect count.'); - $this->assertEquals(true, isset($content->result[0]->data), 'No data.'); - $this->assertEquals(true, is_array($content->result[0]->data), 'Incorrect data type.'); - $this->assertEquals(static::$statements, count($content->result[0]->data), 'Incorrect data.'); - $this->assertEquals(true, isset($content->result[0]->data[0]), 'Incorrect data item.'); - $this->assertEquals(true, is_object($content->result[0]->data[0]), 'Incorrect data item type.'); - $this->assertEquals(true, isset($content->result[0]->data[0]->actor), 'No actor.'); - $this->assertEquals(true, is_object($content->result[0]->data[0]->actor), 'Incorrect actor type.'); - $this->assertEquals(true, isset($content->ok), 'No ok.'); - $this->assertEquals(true, is_numeric($content->ok), 'Incorrect ok type.'); - $this->assertEquals(1, $content->ok, 'Incorrect ok.'); - } - - public function testWhere() { - $response = $this->requestStatementsAPI('GET', static::$endpoint.'/where', [ - 'filter' => '[[{"active", true}]]', - 'limit' => 1, - 'page' => 1 - ]); - - // Checks that the response is correct. - $this->assertEquals(200, $response->getStatusCode(), 'Incorrect status code.'); - $this->assertEquals(true, method_exists($response, 'getContent'), 'Incorrect response.'); - - // Checks that the content is correct. - $content = json_decode($response->getContent()); - $this->assertEquals(true, is_object($content), 'Incorrect content type.'); - - // Checks set props. - $this->assertEquals(true, isset($content->total), 'No total.'); - $this->assertEquals(true, isset($content->per_page), 'No per_page.'); - $this->assertEquals(true, isset($content->current_page), 'No current_page.'); - $this->assertEquals(true, isset($content->last_page), 'No last_page.'); - $this->assertEquals(true, isset($content->from), 'No from.'); - $this->assertEquals(true, isset($content->to), 'No to.'); - $this->assertEquals(true, isset($content->data), 'No data.'); - - // Checks prop types. - $this->assertEquals(true, is_numeric($content->total), 'Incorrect total type.'); - $this->assertEquals(true, is_numeric($content->per_page), 'Incorrect per_page type.'); - $this->assertEquals(true, is_numeric($content->current_page), 'Incorrect current_page type.'); - $this->assertEquals(true, is_numeric($content->last_page), 'Incorrect last_page type.'); - $this->assertEquals(true, is_numeric($content->from), 'Incorrect from type.'); - $this->assertEquals(true, is_numeric($content->to), 'Incorrect to type.'); - $this->assertEquals(true, is_array($content->data), 'Incorrect data type.'); - - // Checks prop content. - $this->assertEquals(static::$statements, $content->total, 'Incorrect total value.'); - $this->assertEquals(1, $content->per_page, 'Incorrect per_page value.'); - $this->assertEquals(1, $content->current_page, 'Incorrect current_page value.'); - $this->assertEquals(static::$statements, $content->last_page, 'Incorrect last_page value.'); - $this->assertEquals(1, $content->from, 'Incorrect from value.'); - $this->assertEquals(1, $content->to, 'Incorrect to value.'); - $this->assertEquals(1, count($content->data), 'Incorrect data count.'); - $this->assertEquals(true, isset($content->data[0]), 'No data item.'); - $this->assertEquals(true, is_object($content->data[0]), 'Incorrect data item type.'); - $this->assertEquals(true, isset($content->data[0]->statement), 'No statement.'); - $this->assertEquals(true, is_object($content->data[0]->statement), 'Incorrect statement type.'); - $this->assertEquals(true, isset($content->data[0]->statement->actor), 'No actor.'); - $this->assertEquals(true, is_object($content->data[0]->statement->actor), 'Incorrect actor type.'); - } - - public function tearDown() { - parent::tearDown(); - } -} diff --git a/app/tests/API/TestCase.php b/app/tests/API/TestCase.php deleted file mode 100644 index 351d54a182..0000000000 --- a/app/tests/API/TestCase.php +++ /dev/null @@ -1,108 +0,0 @@ -<?php namespace Tests\API; -use \Locker\Helpers\Helpers as Helpers; -use \Illuminate\Foundation\Testing\TestCase as Base; -use \User as User; -use \Site as Site; -use \Lrs as Lrs; -use \Auth as Auth; -use \Route as Route; -use \Statement as Statement; - -abstract class TestCase extends Base { - static protected $endpoint = '/api/v1/...'; - static protected $statements = 5; - protected $user = null; - protected $lrs = null; - - public function setup() { - parent::setup(); - $this->user = $this->createUser(); - Auth::login($this->user); - $this->lrs = $this->createLRS(); - $this->createStatements(); - } - - public function createApplication() { - $unitTesting = true; - $testEnvironment = 'testing'; - return require __DIR__ . '/../../../bootstrap/start.php'; - } - - protected function createUser() { - $user = User::firstOrCreate(['email' => 'test@example.com']); - - // If first user, create site object - if (User::count() === 1) { - $site = new Site([ - 'name' => 'Test Site', - 'email' => $user->email, - 'lang' => 'en-US', - 'create_lrs' => ['super'], - 'registration' => 'Closed', - 'restrict' => 'None', - 'domain' => '', - 'super' => [['user' => $user->_id]], - ]); - $site->save(); - } - return $user; - } - - protected function createLRS() { - $lrs = new Lrs([ - 'title' => 'TestLRS', - 'api' => [ - 'basic_key' => Helpers::getRandomValue(), - 'basic_secret' => Helpers::getRandomValue() - ], - 'owner' => [ - '_id' => Auth::user()->_id, - ], - 'users' => [[ - '_id' => Auth::user()->_id, - 'email' => Auth::user()->email, - 'name' => Auth::user()->name, - 'role' => 'admin' - ]], - 'domain' => '', - ]); - - $lrs->save(); - - // Hack header request - $_SERVER['SERVER_NAME'] = $lrs->title . '.com.vn'; - return $lrs; - } - - protected function createStatements() { - $statement = $this->getStatement(); - for ($i = 0; $i < static::$statements; $i += 1) { - $response = $this->requestAPI('POST', '/data/xAPI/statements', $statement); - } - } - - protected function getHeaders($auth) { - return [ - 'PHP_AUTH_USER' => $auth['basic_key'], - 'PHP_AUTH_PW' => $auth['basic_secret'] - ]; - } - - protected function requestAPI($method = 'GET', $url = '', $content = '') { - $server = $this->getHeaders($this->lrs->api); - $server['HTTP_X-Experience-API-Version'] = '1.0.1'; - Route::enableFilters(); - return $this->call($method, $url, [], [], $server, $content); - } - - protected function getStatement() { - return file_get_contents(__DIR__ . '/../Fixtures/statement.json'); - } - - public function tearDown() { - Statement::where('lrs._id', $this->lrs->_id)->delete(); - $this->lrs->delete(); - $this->user->delete(); - parent::tearDown(); - } -} diff --git a/app/tests/ActivityTest.php b/app/tests/ActivityTest.php deleted file mode 100644 index 2168d5d17d..0000000000 --- a/app/tests/ActivityTest.php +++ /dev/null @@ -1,33 +0,0 @@ -<?php - -class ActivityTest extends TestCase { - - /** - * Test Activity - */ - public function testActivityCRUD() { - $activity = new Activity; - $activity->_id = \Locker\Helpers\Helpers::getRandomValue(); - $activity->definition = array( - 'type' => \Locker\Helpers\Helpers::getRandomValue(), - 'name' => array( - 'en-US' => \Locker\Helpers\Helpers::getRandomValue() - ), - 'description' => array( - 'en-US' => \Locker\Helpers\Helpers::getRandomValue() - ) - ); - $result = $activity->save(); - $this->assertTrue($result); - - // Load activity from db - $aid = $activity->_id; - $db_activity = Activity::find($aid); - $this->assertEquals($db_activity->_id, $activity->_id); - - // Delete activity - $db_activity->delete(); - $this->assertEquals(Activity::find($aid), NULL); - } -} - diff --git a/app/tests/ApiV1QueryAnalyticsTest.php b/app/tests/ApiV1QueryAnalyticsTest.php deleted file mode 100644 index f89dd6166d..0000000000 --- a/app/tests/ApiV1QueryAnalyticsTest.php +++ /dev/null @@ -1,122 +0,0 @@ -<?php - -class ApiV1QueryAnalyticsTest extends TestCase { - - public function setUp() { - // Calls parent setup. - parent::setUp(); - - // Authenticates as super user. - Route::enableFilters(); - $user = User::firstOrCreate(['email' => 'quan@ll.com']); - Auth::login($user); - $this->createLRS(); - - // Creates testing statements. - $vs = json_decode(file_get_contents(__DIR__ . '/Fixtures/Analytics.json'), true); - $statement = App::make('Locker\Repository\Statement\EloquentStatementRepository'); - $statement->create([json_decode(json_encode($vs))], $this->lrs); - - $vs2 = $vs; - $vs2['object']['definition']['type'] = 'http://activitystrea.ms/schema/2.0/badge'; - - $statement2 = App::make('Locker\Repository\Statement\EloquentStatementRepository'); - $statement2->create([json_decode(json_encode($vs2))], $this->lrs); - } - - private function callResponse($params = [], $lrs) { - $auth = [ - 'PHP_AUTH_USER' => $lrs->api['basic_key'], - 'PHP_AUTH_PW' => $lrs->api['basic_secret'] - ]; - $route = '/api/v1/query/analytics'; - - return $this->call('GET', $route, $params, [], $auth); - } - - public function testDefaultQuery() { - $response = $this->callResponse([], $this->lrs); - $data = $response->getData(); - $this->assertEquals($data->version, 'v1'); - $this->assertEquals($data->route, 'api/v1/query/analytics'); - } - - public function testTimeQuery() { - $response = $this->callResponse(['type' => 'time'], $this->lrs); - $data = $response->getData()->data; - $this->assertEquals($data[0]->count, 2); - } - - public function testUserQuery() { - $response = $this->callResponse(['type' => 'user'], $this->lrs); - - $data = $response->getData()->data; - $checkTypeUser = TRUE; - foreach ($data as $value) { - if (!in_array($value->data->name, array('quanvm', 'quanvm2'))) { - $checkTypeUser = FALSE; - } - } - $this->assertTRUE($checkTypeUser); - } - - public function testVerbQuery() { - $response = $this->callResponse(['type' => 'verb'], $this->lrs); - $data = $response->getData()->data; - $this->assertEquals($data[0]->data->id, "http://adlnet.gov/expapi/verbs/experienced"); - } - - public function testDayQuery() { - $intervalLrs = Lrs::find('536b02d4c01f1325618b4567'); - if ($intervalLrs) { - $response = $this->callResponse(['interval' => 'Day'], $intervalLrs); - $data = $response->getData()->data; - $this->assertEquals(count($data), 2); - } - } - - public function testMonthQuery() { - $intervalLrs = Lrs::find('536b03bbc01f13a6618b4567'); - if ($intervalLrs) { - $response = $this->callResponse(['interval' => 'Month'], $intervalLrs); - $data = $response->getData()->data; - $this->assertEquals(count($data), 2); - } - } - - public function testYearQuery() { - $intervalLrs = Lrs::find('536b05ccc01f1392638b4567'); - if ($intervalLrs) { - $response = $this->callResponse(['interval' => 'Year'], $intervalLrs); - $data = $response->getData()->data; - $this->assertEquals(count($data), 2); - } - } - - public function testSinceQuery() { - $response = $this->callResponse(['since' => date('Y-m-d')], $this->lrs); - $data = $response->getData()->data; - $this->assertEquals($data[0]->count, 2); - } - - public function testEmptySinceQuery() { - $date = date('Y-m-d', strtotime("+1 day")); - $response = $this->callResponse(['since' => $date], $this->lrs); - $data = $response->getData()->data; - $this->assertTrue(empty($data)); - } - - public function testUntilQuery() { - $date = date('Y-m-d', strtotime("+1 day")); - $response = $this->callResponse(['until' => $date], $this->lrs); - $data = $response->getData()->data; - $this->assertEquals($data[0]->count, 2); - } - - public function testEmptyUntilQuery() { - $date = date('Y-m-d', strtotime("-1 day")); - $response = $this->callResponse(['until' => $date], $this->lrs); - $data = $response->getData()->data; - $this->assertTrue(empty($data)); - } -} diff --git a/app/tests/AuthorityTest.php b/app/tests/AuthorityTest.php deleted file mode 100644 index eaa7de93f7..0000000000 --- a/app/tests/AuthorityTest.php +++ /dev/null @@ -1,49 +0,0 @@ -<?php - -/** - * Test authority in statment - * - * @see https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#authority - */ -class AuthorityTest extends TestCase -{ - - public $lrs; - protected $statement; - - public function setUp() - { - parent::setUp(); - // Authentication as super user. - $user = User::firstOrCreate(array('email' => $this->dummyEmail())); - Auth::login($user); - $this->lrs = $this->createLRS(); - $this->statement = App::make('Locker\Repository\Statement\EloquentStatementRepository'); - } - - public function testAuthority() - { - $stmt = $this->defaultStatment(); - - // Ensure if authority is empty the LRS will be create anonymous authoriry - $authority = $stmt['authority']; - unset($stmt['authority']); - $return = $this->createStatement($stmt, $this->lrs); - $this->assertEquals(gettype($return), 'array'); - - $stmt_id = reset($return); - $obj_stmt = $this->statement->show($this->lrs->_id, $stmt_id)->first(); - $stmt_authority = $obj_stmt->statement['authority']; - $this->assertTrue(!empty($stmt_authority)); - - // Ensure authority stored in db is same value with statment send to LRS - $stmt['authority'] = $authority; - $return = $this->createStatement($stmt, $this->lrs); - $stmt_id = reset($return); - $obj_stmt = $this->statement->show($this->lrs->_id, $stmt_id)->first(); - - $stmt_authority = $obj_stmt->statement['authority']; - $this->assertEquals($authority, $stmt_authority); - } - -} diff --git a/app/tests/ExampleTest.php b/app/tests/ExampleTest.php deleted file mode 100644 index 39c122901a..0000000000 --- a/app/tests/ExampleTest.php +++ /dev/null @@ -1,17 +0,0 @@ -<?php - -class ExampleTest extends TestCase -{ - /** - * A basic functional test example. - * - * @return void - */ - public function testBasicExample() - { - $crawler = $this->client->request('GET', '/'); - - $this->assertTrue($this->client->getResponse()->isOk()); - } - -} diff --git a/app/tests/Fixtures/Analytics.json b/app/tests/Fixtures/Analytics.json deleted file mode 100644 index 2ec09a1543..0000000000 --- a/app/tests/Fixtures/Analytics.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "actor": { - "objectType": "Agent", - "mbox": "mailto:duy.nguyen@go1.com.au", - "name": "quanvm" - }, - "verb": { - "id": "http://adlnet.gov/expapi/verbs/experienced", - "display": {"und": "experienced"} - }, - "context": { - "contextActivities": { - "parent": [{ - "id": "http://tincanapi.com/GolfExample_TCAPI", - "objectType": "Activity" - }], - "grouping": [{ - "id": "http://tincanapi.com/GolfExample_TCAPI", - "objectType": "Activity" - }] - } - }, - "object": { - "id": "http://tincanapi.com/GolfExample_TCAPI/Playing/Scoring.html", - "objectType": "Activity", - "definition": { - "name": { - "en-US": "Scoring" - }, - "description": { - "en-US": "An overview of how to score a round of golf." - }, - "type": "http://activitystrea.ms/schema/1.0/badge" - } - }, - "authority": { - "name": "", - "mbox": "mailto:quan@ll.com", - "objectType": "Agent" - } -} \ No newline at end of file diff --git a/app/tests/Fixtures/Statements/Invalid/Actor/Group/Member/object-type-is-not-agent.json b/app/tests/Fixtures/Statements/Invalid/Actor/Group/Member/object-type-is-not-agent.json deleted file mode 100644 index b75a1b373c..0000000000 --- a/app/tests/Fixtures/Statements/Invalid/Actor/Group/Member/object-type-is-not-agent.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "id": "88664422-1234-5678-1234-567812345678", - "actor": { - "objectType": "Group", - "member": [ - { - "objectType": "Agent", - "mbox": "mailto:bob@example.com" - }, - { - "objectType": "Group", - "mbox": "mailto:group@example.com" - } - ] - }, - "verb": { - "id": "http://adlnet.gov/expapi/verbs/created", - "display": { - "en-US": "created" - } - }, - "object": { - "id": "http://ZackPierce.github.io/xAPI-Validator-JS", - "objectType": "Activity" - } -} \ No newline at end of file diff --git a/app/tests/Fixtures/Statements/Invalid/Actor/Group/missing-member.json b/app/tests/Fixtures/Statements/Invalid/Actor/Group/missing-member.json deleted file mode 100644 index 4a6c28c42c..0000000000 --- a/app/tests/Fixtures/Statements/Invalid/Actor/Group/missing-member.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "88664422-1234-5678-1234-567812345678", - "actor": { - "objectType": "Group" - }, - "verb": { - "id": "http://adlnet.gov/expapi/verbs/created", - "display": { - "en-US": "created" - } - }, - "object": { - "id": "http://ZackPierce.github.io/xAPI-Validator-JS", - "objectType": "Activity" - } -} \ No newline at end of file diff --git a/app/tests/Fixtures/Statements/Invalid/Actor/Mbox/invalid-format.json b/app/tests/Fixtures/Statements/Invalid/Actor/Mbox/invalid-format.json deleted file mode 100644 index 0a922804dc..0000000000 --- a/app/tests/Fixtures/Statements/Invalid/Actor/Mbox/invalid-format.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "88664422-1234-5678-1234-567812345678", - "actor": { - "mbox": "A@b@c@example.com" - }, - "verb": { - "id": "http://adlnet.gov/expapi/verbs/created", - "display": { - "en-US": "created" - } - }, - "object": { - "id": "http://ZackPierce.github.io/xAPI-Validator-JS", - "objectType": "Activity" - } -} \ No newline at end of file diff --git a/app/tests/Fixtures/Statements/Invalid/Actor/missing-actor.json b/app/tests/Fixtures/Statements/Invalid/Actor/missing-actor.json deleted file mode 100644 index 950d2d9831..0000000000 --- a/app/tests/Fixtures/Statements/Invalid/Actor/missing-actor.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "54321876-b88b-4b20-a0a5-a4c32391aaa0", - "verb": { - "id": "http://adlnet.gov/expapi/verbs/created", - "display": { - "en-US": "created" - } - }, - "object": { - "id": "http://ZackPierce.github.io/xAPI-Validator-JS", - "objectType": "Activity" - }, - "verb": { - "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced", - "display": { - "und": "experienced" - } - } -} \ No newline at end of file diff --git a/app/tests/Fixtures/Statements/Invalid/Attachment/content-type-is-not-string.json b/app/tests/Fixtures/Statements/Invalid/Attachment/content-type-is-not-string.json deleted file mode 100644 index d8b5605139..0000000000 --- a/app/tests/Fixtures/Statements/Invalid/Attachment/content-type-is-not-string.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "id": "54321876-b88b-4b20-a0a5-a4c32391aaa0", - "actor": { - "mbox": "mailto:zachary+pierce@gmail.com" - }, - "verb": { - "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced", - "display": { - "und": "experienced" - } - }, - "object": { - "id": "http://ZackPierce.github.io/xAPI-Validator-JS", - "objectType": "Activity" - }, - "attachments": [ - { - "usageType": "http://example.com/usage/info/A", - "display": {"en-GB":"hello world"}, - "contentType": 1.23, - "length": 654, - "sha2": "71b9be12fb30c4648b5f17b37d705440c78bbec56b4e30a40deeb5f163e617f2" - } - ] -} diff --git a/app/tests/Fixtures/Statements/Invalid/Attachment/display.json b/app/tests/Fixtures/Statements/Invalid/Attachment/display.json deleted file mode 100644 index 67bf027986..0000000000 --- a/app/tests/Fixtures/Statements/Invalid/Attachment/display.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "id": "54321876-b88b-4b20-a0a5-a4c32391aaa0", - "actor": { - "mbox": "mailto:zachary+pierce@gmail.com" - }, - "verb": { - "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced", - "display": { - "und": "experienced" - } - }, - "object": { - "id": "http://ZackPierce.github.io/xAPI-Validator-JS", - "objectType": "Activity" - }, - "attachments": [ - { - "usageType": "http://example.com/usage/info/A", - "display": { - "$": "hello" - }, - "contentType": "text/plain", - "length": 100, - "sha2": "71b9be12fb30c4648b5f17b37d705440c78bbec56b4e30a40deeb5f163e617f2" - } - ] -} diff --git a/app/tests/Fixtures/Statements/Invalid/Attachment/length-is-not-integer.json b/app/tests/Fixtures/Statements/Invalid/Attachment/length-is-not-integer.json deleted file mode 100644 index 8f10f365a3..0000000000 --- a/app/tests/Fixtures/Statements/Invalid/Attachment/length-is-not-integer.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "id": "54321876-b88b-4b20-a0a5-a4c32391aaa0", - "actor": { - "mbox": "mailto:zachary+pierce@gmail.com" - }, - "verb": { - "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced", - "display": { - "und": "experienced" - } - }, - "object": { - "id": "http://ZackPierce.github.io/xAPI-Validator-JS", - "objectType": "Activity" - }, - "attachments": [ - { - "usageType": "http://example.com/usage/info/A", - "display": {"en-GB":"hello world"}, - "contentType": "text/plain", - "length": "100MB", - "sha2": "71b9be12fb30c4648b5f17b37d705440c78bbec56b4e30a40deeb5f163e617f2" - } - ] -} diff --git a/app/tests/Fixtures/Statements/Invalid/Attachment/missing-sha2.json b/app/tests/Fixtures/Statements/Invalid/Attachment/missing-sha2.json deleted file mode 100644 index 8a016e9286..0000000000 --- a/app/tests/Fixtures/Statements/Invalid/Attachment/missing-sha2.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "id": "54321876-b88b-4b20-a0a5-a4c32391aaa0", - "actor": { - "mbox": "mailto:zachary+pierce@gmail.com" - }, - "verb": { - "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced", - "display": { - "und": "experienced" - } - }, - "object": { - "id": "http://ZackPierce.github.io/xAPI-Validator-JS", - "objectType": "Activity" - }, - "attachments": [ - { - "usageType": "http://example.com/usage/info/A", - "display": {"en-GB":"hello world"}, - "contentType": "text/plain", - "length": 100 - } - ] -} diff --git a/app/tests/Fixtures/Statements/Invalid/Attachment/missing-usage-type.json b/app/tests/Fixtures/Statements/Invalid/Attachment/missing-usage-type.json deleted file mode 100644 index fea1a0df39..0000000000 --- a/app/tests/Fixtures/Statements/Invalid/Attachment/missing-usage-type.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "id": "54321876-b88b-4b20-a0a5-a4c32391aaa0", - "actor": { - "mbox": "mailto:zachary+pierce@gmail.com" - }, - "verb": { - "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced", - "display": { - "und": "experienced" - } - }, - "object": { - "id": "http://ZackPierce.github.io/xAPI-Validator-JS", - "objectType": "Activity" - }, - "attachments": [ - { - "display": {"en-GB":"hello world"}, - "contentType": "text/plain", - "length": 100, - "sha2": "71b9be12fb30c4648b5f17b37d705440c78bbec56b4e30a40deeb5f163e617f2" - } - ] -} diff --git a/app/tests/Fixtures/Statements/Invalid/Attachment/sha2-is-not-valid.json b/app/tests/Fixtures/Statements/Invalid/Attachment/sha2-is-not-valid.json deleted file mode 100644 index c131f9dc41..0000000000 --- a/app/tests/Fixtures/Statements/Invalid/Attachment/sha2-is-not-valid.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "id": "54321876-b88b-4b20-a0a5-a4c32391aaa0", - "actor": { - "mbox": "mailto:zachary+pierce@gmail.com" - }, - "verb": { - "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced", - "display": { - "und": "experienced" - } - }, - "object": { - "id": "http://ZackPierce.github.io/xAPI-Validator-JS", - "objectType": "Activity" - }, - "attachments": [ - { - "usageType": "http://example.com/usage/info/A", - "display": {"en-GB":"hello world"}, - "contentType": "text/plain", - "length": 100, - "sha2": "invalid-sha2-string" - } - ] -} diff --git a/app/tests/Fixtures/Statements/Invalid/Authority/Member/wrong-object-type.json b/app/tests/Fixtures/Statements/Invalid/Authority/Member/wrong-object-type.json deleted file mode 100644 index 57b6b86785..0000000000 --- a/app/tests/Fixtures/Statements/Invalid/Authority/Member/wrong-object-type.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "id": "54321876-b88b-4b20-a0a5-a4c32391aaa0", - "actor": { - "mbox": "mailto:zachary+pierce@gmail.com" - }, - "verb": { - "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced", - "display": { - "und": "experienced" - } - }, - "object": { - "id": "http://ZackPierce.github.io/xAPI-Validator-JS", - "objectType": "Activity" - }, - "authority": { - "objectType": "Group", - "member": [ - { - "objectType": "Agent", - "mbox": "mailto:bob@example.com" - }, - { - "objectType": "Group", - "mbox": "mailto:group@example.com" - } - ] - } -} diff --git a/app/tests/Fixtures/Statements/Invalid/Verb/missing-display.json b/app/tests/Fixtures/Statements/Invalid/Verb/missing-display.json deleted file mode 100644 index e5195cfe93..0000000000 --- a/app/tests/Fixtures/Statements/Invalid/Verb/missing-display.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "actor": { - "mbox": "mailto:zachary+pierce@gmail.com" - }, - "verb": { - "id": "http:\/\/adlnet.gov\/expapi\/verbs\/experienced" - }, - "object": { - "id": "http://ZackPierce.github.io/xAPI-Validator-JS", - "objectType": "Activity" - } -} diff --git a/app/tests/Fixtures/Statements/Valid/Actor/group.json b/app/tests/Fixtures/Statements/Valid/Actor/group.json deleted file mode 100644 index 77926f0ff1..0000000000 --- a/app/tests/Fixtures/Statements/Valid/Actor/group.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "id": "88664422-1234-5678-1234-567812345678", - "actor": { - "objectType": "Group", - "member": [ - { - "objectType": "Agent", - "mbox": "mailto:bob@example.com" - }, - { - "objectType": "Agent", - "mbox": "mailto:group@example.com" - } - ] - }, - "verb": { - "id": "http://adlnet.gov/expapi/verbs/created", - "display": { - "en-US": "created" - } - }, - "object": { - "id": "http://ZackPierce.github.io/xAPI-Validator-JS", - "objectType": "Activity" - } -} \ No newline at end of file diff --git a/app/tests/Fixtures/Statements/Valid/Object/with-definition.json b/app/tests/Fixtures/Statements/Valid/Object/with-definition.json deleted file mode 100644 index 7bab3ca69e..0000000000 --- a/app/tests/Fixtures/Statements/Valid/Object/with-definition.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "id": "12345678-1234-5678-1234-567812345678", - "actor": { - "mbox": "mailto:zachary+pierce@gmail.com" - }, - "verb": { - "id": "http://adlnet.gov/expapi/verbs/created", - "display": { - "en-US": "created" - } - }, - "object": { - "id": "http:\/\/tincanapi.com\/GolfExample_TCAPI\/Playing\/Scoring.html", - "objectType": "Activity", - "definition": { - "name": { - "en-US": "Scoring" - }, - "description": { - "en-US": "An overview of how to score a round of golf." - } - } - } -} diff --git a/app/tests/Fixtures/Statements/Valid/Verb/Display/multilingual.json b/app/tests/Fixtures/Statements/Valid/Verb/Display/multilingual.json deleted file mode 100644 index 947ad1d520..0000000000 --- a/app/tests/Fixtures/Statements/Valid/Verb/Display/multilingual.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "id": "12345678-1234-5678-1234-567812345678", - "actor": { - "mbox": "mailto:zachary+pierce@gmail.com" - }, - "verb": { - "id": "http://adlnet.gov/expapi/verbs/passed/", - "display": { - "en-US": "created", - "en-US": "created", - "vn": "tạo" - } - }, - "object": { - "id": "http://ZackPierce.github.io/xAPI-Validator-JS", - "objectType": "Activity" - } -} \ No newline at end of file diff --git a/app/tests/Fixtures/Statements/Valid/simple.json b/app/tests/Fixtures/Statements/Valid/simple.json deleted file mode 100644 index 957017d7e7..0000000000 --- a/app/tests/Fixtures/Statements/Valid/simple.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "id": "12345678-1234-5678-1234-567812345678", - "actor": { - "mbox": "mailto:zachary+pierce@gmail.com" - }, - "verb": { - "id": "http://adlnet.gov/expapi/verbs/created", - "display": { - "en-US": "created" - } - }, - "object": { - "id": "http://ZackPierce.github.io/xAPI-Validator-JS", - "objectType": "Activity" - } -} \ No newline at end of file diff --git a/app/tests/Fixtures/Statements/Valid/with-authority.json b/app/tests/Fixtures/Statements/Valid/with-authority.json deleted file mode 100644 index 8d87e9a0f5..0000000000 --- a/app/tests/Fixtures/Statements/Valid/with-authority.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "id": "12345678-1234-5678-1234-567812345678", - "actor": { - "mbox": "mailto:zachary+pierce@gmail.com" - }, - "verb": { - "id": "http://adlnet.gov/expapi/verbs/created", - "display": { - "en-US": "created" - } - }, - "object": { - "id": "http://ZackPierce.github.io/xAPI-Validator-JS", - "objectType": "Activity" - }, - "authority": { - "name": "Quan Vo", - "mbox": "mailto:quan@ll.com", - "objectType": "Agent" - } -} \ No newline at end of file diff --git a/app/tests/Fixtures/Statements/Valid/with-context.json b/app/tests/Fixtures/Statements/Valid/with-context.json deleted file mode 100644 index e01466d3a0..0000000000 --- a/app/tests/Fixtures/Statements/Valid/with-context.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "id": "12345678-1234-5678-1234-567812345678", - "actor": { - "mbox": "mailto:zachary+pierce@gmail.com" - }, - "verb": { - "id": "http://adlnet.gov/expapi/verbs/created", - "display": { - "en-US": "created" - } - }, - "object": { - "id": "http://ZackPierce.github.io/xAPI-Validator-JS", - "objectType": "Activity" - }, - "context": { - "registration": "ec531277-b57b-4c15-8d91-d292c5b2b8f7", - "contextActivities": { - "parent": [ - { - "id": "http://www.example.com/meetings/series/267", - "objectType": "Activity" - } - ], - "category": [ - { - "id": "http://www.example.com/meetings/categories/teammeeting", - "objectType": "Activity", - "definition": { - "name": { - "en": "team meeting" - }, - "description": { - "en": "A category of meeting used for regular team meetings." - }, - "type": "http://example.com/expapi/activities/meetingcategory" - } - } - ], - "other": [ - { - "id": "http://www.example.com/meetings/occurances/3425567", - "objectType": "Activity" - } - ] - } - } -} \ No newline at end of file diff --git a/app/tests/Fixtures/pipeline.json b/app/tests/Fixtures/pipeline.json deleted file mode 100644 index dcbeec71d0..0000000000 --- a/app/tests/Fixtures/pipeline.json +++ /dev/null @@ -1 +0,0 @@ -[{"$match":{"active":true}},{"$project":{"_id":0,"statement":1}}] diff --git a/app/tests/InstanceTestCase.php b/app/tests/InstanceTestCase.php new file mode 100644 index 0000000000..3577925d45 --- /dev/null +++ b/app/tests/InstanceTestCase.php @@ -0,0 +1,41 @@ +<?php namespace Tests; + +abstract class InstanceTestCase extends TestCase { + protected $user, $site; + + public function setUp() { + parent::setUp(); + $this->user = $this->createUser(); + $this->site = $this->createSite($this->user); + } + + protected function createSite(\User $user) { + $model = new \Site([ + 'name' => 'Test', + 'description' => '', + 'email' => $user->email, + 'lang' => 'en-US', + 'create_lrs' => 'super', + 'registration' => 'Closed', + 'restrict' => 'None', + 'domain' => '', + 'super' => [['user' => $user->_id]] + ]); + $model->save(); + return $model; + } + + protected function createUser() { + $model = new \User([ + 'email' => 'test@example.com' + ]); + $model->save(); + return $model; + } + + public function tearDown() { + $this->site->delete(); + $this->user->delete(); + parent::tearDown(); + } +} diff --git a/app/tests/LrsTest.php b/app/tests/LrsTest.php deleted file mode 100644 index defd20a775..0000000000 --- a/app/tests/LrsTest.php +++ /dev/null @@ -1,107 +0,0 @@ -<?php - -class LrsTest extends TestCase -{ - - public function setUp() - { - parent::setUp(); - - Route::enableFilters(); - - // Authentication as super user. - $user = User::firstOrCreate(['email' => 'quan@ll.com']); - Auth::login($user); - } - - /** - * Test LRS - */ - public function testLRS() - { - $lrs = new Lrs; - - // Test title required. - $values = array( - 'title' => '', - 'description' => \Locker\Helpers\Helpers::getRandomValue(), - 'api' => array('basic_key' => \Locker\Helpers\Helpers::getRandomValue(), - 'basic_secret' => \Locker\Helpers\Helpers::getRandomValue()) - ); - $validator = $lrs->validate($values); - $this->assertTrue($validator->fails()); - $this->assertFalse($validator->passes()); - - $values['title'] = \Locker\Helpers\Helpers::getRandomValue(); - $validator = $lrs->validate($values); - $this->assertTrue($validator->passes()); - - // Validate auth_service - - $values['auth_service_url'] = 'http://' . \Locker\Helpers\Helpers::getRandomValue() . '.adurolms.com'; - $validator = $lrs->validate($values); - $this->assertTrue($validator->passes()); - - // Add new lrs - $lrs->title = $values['title']; - $lrs->description = $values['description']; - $lrs->api = $values['api']; - $result = $lrs->save(); - $this->assertTrue($result); - - // Load lrs from db - $lrs_id = $lrs->_id; - $db_lrs = Lrs::find($lrs_id); - $this->assertEquals($db_lrs->_id, $lrs->_id); - - // Edit lrs - $title = \Locker\Helpers\Helpers::getRandomValue(); - $db_lrs->title = $title; - $db_lrs->save(); - $this->assertEquals($db_lrs->title, $title); - - // Delete lrs - $db_lrs->delete(); - $this->assertEquals(Lrs::find($lrs_id), NULL, 'delete lrs'); - } - - public function testInternalAuthentication() - { - $this->createLRS(); - - //create client for Auth Service - $auth = [ - 'api_key' => $this->lrs->api['basic_key'], - 'api_secret' => $this->lrs->api['basic_secret'], - ]; - - $response = $this->_makeRequest($auth); - $this->assertEquals($response->getStatusCode(), 200); - } - - public function testAuthenticationService() - { - $this->createLRS(); - - //create client for Auth Service - $auth = [ - 'api_key' => $this->lrs->api['basic_key'], - 'api_secret' => $this->lrs->api['basic_secret'], - ]; - - // Make sure response data for the get request - $response = $this->_makeRequest($auth, ['auth_type' => 'central']); - $this->assertEquals($response->getStatusCode(), 200); - } - - private function _makeRequest($auth, $param = []) - { - return $this->call("GET", '/data/xAPI/statements', $param, [], $this->makeRequestHeaders($auth)); - } - - public function testEnpoint() - { - $this->assertTrue(true); - } - -} diff --git a/app/tests/LrsTestCase.php b/app/tests/LrsTestCase.php new file mode 100644 index 0000000000..095fc94564 --- /dev/null +++ b/app/tests/LrsTestCase.php @@ -0,0 +1,39 @@ +<?php namespace Tests; +use \Locker\Helpers\Helpers as Helpers; + +abstract class LrsTestCase extends InstanceTestCase { + protected $lrs; + + public function setUp() { + parent::setUp(); + $this->lrs = $this->createLrs($this->user); + $_SERVER['SERVER_NAME'] = $this->lrs->title.'.com.vn'; + } + + protected function createLrs(\User $user) { + $model = new \Lrs([ + 'title' => Helpers::getRandomValue(), + 'description' => Helpers::getRandomValue(), + 'subdomain' => Helpers::getRandomValue(), + 'api' => [ + 'basic_key' => helpers::getRandomValue(), + 'basic_secret' => helpers::getRandomValue() + ], + 'owner' => ['_id' => $user->_id], + 'users' => Helpers::getRandomValue(), + 'users' => [[ + '_id' => $user->_id, + 'email' => $user->email, + 'name' => $user->name, + 'role' => 'admin' + ]] + ]); + $model->save(); + return $model; + } + + public function tearDown() { + $this->lrs->delete(); + parent::tearDown(); + } +} diff --git a/app/tests/StatementContextTest.php b/app/tests/StatementContextTest.php deleted file mode 100644 index 569f257d79..0000000000 --- a/app/tests/StatementContextTest.php +++ /dev/null @@ -1,41 +0,0 @@ -<?php - -class StatementContextTest extends TestCase -{ - - public function setUp() - { - parent::setUp(); - // Authentication as super user. - $user = User::firstOrCreate(array('email' => $this->dummyEmail())); - Auth::login($user); - $this->lrs = $this->createLRS(); - $this->statement = App::make('Locker\Repository\Statement\EloquentStatementRepository'); - } - - /** - * The LRS MUST return single Activity Objects as an array of length one - * containing the same Activity. - */ - public function testContextActivities() - { - $stmt = $this->defaultStatment(); - // $parent = new stdClass(); - // $parent->id = 'http://tincanapi.com/GolfExample_TCAPI'; - // $parent->objectType = 'Activity'; - $parent = [ - 'id' => 'http://tincanapi.com/GolfExample_TCAPI', - 'objectType' => 'Activity' - ]; - $contextActivities = ['parent' => [$parent]]; // $parent should be an array not an object. - $stmt['context']['contextActivities'] = $contextActivities; - - $return = $this->createStatement($stmt, $this->lrs); - - $id = $return[0]; - $saved_statement = $this->statement->show($this->lrs->_id, $id)->first(); - // The parent must be array. - $this->assertTrue(is_array($saved_statement['statement']['context']['contextActivities']['parent'])); - } - -} diff --git a/app/tests/StatementGetTest.php b/app/tests/StatementGetTest.php deleted file mode 100644 index 48d3cdfebd..0000000000 --- a/app/tests/StatementGetTest.php +++ /dev/null @@ -1,61 +0,0 @@ -<?php - -class StatementGetTest extends TestCase -{ - - public function setUp() - { - parent::setUp(); - - Route::enableFilters(); - - // Authentication as super user. - $user = User::firstOrCreate(['email' => 'quan@ll.com']); - Auth::login($user); - } - - private function _makeRequest($auth, $version) - { - return $this->call("GET", '/data/xAPI/statements', [], [], $this->makeRequestHeaders($auth, $version)); - } - - /** - * Create statements for lrs - * - * @param string $version Make sure LRS response to all valid version. - * @return void - * @dataProvider dataGetAuthService - */ - public function testGetAuthService($version, $expecting_code) - { - $lrs = $this->createLRS(); - - // create client for Auth Service - $auth = [ - 'api_key' => $this->lrs->api['basic_key'], - 'api_secret' => $this->lrs->api['basic_secret'], - ]; - - - // Make sure response data for the get request - $response = $this->_makeRequest($auth, $version); - $this->assertEquals($expecting_code, $response->getStatusCode()); - - $lrs->delete(); - } - - public function dataGetAuthService() { - $data = []; - - foreach (range(0, 20) as $i) { - if (array_rand([true, false])) { - $data[] = ["1.0.{$i}", 200]; - } - } - - //$data[] = ["0.9", 400]; - //$data[] = ["1.1", 400]; - - return $data; - } -} diff --git a/app/tests/StatementPostTest.php b/app/tests/StatementPostTest.php deleted file mode 100644 index 943f13cc1f..0000000000 --- a/app/tests/StatementPostTest.php +++ /dev/null @@ -1,114 +0,0 @@ -<?php - -class StatementPostTest extends TestCase -{ - - public function setUp() - { - parent::setUp(); - - Route::enableFilters(); - - // Authentication as super user. - $user = User::firstOrCreate(['email' => 'quan@ll.com']); - Auth::login($user); - } - - private function _makeRequest($param, $method, $auth) - { - return $this->call($method, '/data/xAPI/statements', [], [], $this->makeRequestHeaders($auth), !empty($param) ? json_encode($param) : []); - } - - /** - * Make a post request to LRS - * - * @return void - */ - public function testPostBehavior() - { - $this->createLRS(); - - $vs = $this->defaultStatment(); - $result = $this->createStatement($vs, $this->lrs); - - $statement = App::make('Locker\Repository\Statement\EloquentStatementRepository'); - $createdStatement = $statement->show($this->lrs->_id, $result[0])->first(); - - $param = array( - 'actor' => $createdStatement->statement['actor'], - 'verb' => $createdStatement->statement['verb'], - 'context' => $createdStatement->statement['context'], - 'object' => $createdStatement->statement['object'], - 'id' => $createdStatement->statement['id'], - 'timestamp' => $createdStatement->statement['timestamp'], - ); - - // create client for Auth Service - $auth = [ - 'api_key' => $this->lrs->api['basic_key'], - 'api_secret' => $this->lrs->api['basic_secret'], - ]; - - // case: conflict-matches - /* @var $response Illuminate\Http\JsonResponse */ - $response = $this->_makeRequest($param, "POST", $auth); - $this->assertEquals(200, $response->getStatusCode()); - - // case: conflict nomatch - $param['result'] = new \stdClass(); - try { - $response = $this->_makeRequest($param, "POST", $auth); - } catch (\Exception $ex) { - $this->assertEquals(true, method_exists($ex, 'getStatusCode')); - $this->assertEquals(409, $ex->getStatusCode()); - } - - // Make sure response data for the get request - $responseGet = $this->_makeRequest(new \stdClass(), "GET", $auth); - $this->assertEquals(200, $responseGet->getStatusCode()); - - // Make sure response data for the get request - unset($param['result']); - $responsePost = $this->_makeRequest($param, "POST", $auth); - $this->assertEquals(200, $responsePost->getStatusCode()); - } - - /** - * make a post request to lrs with Auth Service - * - * @return void - */ - public function testPostAuthService() - { - $this->createLRS(); - - $vs = $this->defaultStatment(); - $result = $this->createStatement($vs, $this->lrs); - - $statement = App::make('Locker\Repository\Statement\EloquentStatementRepository'); - $createdStatement = $statement->show($this->lrs->_id, $result[0])->first(); - - $param = [ - 'actor' => $createdStatement->statement['actor'], - 'verb' => $createdStatement->statement['verb'], - 'context' => $createdStatement->statement['context'], - 'object' => $createdStatement->statement['object'], - 'id' => $createdStatement->statement['id'], - 'timestamp' => $createdStatement->statement['timestamp'], - ]; - - // create client for Auth Service - $auth = [ - 'api_key' => $this->lrs->api['basic_key'], - 'api_secret' => $this->lrs->api['basic_secret'], - ]; - - $response = $this->_makeRequest($param, "POST", $auth); - - $responseData = $response->getContent(); - $responseStatus = $response->getStatusCode(); - - $this->assertEquals(200, $responseStatus); - } - -} diff --git a/app/tests/StatementPutTest.php b/app/tests/StatementPutTest.php deleted file mode 100644 index 13d328e418..0000000000 --- a/app/tests/StatementPutTest.php +++ /dev/null @@ -1,119 +0,0 @@ -<?php - -class StatementPutTest extends TestCase -{ - - public function setUp() - { - parent::setUp(); - - Route::enableFilters(); - - // Authentication as super user. - $user = User::firstOrCreate(['email' => 'quan@ll.com']); - Auth::login($user); - } - - private function _makeRequest($param, $auth) - { - return $this->call('PUT', '/data/xAPI/statements', ['statementId' => $param['id']], [], $this->makeRequestHeaders($auth), json_encode($param)); - } - - /** - * Create statements for lrs - * - * @return void - */ - public function testPutBehavior() - { - $this->createLRS(); - - $vs = $this->defaultStatment(); - $result = $this->createStatement($vs, $this->lrs); - - $statement = App::make('Locker\Repository\Statement\EloquentStatementRepository'); - $createdStatement = $statement->show($this->lrs->_id, $result[0])->first(); - - $param = array( - 'actor' => $createdStatement->statement['actor'], - 'verb' => $createdStatement->statement['verb'], - 'context' => $createdStatement->statement['context'], - 'object' => $createdStatement->statement['object'], - 'id' => $createdStatement->statement['id'], - 'timestamp' => $createdStatement->statement['timestamp'], - ); - - // create client for Auth Service - $auth = [ - 'api_key' => $this->lrs->api['basic_key'], - 'api_secret' => $this->lrs->api['basic_secret'], - ]; - - // case: conflict-matches - $response = $this->_makeRequest($param, $auth); - $responseData = method_exists($response, 'getData'); - $responseStatus = $response->getStatusCode(); - - $this->assertEquals(204, $responseStatus); - $this->assertEquals(false, $responseData); - - // case: conflict nomatch - $param['result'] = new \stdClass(); - try { - $response = $this->_makeRequest($param, $auth); - } catch (\Exception $ex) { - $this->assertEquals(true, method_exists($ex, 'getStatusCode')); - $this->assertEquals(409, $ex->getStatusCode()); - } - } - - /** - * Create statements for lrs with Auth Service - * - * @return void - */ - public function testPutAuthService() - { - $this->createLRS(); - - $vs = $this->defaultStatment(); - $result = $this->createStatement($vs, $this->lrs); - - $statement = App::make('Locker\Repository\Statement\EloquentStatementRepository'); - $createdStatement = $statement->show($this->lrs->_id, $result[0])->first(); - - $param = [ - 'actor' => $createdStatement->statement['actor'], - 'verb' => $createdStatement->statement['verb'], - 'context' => $createdStatement->statement['context'], - 'object' => $createdStatement->statement['object'], - 'id' => $createdStatement->statement['id'], - 'timestamp' => $createdStatement->statement['timestamp'], - ]; - - // create client for Auth Service - // create client for Auth Service - $auth = [ - 'api_key' => $this->lrs->api['basic_key'], - 'api_secret' => $this->lrs->api['basic_secret'], - ]; - - $response = $this->_makeRequest($param, $auth); - $responseData = method_exists($response, 'getData'); - $responseStatus = $response->getStatusCode(); - - $this->assertEquals(204, $responseStatus); - $this->assertEquals(false, $responseData); - } - - public function tearDown() - { - parent::tearDown(); - - // Need LRS table is empty because waiting the getLrsBySubdomain() - if ($this->lrs) { - $this->lrs->delete(); - } - } - -} diff --git a/app/tests/StatementTest.php b/app/tests/StatementTest.php deleted file mode 100644 index 6abdfbc996..0000000000 --- a/app/tests/StatementTest.php +++ /dev/null @@ -1,27 +0,0 @@ -<?php - -class StatementTest extends TestCase { - - public function setUp() { - parent::setUp(); - // Authentication as super user. - $user = User::firstOrCreate(array('email' => 'quan@ll.com')); - Auth::login($user); - $this->createLRS(); - } - - /** - * Create statements for lrs - * - * @return void - */ - public function testCreate() { - $vs = $this->defaultStatment(); - - $statement = App::make('Locker\Repository\Statement\EloquentStatementRepository'); - $result = $statement->create([json_decode(json_encode($vs))], $this->lrs); - - $this->assertTrue(is_array($result)); - } - -} diff --git a/app/tests/StatementVersionTest.php b/app/tests/StatementVersionTest.php deleted file mode 100644 index 7bb7cd62d7..0000000000 --- a/app/tests/StatementVersionTest.php +++ /dev/null @@ -1,40 +0,0 @@ -<?php - -class StatementVersionTest extends TestCase -{ - - public function setUp() - { - parent::setUp(); - - Route::enableFilters(); - - // Authentication as super user. - $user = User::firstOrCreate(['email' => 'andy@ll.com']); - Auth::login($user); - - $this->lrs = $this->createLRS(); - } - - /** - * If no 'version' provided in statement, LRS must save '1.0.0' as default - * as value. - */ - public function testMissingVersion() - { - $statement = $this->defaultStatment(); - - // make sure there is no version - $this->assertTrue(!isset($statement['version']), 'There is no version provided'); - - $return = $this->createStatement($statement, $this->lrs); - - $id = reset($return); - - /* @var $saved_statement Statement */ - $saved_statement = App::make('Locker\Repository\Statement\EloquentStatementRepository')->show($this->lrs->_id, $id)->first(); - $this->assertTrue(isset($saved_statement->statement['version']), 'There is no version provided'); - $this->assertEquals('1.0.0', $saved_statement->statement['version']); - } - -} diff --git a/app/tests/StatementsTestCase.php b/app/tests/StatementsTestCase.php new file mode 100644 index 0000000000..d74093901f --- /dev/null +++ b/app/tests/StatementsTestCase.php @@ -0,0 +1,57 @@ +<?php namespace Tests; +use \Locker\Helpers\Helpers as Helpers; + +abstract class StatementsTestCase extends LrsTestCase { + protected $statements; + + public function setUp() { + parent::setUp(); + $this->statements = [$this->createStatement( + $this->lrs, + $this->generateStatement() + )]; + } + + protected function generateStatement($statement = []) { + $timestamp = Helpers::getCurrentDate(); + return array_merge([ + 'actor' => [ + 'mbox' => 'mailto:test@example.com', + 'objectType' => 'Agent' + ], + 'verb' => [ + 'id' => 'http://www.example.com/verbs/test' + ], + 'object' => [ + 'id' => 'http://www.example.com/objects/test', + 'objectType' => 'Activity' + ], + 'timestamp' => $timestamp, + 'stored' => $timestamp, + 'authority' => [ + 'mbox' => 'mailto:test@example.com', + 'objectType' => 'Agent' + ] + ], $statement); + } + + protected function createStatement(\Lrs $lrs, array $statement) { + $model = new \Statement([ + 'lrs' => ['_id' => $lrs->_id], + 'statement' => $statement, + 'active' => true, + 'voided' => false, + 'refs' => [] + ]); + $model->timestamp = new \MongoDate(strtotime($model->statement['timestamp'])); + $model->save(); + return $model; + } + + public function tearDown() { + array_map(function ($statement) { + $statement->delete(); + }, $this->statements); + parent::tearDown(); + } +} diff --git a/app/tests/TestCase.php b/app/tests/TestCase.php index 4e791cb2e9..c39d6bab66 100644 --- a/app/tests/TestCase.php +++ b/app/tests/TestCase.php @@ -1,168 +1,16 @@ -<?php +<?php namespace Tests; +use \Illuminate\Foundation\Testing\TestCase as IlluminateTest; -use \Locker\Helpers\Helpers as helpers; - -class TestCase extends Illuminate\Foundation\Testing\TestCase -{ +abstract class TestCase extends IlluminateTest { /** * Creates the application. - * * @return \Symfony\Component\HttpKernel\HttpKernelInterface */ - public function createApplication() - { + public function createApplication() { $unitTesting = true; - $testEnvironment = 'testing'; - return require __DIR__ . '/../../bootstrap/start.php'; } - public function setUp() - { - parent::setUp(); - $user = User::firstOrCreate(['email' => 'quan@ll.com']); - //if first user, create site object - if (\User::count() == 1) { - $site = new \Site; - $site->name = 'Test'; - $site->description = ''; - $site->email = $user->email; - $site->lang = 'en-US'; - $site->create_lrs = array('super'); - $site->registration = 'Closed'; - $site->restrict = 'None'; //restrict registration to a specific email domain - $site->domain = ''; - $site->super = array(array('user' => $user->_id)); - $site->save(); - } - } - - /** - * Create dummy LRS - * @return \Lrs - */ - protected function createLRS() - { - $lrs = new Lrs; - $lrs->title = helpers::getRandomValue(); - $lrs->description = helpers::getRandomValue(); - $lrs->subdomain = helpers::getRandomValue(); - $lrs->api = array( - 'basic_key' => helpers::getRandomValue(), - 'basic_secret' => helpers::getRandomValue() - ); - - // $lrs->auth_service = property_exists($this, 'lrsAuthMethod') ? $this->lrsAuthMethod : Lrs::INTERNAL_LRS; - // $lrs->auth_service_url = property_exists($this, 'auth_service_url') ? - // $this->auth_service_url : ''; - // $lrs->token = 'our-token'; - - $lrs->owner = array('_id' => Auth::user()->_id); - $lrs->users = array( - array('_id' => Auth::user()->_id, - 'email' => Auth::user()->email, - 'name' => Auth::user()->name, - 'role' => 'admin' - ) - ); - - $lrs->save(); - $this->lrs = $lrs; - - // Hack header request - $_SERVER['SERVER_NAME'] = $this->lrs->title . '.com.vn'; - return $lrs; - } - - /** - * Return default statement data. - */ - protected function defaultStatment() - { - $siteAttrs = \Site::first(); - - return [ - 'actor' => [ - 'objectType' => 'Agent', - 'mbox' => 'mailto:duy.nguyen@go1.com.au', - 'name' => 'duynguyen' - ], - 'verb' => [ - "id" => "http://adlnet.gov/expapi/verbs/experienced", - "display" => ["und" => "experienced"] - ], - 'context' => [ - "contextActivities" => [ - "parent" => [[ - "id" => "http://tincanapi.com/GolfExample_TCAPI", - "objectType" => "Activity" - ]], - "grouping" => [[ - "id" => "http://tincanapi.com/GolfExample_TCAPI", - "objectType" => "Activity" - ]] - ] - ], - "object" => [ - "id" => "http://tincanapi.com/GolfExample_TCAPI/Playing/Scoring.html", - "objectType" => "Activity", - "definition" => [ - "name" => [ - "en-US" => "Scoring" - ], - "description" => [ - "en-US" => "An overview of how to score a round of golf." - ] - ] - ], - "authority" => [ - "name" => $siteAttrs->name, - "mbox" => "mailto:" . $siteAttrs->email, - "objectType" => "Agent" - ], - ]; - } - - /** - * Create dummy statement with lrs. - * - * @param Lrs $lrs - * @return type - */ - protected function createStatement($statement, $lrs) - { - return App::make('Locker\Repository\Statement\EloquentStatementRepository') - ->create([json_decode(json_encode($statement))], $lrs); - } - - /** - * Create dummy Auth Client - * @param type $lrs - * @return type - */ - protected function createClientAuth($auth) - { - return [ - 'name' => helpers::getRandomValue(), - 'api_key' => $auth['api_key'], - 'api_secret' => $auth['api_secret'], - ]; - } - - protected function dummyEmail() - { - return helpers::getRandomValue() . '@go1.com.au'; - } - - protected function makeRequestHeaders($auth, $version="1.0.1") - { - return [ - 'PHP_AUTH_USER' => $auth['api_key'], - 'PHP_AUTH_PW' => $auth['api_secret'], - 'HTTP_X-Experience-API-Version' => $version - ]; - } - } diff --git a/app/tests/Fixtures/Repos/StatementFormatter1.json b/app/tests/fixtures/Repos/StatementFormatter1.json similarity index 100% rename from app/tests/Fixtures/Repos/StatementFormatter1.json rename to app/tests/fixtures/Repos/StatementFormatter1.json diff --git a/app/tests/Fixtures/statement.json b/app/tests/fixtures/statement.json similarity index 100% rename from app/tests/Fixtures/statement.json rename to app/tests/fixtures/statement.json diff --git a/app/tests/readme.md b/app/tests/readme.md new file mode 100644 index 0000000000..b511630e12 --- /dev/null +++ b/app/tests/readme.md @@ -0,0 +1,25 @@ +# Tests +This file explains the purposes of some of the classes and directories within this directory. + +## Abstract Classes +### TestCase +This is a class to extend for all tests. + +### InstanceTestCase (extends TestCase) +This is a class to extend for testing a Learning Locker (LL) instance/installation. Any tests that depend on a LL instance being setup before testing should utilise this class. + +### LrsTestCase (extends InstanceTestCase) +This is a class to extend for testing an LRS inside a LL instance. Any tests that depend on an LRS being created before testing should utilise this class. + +### StatementsTestCase (extends LrsTestCase) +This is a class to extend for testing retrieval of statements inside an LRS. Any tests that depend on a statements being created before or during testing should utilise this class. + +## Directories +### fixtures +This directory should contain any files required by the tests. + +### repos +This directory should contain tests for repositories (data in, data out). + +### routes +This directory should contain tests for routes (request in, response out). Inside this directory there is a RouteTestTrait that should be used by all classes inside this directory for sending requests. diff --git a/app/tests/repos/Statement/EloquentIndexerTest.php b/app/tests/repos/Statement/EloquentIndexerTest.php new file mode 100644 index 0000000000..3ae0fb5ed5 --- /dev/null +++ b/app/tests/repos/Statement/EloquentIndexerTest.php @@ -0,0 +1,56 @@ +<?php namespace Tests\Repos\Statement; + +use \Locker\Repository\Statement\EloquentIndexer as Indexer; +use \Locker\Repository\Statement\IndexOptions as IndexOptions; + +class EloquentIndexerTest extends EloquentTest { + + public function setup() { + parent::setup(); + $this->indexer = new Indexer(); + } + + public function testIndex() { + $opts = new IndexOptions([ + 'lrs_id' => $this->lrs->_id + ]); + $result = $this->indexer->index($opts); + + $this->assertEquals(true, is_object($result)); + $this->assertEquals('Jenssegers\Mongodb\Eloquent\Builder', get_class($result)); + } + + public function testFormat() { + $opts = new IndexOptions([ + 'lrs_id' => $this->lrs->_id + ]); + $result = $this->indexer->index($opts); + $result = $this->indexer->format($result, $opts); + + $this->assertEquals(true, is_array($result)); + $this->assertEquals(count($this->statements), count($result)); + foreach ($result as $statement) { + $this->assertEquals(true, is_object($statement)); + $this->assertEquals(true, isset($statement->id)); + $this->assertEquals(true, is_string($statement->id)); + $expected_statement = $this->statements[0]->statement; + $this->assertStatementMatch($expected_statement, $statement); + } + } + + public function testCount() { + $opts = new IndexOptions([ + 'lrs_id' => $this->lrs->_id + ]); + $result = $this->indexer->index($opts); + $result = $this->indexer->count($result, $opts); + + $this->assertEquals(true, is_int($result)); + $this->assertEquals(count($this->statements), $result); + } + + protected function assertStatementMatch(\stdClass $statement_a, \stdClass $statement_b) { + unset($statement_b->version); + $this->assertEquals(true, $statement_a == $statement_b); + } +} diff --git a/app/tests/repos/Statement/EloquentLinkerTest.php b/app/tests/repos/Statement/EloquentLinkerTest.php new file mode 100644 index 0000000000..f1108f2146 --- /dev/null +++ b/app/tests/repos/Statement/EloquentLinkerTest.php @@ -0,0 +1,16 @@ +<?php namespace Tests\Repos\Statement; + +use \Locker\Repository\Statement\EloquentLinker as Linker; +use \Locker\Repository\Statement\StorerOptions as StorerOptions; + +class EloquentLinkerTest extends EloquentTest { + + public function setup() { + parent::setup(); + $this->linker = new Linker(); + } + + public function test() { + return null; + } +} diff --git a/app/tests/repos/Statement/EloquentShowerTest.php b/app/tests/repos/Statement/EloquentShowerTest.php new file mode 100644 index 0000000000..911acaff98 --- /dev/null +++ b/app/tests/repos/Statement/EloquentShowerTest.php @@ -0,0 +1,75 @@ +<?php namespace Tests\Repos\Statement; + +use \Locker\Repository\Statement\EloquentShower as Shower; +use \Locker\Repository\Statement\ShowOptions as ShowOptions; + +class EloquentShowerTest extends EloquentTest { + + public function setup() { + parent::setup(); + $this->shower = new Shower(); + } + + public function testShow() { + $opts = new ShowOptions([ + 'lrs_id' => $this->lrs->_id + ]); + $result = $this->shower->show('00000000-0000-0000-0000-000000000000', $opts); + + $this->assertEquals(true, is_object($result)); + $this->assertEquals('stdClass', get_class($result)); + $this->assertStatementMatch($this->statements[0]->statement, $result); + } + + public function testShowInactive() { + $opts = new ShowOptions([ + 'lrs_id' => $this->lrs->_id, + 'active' => false + ]); + $model = $this->createStatement(1); + $model->active = false; + $model->save(); + $result = $this->shower->show('10000000-0000-0000-0000-000000000000', $opts); + + $this->assertEquals(true, is_object($result)); + $this->assertEquals('stdClass', get_class($result)); + $this->assertStatementMatch($model->statement, $result); + } + + public function testShowVoided() { + $opts = new ShowOptions([ + 'lrs_id' => $this->lrs->_id, + 'voided' => true + ]); + $model = $this->createStatement(1); + $model->voided = true; + $model->save(); + $result = $this->shower->show('10000000-0000-0000-0000-000000000000', $opts); + + $this->assertEquals(true, is_object($result)); + $this->assertEquals('stdClass', get_class($result)); + $this->assertStatementMatch($model->statement, $result); + } + + public function testShowInactiveVoided() { + $opts = new ShowOptions([ + 'lrs_id' => $this->lrs->_id, + 'active' => false, + 'voided' => true + ]); + $model = $this->createStatement(1); + $model->active = false; + $model->voided = true; + $model->save(); + $result = $this->shower->show('10000000-0000-0000-0000-000000000000', $opts); + + $this->assertEquals(true, is_object($result)); + $this->assertEquals('stdClass', get_class($result)); + $this->assertStatementMatch($model->statement, $result); + } + + private function assertStatementMatch(\stdClass $statement_a, \stdClass $statement_b) { + unset($statement_b->version); + $this->assertEquals(true, $statement_a == $statement_b); + } +} diff --git a/app/tests/repos/Statement/EloquentTest.php b/app/tests/repos/Statement/EloquentTest.php new file mode 100644 index 0000000000..84229c3875 --- /dev/null +++ b/app/tests/repos/Statement/EloquentTest.php @@ -0,0 +1,59 @@ +<?php namespace Tests\Repos\Statement; + +use \Illuminate\Foundation\Testing\TestCase as Base; + +abstract class EloquentTest extends Base { + + public function setup() { + parent::setup(); + $this->lrs = $this->createLRS(); + $this->statements = [$this->createStatement(0)]; + } + + public function createApplication() { + $unitTesting = true; + $testEnvironment = 'testing'; + return require __DIR__ . '/../../../../bootstrap/start.php'; + } + + private function createLrs() { + $lrs = new \Lrs([ + 'title' => 'TestLRS', + 'api' => [], + 'owner' => [], + 'users' => [], + 'domain' => '', + ]); + + $lrs->save(); + return $lrs; + } + + protected function createStatement($id) { + $model = new \Statement($this->getStatement()); + $model->statement->id = ((string) $id).'0000000-0000-0000-0000-000000000000'; + $model->save(); + return $model; + } + + protected function getStatement() { + return [ + 'statement' => json_decode(file_get_contents(__DIR__ . '../../../fixtures/statement.json')), + 'active' => true, + 'voided' => false, + 'refs' => [], + 'timestamp' => new \MongoDate(strtotime('now')), + 'lrs' => [ + '_id' => $this->lrs->_id + ] + ]; + } + + public function tearDown() { + $this->lrs->delete(); + foreach ($this->statements as $statement) { + $statement->delete(); + } + parent::tearDown(); + } +} diff --git a/app/tests/Repos/StatementFormatterTest.php b/app/tests/repos/Statement/FormatterTest.php similarity index 90% rename from app/tests/Repos/StatementFormatterTest.php rename to app/tests/repos/Statement/FormatterTest.php index 977cfcd756..84524e258f 100644 --- a/app/tests/Repos/StatementFormatterTest.php +++ b/app/tests/repos/Statement/FormatterTest.php @@ -1,9 +1,9 @@ -<?php namespace Tests\Repos; +<?php namespace Tests\Repos\Statement; use \Illuminate\Foundation\Testing\TestCase as Base; use \Locker\Repository\Statement\Formatter as Formatter; -class StatementFormatterTest extends Base { +class FormatterTest extends Base { public function setup() { parent::setup(); @@ -13,7 +13,7 @@ public function setup() { public function createApplication() { $unitTesting = true; $testEnvironment = 'testing'; - return require __DIR__ . '/../../../bootstrap/start.php'; + return require __DIR__ . '/../../../../bootstrap/start.php'; } private function cloneObj(\stdClass $obj) { @@ -21,7 +21,7 @@ private function cloneObj(\stdClass $obj) { } public function testIdentityStatements() { - $statement = json_decode(file_get_contents(__DIR__ . '/../Fixtures/Repos/StatementFormatter1.json')); + $statement = json_decode(file_get_contents(__DIR__ . '/../../fixtures/Repos/StatementFormatter1.json')); $formatted = $this->formatter->identityStatement($this->cloneObj($statement)); $this->assertEquals(true, is_object($formatted)); @@ -54,7 +54,7 @@ public function testIdentityStatements() { } public function testCanonicalStatements() { - $statement = json_decode(file_get_contents(__DIR__ . '/../Fixtures/Repos/StatementFormatter1.json')); + $statement = json_decode(file_get_contents(__DIR__ . '/../../fixtures/Repos/StatementFormatter1.json')); $formatted = $this->formatter->canonicalStatement($this->cloneObj($statement), ['en-GB']); $this->assertEquals(true, is_object($formatted)); diff --git a/app/tests/repos/Statement/IndexOptionsTest.php b/app/tests/repos/Statement/IndexOptionsTest.php new file mode 100644 index 0000000000..1e67750412 --- /dev/null +++ b/app/tests/repos/Statement/IndexOptionsTest.php @@ -0,0 +1,100 @@ +<?php namespace Tests\Repos\Statement; + +class IndexOptionsTest extends OptionsTest { + protected $options_class = '\Locker\Repository\Statement\IndexOptions'; + protected $overwrite_opts = [ + 'lrs_id' => '1', + ]; + protected $default_opts = [ + 'agent' => null, + 'activity' => null, + 'verb' => null, + 'registration' => null, + 'since' => null, + 'until' => null, + 'active' => true, + 'voided' => false, + 'related_activities' => false, + 'related_agents' => false, + 'ascending' => false, + 'format' => 'exact', + 'offset' => 0, + 'limit' => 100, + 'langs' => [], + 'attachments' => false, + 'lrs_id' => '1' + ]; + protected $valid_opts = [ + 'activity' => 'http://www.example.com', + 'verb' => 'http://www.example.com', + 'registration' => '93439880-e35a-11e4-b571-0800200c9a66', + 'since' => '2015-01-01T00:00Z', + 'until' => '2015-01-01T00:00Z', + 'active' => true, + 'voided' => true, + 'related_activities' => true, + 'related_agents' => true, + 'ascending' => true, + 'format' => 'exact', + 'offset' => 0, + 'limit' => 1, + 'langs' => [], + 'attachments' => true, + 'lrs_id' => '1' + ]; + protected $invalid_opts = [ + 'activity' => 'zz.example.com', + 'verb' => 'zz.example.com', + 'registration' => '93439880-e35a-11e4-b571-0800200c9a66Z', + 'since' => '2015-01-01T00:00ZYX', + 'until' => '2015-01-01T00:00ZYX', + 'active' => 'invalid', + 'voided' => 'invalid', + 'related_activities' => 'invalid', + 'related_agents' => 'invalid', + 'ascending' => 'invalid', + 'format' => 'invalid', + 'offset' => -1, + 'limit' => -1, + 'langs' => 'invalid', + 'attachments' => 'invalid', + 'lrs_id' => true + ]; + + public function setup() { + parent::setup(); + $this->valid_opts['agent'] = (object) ['mbox' => 'mailto:test@example.com']; + $this->invalid_opts['agent'] = (object) ['mbox' => 'test@example.com']; + } + + public function testValidCanonicalFormat() { + try { + $start_opts = $this->valid_opts; + $start_opts['format'] = 'canonical'; + $end_opts = new $this->options_class($start_opts); + + foreach ($end_opts->options as $key => $val) { + $this->assertEquals($start_opts[$key], $val); + } + } catch (\Exception $ex) { + \Log::error($ex); + $this->assertEquals(false, true, $ex->getMessage()); + } + } + + public function testValidIdsFormat() { + try { + $start_opts = $this->valid_opts; + $start_opts['format'] = 'ids'; + $end_opts = new $this->options_class($start_opts); + + foreach ($end_opts->options as $key => $val) { + $this->assertEquals($start_opts[$key], $val); + } + } catch (\Exception $ex) { + \Log::error($ex); + $this->assertEquals(false, true, $ex->getMessage()); + } + } + +} diff --git a/app/tests/repos/Statement/OptionsTest.php b/app/tests/repos/Statement/OptionsTest.php new file mode 100644 index 0000000000..95dd75f6d9 --- /dev/null +++ b/app/tests/repos/Statement/OptionsTest.php @@ -0,0 +1,74 @@ +<?php namespace Tests\Repos\Statement; + +use \Illuminate\Foundation\Testing\TestCase as Base; + +abstract class OptionsTest extends Base { + protected $default_opts = []; + protected $overwrite_opts = []; + protected $valid_opts = []; + protected $invalid_opts = []; + protected $options_class = ''; + + public function createApplication() { + $unitTesting = true; + $testEnvironment = 'testing'; + return require __DIR__ . '/../../../../bootstrap/start.php'; + } + + public function testValidOptions() { + try { + $start_opts = $this->valid_opts; + $end_opts = new $this->options_class($start_opts); + + foreach ($end_opts->options as $key => $val) { + $this->assertEquals($start_opts[$key], $val); + } + } catch (\Exception $ex) { + \Log::error($ex); + $this->assertEquals(false, true, $ex->getMessage()); + } + } + + public function testDefaultOptions() { + try { + $start_opts = $this->default_opts; + $end_opts = new $this->options_class($this->overwrite_opts); + + foreach ($end_opts->options as $key => $val) { + $this->assertEquals($start_opts[$key], $val); + } + } catch (\Exception $ex) { + \Log::error($ex); + $this->assertEquals(false, true, $ex->getMessage()); + } + } + + public function testInvalidOptions() { + foreach ($this->invalid_opts as $invalid_key => $invalid_val) { + try { + $caught = false; + $start_opts = $this->valid_opts; + $start_opts[$invalid_key] = $invalid_val; + $end_opts = new $this->options_class($start_opts); + } catch (\Exception $ex) { + $caught = true; + } + $this->assertEquals(true, $caught); + } + } + + public function testGetOpts() { + try { + $start_opts = $this->valid_opts; + $end_opts = new $this->options_class($start_opts); + + foreach ($start_opts as $key => $val) { + $this->assertEquals($val, $end_opts->getOpt($key)); + } + } catch (\Exception $ex) { + \Log::error($ex); + $this->assertEquals(false, true, $ex->getMessage()); + } + } + +} diff --git a/app/tests/repos/Statement/ShowOptionsTest.php b/app/tests/repos/Statement/ShowOptionsTest.php new file mode 100644 index 0000000000..67cd536095 --- /dev/null +++ b/app/tests/repos/Statement/ShowOptionsTest.php @@ -0,0 +1,24 @@ +<?php namespace Tests\Repos\Statement; + +class ShowOptionsTest extends OptionsTest { + protected $options_class = '\Locker\Repository\Statement\ShowOptions'; + protected $overwrite_opts = [ + 'lrs_id' => '1', + ]; + protected $default_opts = [ + 'lrs_id' => '1', + 'voided' => false, + 'active' => true + ]; + protected $valid_opts = [ + 'lrs_id' => '1', + 'voided' => true, + 'active' => false + ]; + protected $invalid_opts = [ + 'lrs_id' => true, + 'voided' => 'false', + 'active' => 'true' + ]; + +} diff --git a/app/tests/repos/Statement/StoreOptionsTest.php b/app/tests/repos/Statement/StoreOptionsTest.php new file mode 100644 index 0000000000..623c8a0069 --- /dev/null +++ b/app/tests/repos/Statement/StoreOptionsTest.php @@ -0,0 +1,26 @@ +<?php namespace Tests\Repos\Statement; + +class StoreOptionsTest extends OptionsTest { + protected $options_class = '\Locker\Repository\Statement\StoreOptions'; + protected $overwrite_opts = [ + 'lrs_id' => '1', + ]; + protected $default_opts = [ + 'lrs_id' => '1', + ]; + protected $valid_opts = [ + 'lrs_id' => '1', + ]; + protected $invalid_opts = [ + 'lrs_id' => true + ]; + + public function setup() { + parent::setup(); + $this->overwrite_opts['authority'] = (object) ['mbox' => 'mailto:test@example.com']; + $this->default_opts['authority'] = $this->overwrite_opts['authority']; + $this->valid_opts['authority'] = $this->overwrite_opts['authority']; + $this->invalid_opts['authority'] = (object) ['mbox' => 'test@example.com']; + } + +} diff --git a/app/tests/API/ExportsTest.php b/app/tests/routes/ExportsTest.php similarity index 83% rename from app/tests/API/ExportsTest.php rename to app/tests/routes/ExportsTest.php index 2355ae025d..02a529380e 100644 --- a/app/tests/API/ExportsTest.php +++ b/app/tests/routes/ExportsTest.php @@ -1,5 +1,4 @@ -<?php namespace Tests\API; -use \Report as Report; +<?php namespace Tests\Routes; class ExportsTest extends ResourcesTestCase { static protected $endpoint = '/api/v1/exports'; @@ -22,7 +21,7 @@ protected function constructData($data) { } protected function createReport() { - $model = new Report([ + $model = new \Report([ 'name' => 'Test report', 'description' => 'Test report description', 'query' => [ @@ -35,7 +34,7 @@ protected function createReport() { } public function testJson() { - $response = $this->requestAPI('GET', static::$endpoint.'/'."{$this->model->_id}/show"); + $response = $this->requestResource('GET', static::$endpoint.'/'."{$this->model->_id}/show"); $content = $response->getContent(); // Checks that the response is correct. @@ -43,7 +42,7 @@ public function testJson() { } public function testCsv() { - $response = $this->requestAPI('GET', static::$endpoint.'/'."{$this->model->_id}/show/csv"); + $response = $this->requestResource('GET', static::$endpoint.'/'."{$this->model->_id}/show/csv"); $content = $response->getContent(); // Checks that the response is correct. diff --git a/app/tests/routes/QueryTest.php b/app/tests/routes/QueryTest.php new file mode 100644 index 0000000000..055aee6d89 --- /dev/null +++ b/app/tests/routes/QueryTest.php @@ -0,0 +1,119 @@ +<?php namespace Tests\Routes; + +class QueryTest extends \Tests\StatementsTestCase { + use RouteTestTrait; + + public function setup() { + parent::setup(); + } + + protected function requestAnalytics($params = []) { + $content = null; + return $this->request('GET', 'api/v1/query/analytics', $params, $this->getServer($this->lrs), $content); + } + + public function testAnalyticsDefault() { + $response = $this->requestAnalytics(); + $data = $response->getData(); + $this->assertEquals($data->version, 'v1'); + $this->assertEquals($data->route, 'api/v1/query/analytics'); + } + + public function testAnalyticsTime() { + $response = $this->requestAnalytics(['type' => 'time']); + $data = $response->getData()->data; + $this->assertEquals(count($this->statements), $data[0]->count); + } + + public function testUserQuery() { + $response = $this->requestAnalytics(['type' => 'user']); + $data = $response->getData(); + + $this->assertEquals(true, is_object($data)); + $this->assertEquals(true, isset($data->data)); + $this->assertEquals(true, is_array($data->data)); + $this->assertEquals(count($this->statements), count($data->data)); + + // Asserts correct properties on data[0]. + $this->assertEquals(true, isset($data->data[0]->count)); + $this->assertEquals(true, isset($data->data[0]->dates)); + $this->assertEquals(true, isset($data->data[0]->data)); + + // Asserts correct property types on data[0]. + $this->assertEquals(true, is_numeric($data->data[0]->count)); + $this->assertEquals(true, is_array($data->data[0]->dates)); + $this->assertEquals(true, is_object($data->data[0]->data)); + + // Asserts correct values on data[0]. + $this->assertEquals(count($this->statements), $data->data[0]->count); + $this->assertEquals(count($this->statements), count($data->data[0]->dates)); + + // Asserts correct properties on data[0]->data. + $this->assertEquals(true, isset($data->data[0]->data->mbox)); + $this->assertEquals(true, isset($data->data[0]->data->objectType)); + + // Asserts correct property types on data[0]->data. + $this->assertEquals(true, is_string($data->data[0]->data->mbox)); + $this->assertEquals(true, is_string($data->data[0]->data->objectType)); + + // Asserts correct property values on data[0]->data. + $this->assertEquals('mailto:test@example.com', $data->data[0]->data->mbox); + $this->assertEquals('Agent', $data->data[0]->data->objectType); + } + + public function testAnalyticsVerb() { + $response = $this->requestAnalytics(['type' => 'verb']); + $data = $response->getData()->data; + $this->assertEquals($data[0]->data->id, 'http://www.example.com/verbs/test'); + } + + public function testAnalyticsDay() { + $response = $this->requestAnalytics(['interval' => 'day']); + $data = $response->getData()->data; + $this->assertEquals(count($this->statements), count($data)); + } + + public function testAnalyticsMonth() { + $response = $this->requestAnalytics(['interval' => 'month']); + $data = $response->getData()->data; + $this->assertEquals(count($this->statements), count($data)); + } + + public function testAnalyticsYear() { + $response = $this->requestAnalytics(['interval' => 'year']); + $data = $response->getData()->data; + $this->assertEquals(count($this->statements), count($data)); + } + + public function testAnalyticsSince() { + $date = date('Y-m-d', strtotime("-1 day")); + $response = $this->requestAnalytics(['since' => $date]); + $data = $response->getData()->data; + $this->assertEquals(count($this->statements), $data[0]->count); + } + + public function testAnalyticsEmptySince() { + $date = date('Y-m-d', strtotime("+1 day")); + $response = $this->requestAnalytics(['since' => $date]); + $data = $response->getData()->data; + $this->assertEquals(true, empty($data)); + } + + public function testAnalyticsUntil() { + $date = date('Y-m-d', strtotime("+1 day")); + $response = $this->requestAnalytics(['until' => $date]); + $data = $response->getData()->data; + $this->assertEquals(count($this->statements), $data[0]->count); + } + + public function testAnalyticsEmptyUntil() { + $date = date('Y-m-d', strtotime("-1 day")); + $response = $this->requestAnalytics(['until' => $date]); + $data = $response->getData()->data; + $this->assertEquals(true, empty($data)); + } + + public function tearDown() { + parent::tearDown(); + } +} diff --git a/app/tests/API/ReportsTest.php b/app/tests/routes/ReportsTest.php similarity index 51% rename from app/tests/API/ReportsTest.php rename to app/tests/routes/ReportsTest.php index da9509a636..862560a43f 100644 --- a/app/tests/API/ReportsTest.php +++ b/app/tests/routes/ReportsTest.php @@ -1,4 +1,4 @@ -<?php namespace Tests\API; +<?php namespace Tests\Routes; class ReportsTest extends ResourcesTestCase { static protected $endpoint = '/api/v1/reports'; @@ -17,21 +17,21 @@ class ReportsTest extends ResourcesTestCase { ]; public function testRun() { - $response = $this->requestAPI('GET', static::$endpoint.'/'."{$this->model->_id}/run"); + $response = $this->requestResource('GET', static::$endpoint.'/'."{$this->model->_id}/run"); $content = $this->getContentFromResponse($response); // Checks that the response is correct. - $this->assertEquals(self::$statements, count($content), 'Incorrect amount of statements.'); - $this->assertEquals(200, $response->getStatusCode(), 'Incorrect status code.'); + $this->assertEquals(count($this->statements), count($content)); + $this->assertEquals(200, $response->getStatusCode()); } public function testGraph() { - $response = $this->requestAPI('GET', static::$endpoint.'/'."{$this->model->_id}/graph"); + $response = $this->requestResource('GET', static::$endpoint.'/'."{$this->model->_id}/graph"); $content = $this->getContentFromResponse($response); // Checks that the response is correct. - $this->assertEquals(true, isset($content[0]['count']), 'Incorrectly formed content.'); - $this->assertEquals(self::$statements, $content[0]['count'], 'Incorrect amount of statements.'); - $this->assertEquals(200, $response->getStatusCode(), 'Incorrect status code.'); + $this->assertEquals(true, isset($content[0]['count'])); + $this->assertEquals(count($this->statements), $content[0]['count']); + $this->assertEquals(200, $response->getStatusCode()); } } diff --git a/app/tests/API/ResourcesTest.php b/app/tests/routes/ResourcesTestCase.php similarity index 76% rename from app/tests/API/ResourcesTest.php rename to app/tests/routes/ResourcesTestCase.php index e74d627776..3d398d25e0 100644 --- a/app/tests/API/ResourcesTest.php +++ b/app/tests/routes/ResourcesTestCase.php @@ -1,7 +1,10 @@ -<?php namespace Tests\API; +<?php namespace Tests\Routes; +use \Tests\StatementsTestCase as StatementsTestCase; use \Illuminate\Http\JsonResponse as JsonResponse; -abstract class ResourcesTestCase extends TestCase { +abstract class ResourcesTestCase extends StatementsTestCase { + use RouteTestTrait; + static protected $model_class = '...'; protected $data = []; protected $model = null; @@ -23,8 +26,13 @@ protected function createModel($data) { return $model; } + protected function requestResource($method = 'GET', $uri = '', $content = '') { + $params = []; + return $this->request($method, $uri, $params, $this->getServer($this->lrs), $content); + } + public function testIndex() { - $response = $this->requestAPI('GET', static::$endpoint); + $response = $this->requestResource('GET', static::$endpoint); $content = $this->getContentFromResponse($response); $model = $this->getModelFromContent($content[0]); @@ -35,7 +43,7 @@ public function testIndex() { } public function testStore() { - $response = $this->requestAPI('POST', static::$endpoint, json_encode($this->data)); + $response = $this->requestResource('POST', static::$endpoint, json_encode($this->data)); $model = $this->getModelFromResponse($response); // Checks that the response is correct. @@ -45,7 +53,7 @@ public function testStore() { public function testUpdate() { $data = array_merge($this->data, $this->update); - $response = $this->requestAPI('PUT', static::$endpoint.'/'.$this->model->_id, json_encode($data)); + $response = $this->requestResource('PUT', static::$endpoint.'/'.$this->model->_id, json_encode($data)); $model = $this->getModelFromResponse($response); // Checks that the response is correct. @@ -54,7 +62,7 @@ public function testUpdate() { } public function testShow() { - $response = $this->requestAPI('GET', static::$endpoint.'/'.$this->model->_id); + $response = $this->requestResource('GET', static::$endpoint.'/'.$this->model->_id); $model = $this->getModelFromResponse($response); // Checks that the response is correct. @@ -63,7 +71,7 @@ public function testShow() { } public function testDestroy() { - $response = $this->requestAPI('DELETE', static::$endpoint.'/'.$this->model->_id); + $response = $this->requestResource('DELETE', static::$endpoint.'/'.$this->model->_id); $content = $this->getContentFromResponse($response); // Checks that the response is correct. @@ -87,7 +95,7 @@ protected function getContentFromResponse(JsonResponse $response) { } public function tearDown() { - //$this->model->delete(); + $this->model->delete(); parent::tearDown(); } } diff --git a/app/tests/routes/RouteTestTrait.php b/app/tests/routes/RouteTestTrait.php new file mode 100644 index 0000000000..0bd89961ec --- /dev/null +++ b/app/tests/routes/RouteTestTrait.php @@ -0,0 +1,21 @@ +<?php namespace Tests\Routes; + +trait RouteTestTrait { + + protected function getServer(\Lrs $lrs, $version = '1.0.1') { + return [ + 'PHP_AUTH_USER' => $lrs->api['basic_key'], + 'PHP_AUTH_PW' => $lrs->api['basic_secret'], + 'HTTP_X-Experience-API-Version' => $version + ]; + } + + protected function request($method = 'GET', $uri = '', $params = [], $server = [], $content = null) { + $files = []; + $changeHistory = true; + \Route::enableFilters(); + + // http://laravel.com/api/4.2/Illuminate/Foundation/Testing/TestCase.html#method_call + return $this->call($method, $uri, $params, $files, $server, $content, $changeHistory); + } +} diff --git a/app/tests/StatementRefTest.php b/app/tests/routes/StatementRefTest.php similarity index 74% rename from app/tests/StatementRefTest.php rename to app/tests/routes/StatementRefTest.php index c8b849dc57..19cfe381a6 100644 --- a/app/tests/StatementRefTest.php +++ b/app/tests/routes/StatementRefTest.php @@ -1,36 +1,20 @@ -<?php +<?php namespace Tests\Routes; +use \Tests\StatementsTestCase; -class StatementRefTest extends TestCase { +class StatementRefTest extends StatementsTestCase { + use RouteTestTrait; public function setUp() { parent::setUp(); - Route::enableFilters(); - - // Authentication as super user. - $user = User::firstOrCreate(['email' => 'test@example.com']); - Auth::login($user); - $this->createLrs(); - } - - private function sendStatements($statements) { - $auth = [ - 'api_key' => $this->lrs->api['basic_key'], - 'api_secret' => $this->lrs->api['basic_secret'], - ]; - $headers = $this->makeRequestHeaders($auth); - $statements = json_encode($statements); - return $this->call('POST', '/data/xAPI/statements', [], [], $headers, $statements); } - protected function generateStatement($statement) { - return array_merge($statement, [ - 'actor' => [ - 'mbox' => 'mailto:test@example.com' - ], - 'verb' => [ - 'id' => 'http://www.example.com/verbs/test', - ], - ]); + private function requestStatements($statements) { + $content = json_encode($statements); + $method = 'POST'; + $uri = '/data/xAPI/statements'; + $params = $files = []; + $server = $this->getServer($this->lrs); + return $this->request($method, $uri, $params, $server, $content); } private function createReferenceStatement($reference_id, $statement = []) { @@ -77,7 +61,12 @@ private function checkStatement($id, $expected_references = [], $expected_referr return $ref['statement']['id']; }, $referrers); - $this->assertEmpty(array_diff($expected_referrers, $referrers)); + $diff = array_diff($expected_referrers, $referrers); + $this->assertEquals(true, empty($diff) || count($diff) === 0, + json_encode($diff). + json_encode($expected_referrers). + json_encode($referrers) + ); } private function generateUUID($id) { @@ -87,7 +76,7 @@ private function generateUUID($id) { } public function testInsert1() { - $this->sendStatements([ + $this->requestStatements([ $this->createIdStatement('A', $this->createReferenceStatement('E')) ]); @@ -95,11 +84,11 @@ public function testInsert1() { } public function testInsert2() { - $this->sendStatements([ + $this->requestStatements([ $this->createIdStatement('A', $this->createReferenceStatement('E')) ]); - $this->sendStatements([ + $this->requestStatements([ $this->createIdStatement('C', $this->createReferenceStatement('A')), $this->createIdStatement('D', $this->createReferenceStatement('B')) ]); @@ -110,16 +99,16 @@ public function testInsert2() { } public function testInsert3() { - $this->sendStatements([ + $this->requestStatements([ $this->createIdStatement('A', $this->createReferenceStatement('E')) ]); - $this->sendStatements([ + $this->requestStatements([ $this->createIdStatement('C', $this->createReferenceStatement('A')), $this->createIdStatement('D', $this->createReferenceStatement('B')) ]); - $this->sendStatements([ + $this->requestStatements([ $this->createIdStatement('B', $this->createReferenceStatement('A')) ]); @@ -130,20 +119,20 @@ public function testInsert3() { } public function testInsert4() { - $this->sendStatements([ + $this->requestStatements([ $this->createIdStatement('A', $this->createReferenceStatement('E')) ]); - $this->sendStatements([ + $this->requestStatements([ $this->createIdStatement('C', $this->createReferenceStatement('A')), $this->createIdStatement('D', $this->createReferenceStatement('B')) ]); - $this->sendStatements([ + $this->requestStatements([ $this->createIdStatement('B', $this->createReferenceStatement('A')) ]); - $this->sendStatements([ + $this->requestStatements([ $this->createIdStatement('E', $this->createReferenceStatement('D')) ]); @@ -156,7 +145,6 @@ public function testInsert4() { public function tearDown() { parent::tearDown(); - if ($this->lrs) $this->lrs->delete(); } } diff --git a/app/tests/routes/StatementsTest.php b/app/tests/routes/StatementsTest.php new file mode 100644 index 0000000000..505b0161fd --- /dev/null +++ b/app/tests/routes/StatementsTest.php @@ -0,0 +1,156 @@ +<?php namespace Tests\Routes; +use \Tests\StatementsTestCase as StatementsTestCase; + +class StatementsTest extends StatementsTestCase { + use RouteTestTrait; + + public function setup() { + parent::setup(); + } + + protected function getPipeline() { + return '[{"$match":{"active":true}},{"$project":{"_id":0,"statement":1}}]'; + } + + protected function requestStatements($uri = '', $params = []) { + $method = 'GET'; + $content = null; + $uri = '/api/v1/statements/'.$uri; + $server = $this->getServer($this->lrs); + return $this->request($method, $uri, $params, $server, $content); + } + + public function testAggregate() { + $response = $this->requestStatements('aggregate', [ + 'pipeline' => $this->getPipeline() + ]); + + // Checks that the response is correct. + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(true, method_exists($response, 'getContent')); + + // Checks that the content is correct. + $content = json_decode($response->getContent()); + $this->assertEquals(true, is_object($content)); + $this->assertEquals(true, isset($content->result)); + $this->assertEquals(true, is_array($content->result)); + $this->assertEquals(count($this->statements), count($content->result)); + $this->assertEquals(true, is_object($content->result[0])); + $this->assertEquals(true, isset($content->result[0]->statement)); + $this->assertEquals(true, is_object($content->result[0]->statement)); + $this->assertEquals(true, isset($content->result[0]->statement->actor)); + $this->assertEquals(true, isset($content->ok)); + $this->assertEquals(true, is_numeric($content->ok)); + $this->assertEquals(1, $content->ok); + } + + public function testAggregateTime() { + $response = $this->requestStatements('aggregate/time', [ + 'match' => '{"active": true}' + ]); + + // Checks that the response is correct. + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(true, method_exists($response, 'getContent')); + + // Checks that the content is correct. + $content = json_decode($response->getContent()); + $this->assertEquals(true, is_object($content)); + $this->assertEquals(true, isset($content->result)); + $this->assertEquals(true, is_array($content->result)); + $this->assertEquals(1, count($content->result)); + $this->assertEquals(true, is_object($content->result[0])); + $this->assertEquals(true, isset($content->result[0]->count)); + $this->assertEquals(true, is_numeric($content->result[0]->count)); + $this->assertEquals(count($this->statements), is_numeric($content->result[0]->count)); + $this->assertEquals(true, isset($content->result[0]->date)); + $this->assertEquals(true, is_array($content->result[0]->date)); + $this->assertEquals(true, isset($content->ok)); + $this->assertEquals(true, is_numeric($content->ok)); + $this->assertEquals(1, $content->ok); + } + + public function testAggregateObject() { + $response = $this->requestStatements('aggregate/object', [ + 'match' => '{"active": true}' + ]); + + // Checks that the response is correct. + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(true, method_exists($response, 'getContent')); + + // Checks that the content is correct. + $content = json_decode($response->getContent()); + $this->assertEquals(true, is_object($content)); + $this->assertEquals(true, isset($content->result)); + $this->assertEquals(true, is_array($content->result)); + $this->assertEquals(1, count($content->result)); + $this->assertEquals(true, is_object($content->result[0])); + $this->assertEquals(true, isset($content->result[0]->count)); + $this->assertEquals(true, is_numeric($content->result[0]->count)); + $this->assertEquals(count($this->statements), is_numeric($content->result[0]->count)); + $this->assertEquals(true, isset($content->result[0]->data)); + $this->assertEquals(true, is_array($content->result[0]->data)); + $this->assertEquals(count($this->statements), count($content->result[0]->data)); + $this->assertEquals(true, isset($content->result[0]->data[0])); + $this->assertEquals(true, is_object($content->result[0]->data[0])); + $this->assertEquals(true, isset($content->result[0]->data[0]->actor)); + $this->assertEquals(true, is_object($content->result[0]->data[0]->actor)); + $this->assertEquals(true, isset($content->ok)); + $this->assertEquals(true, is_numeric($content->ok)); + $this->assertEquals(1, $content->ok); + } + + public function testWhere() { + $response = $this->requestStatements('where', [ + 'filter' => '[[{"active", true}]]', + 'limit' => 1, + 'page' => 1 + ]); + + // Checks that the response is correct. + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(true, method_exists($response, 'getContent')); + + // Checks that the content is correct. + $content = json_decode($response->getContent()); + $this->assertEquals(true, is_object($content)); + + // Checks set props. + $this->assertEquals(true, isset($content->total)); + $this->assertEquals(true, isset($content->per_page)); + $this->assertEquals(true, isset($content->current_page)); + $this->assertEquals(true, isset($content->last_page)); + $this->assertEquals(true, isset($content->from)); + $this->assertEquals(true, isset($content->to)); + $this->assertEquals(true, isset($content->data)); + + // Checks prop types. + $this->assertEquals(true, is_numeric($content->total)); + $this->assertEquals(true, is_numeric($content->per_page)); + $this->assertEquals(true, is_numeric($content->current_page)); + $this->assertEquals(true, is_numeric($content->last_page)); + $this->assertEquals(true, is_numeric($content->from)); + $this->assertEquals(true, is_numeric($content->to)); + $this->assertEquals(true, is_array($content->data)); + + // Checks prop content. + $this->assertEquals(count($this->statements), $content->total); + $this->assertEquals(1, $content->per_page); + $this->assertEquals(1, $content->current_page); + $this->assertEquals(count($this->statements), $content->last_page); + $this->assertEquals(1, $content->from); + $this->assertEquals(1, $content->to); + $this->assertEquals(1, count($content->data)); + $this->assertEquals(true, isset($content->data[0])); + $this->assertEquals(true, is_object($content->data[0])); + $this->assertEquals(true, isset($content->data[0]->statement)); + $this->assertEquals(true, is_object($content->data[0]->statement)); + $this->assertEquals(true, isset($content->data[0]->statement->actor)); + $this->assertEquals(true, is_object($content->data[0]->statement->actor)); + } + + public function tearDown() { + parent::tearDown(); + } +} diff --git a/app/tests/xAPI/Statement/Validation/BaseStatementValidationTest.php b/app/tests/xAPI/Statement/Validation/BaseStatementValidationTest.php deleted file mode 100644 index 812e4f979d..0000000000 --- a/app/tests/xAPI/Statement/Validation/BaseStatementValidationTest.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php - -/** - * References - * - * - https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md - * - http://zackpierce.github.io/xAPI-Validator-JS/ - */ - -use app\locker\statements\xAPIValidation as xAPIValidation; - -abstract class BaseStatementValidationTest extends PHPUnit_Framework_TestCase -{ - protected $json_input; - - protected function getFixturePath() - { - return __DIR__ . '/../../../Fixtures/Statements'; - } - - protected function exec($path) - { - $json = file_get_contents($path); - $this->json_input = json_decode($json, true); - $auth = isset($this->json_input['authority']) ? $this->json_input['authority'] : [ - 'name' => "John Smith", - 'mbox' => "mailto:test@learninglocker.co.uk", - 'objectType' => "Agent" - ]; - $manager = new xAPIValidation(); - return $manager->runValidation($this->json_input, $auth); - } - -} diff --git a/app/tests/xAPI/Statement/Validation/StatementValidationActorTest.php b/app/tests/xAPI/Statement/Validation/StatementValidationActorTest.php deleted file mode 100644 index 6e8049f58d..0000000000 --- a/app/tests/xAPI/Statement/Validation/StatementValidationActorTest.php +++ /dev/null @@ -1,48 +0,0 @@ -<?php - -require_once __DIR__ . '/BaseStatementValidationTest.php'; - -class StatementValidationActorTest extends BaseStatementValidationTest -{ - - public function testMissingActor() - { - $results = $this->exec($this->getFixturePath() . '/Invalid/Actor/missing-actor.json'); - $this->assertEquals('failed', $results['status']); - $this->assertEquals( - \Lang::get('xAPIValidation.errors.required', array( - 'key' => 'actor', - 'section' => 'core statement' - )), trim($results['errors'][0]) - ); - } - - public function testGroupMissingMember() - { - $results = $this->exec($this->getFixturePath() . '/Invalid/Actor/Group/missing-member.json'); - $this->assertEquals('failed', $results['status']); - $this->assertEquals(\Lang::get('xAPIValidation.errors.required', array( - 'key' => 'member', - 'section' => 'actor' - )), trim($results['errors'][0])); - } - - public function testGroupMemberObjectTypeIsNotAgent() - { - $results = $this->exec($this->getFixturePath() . '/Invalid/Actor/Group/Member/object-type-is-not-agent.json'); - $this->assertEquals('failed', $results['status']); - $this->assertEquals(\Lang::get('xAPIValidation.errors.group.groups'), trim($results['errors'][0])); - } - - public function testMbox() - { - $results = $this->exec($this->getFixturePath() . '/Invalid/Actor/Mbox/invalid-format.json'); - $this->assertEquals('failed', $results['status']); - $this->assertEquals( - \Lang::get('xAPIValidation.errors.format', array( - 'key' => 'mbox', - 'section' => 'actor' - )), trim($results['errors'][0])); - } - -} diff --git a/app/tests/xAPI/Statement/Validation/StatementValidationAttachmentTest.php b/app/tests/xAPI/Statement/Validation/StatementValidationAttachmentTest.php deleted file mode 100644 index e29664ec5e..0000000000 --- a/app/tests/xAPI/Statement/Validation/StatementValidationAttachmentTest.php +++ /dev/null @@ -1,80 +0,0 @@ -<?php - -require_once __DIR__ . '/BaseStatementValidationTest.php'; - -class StatementValidationAttachmentTest extends BaseStatementValidationTest -{ - - public function testContentType() - { - $results = $this->exec($this->getFixturePath() . '/Invalid/Attachment/content-type-is-not-string.json'); - $this->assertEquals('failed', $results['status']); - $this->assertEquals( - \Lang::get('xAPIValidation.errors.type', array( - 'key' => 'contentType', - 'section' => 'attachment', - 'type' => 'Internet Media Type' - )), trim($results['errors'][0]) - ); - } - - public function testLength() - { - $results = $this->exec($this->getFixturePath() . '/Invalid/Attachment/length-is-not-integer.json'); - $this->assertEquals('failed', $results['status']); - $this->assertEquals( - \Lang::get('xAPIValidation.errors.type', array( - 'key' => 'length', - 'section' => 'attachment', - 'type' => 'number' - )), trim($results['errors'][0]) - ); - } - - public function testSha2Required() - { - $results = $this->exec($this->getFixturePath() . '/Invalid/Attachment/missing-sha2.json'); - $this->assertEquals('failed', $results['status']); - $this->assertEquals( - \Lang::get('xAPIValidation.errors.required', array( - 'key' => 'sha2', - 'section' => 'attachment' - )), trim($results['errors'][0]) - ); - } - - public function testDisplayValid() - { - $results = $this->exec($this->getFixturePath() . '/Invalid/Attachment/display.json'); - $this->assertEquals('failed', $results['status']); - $this->assertEquals(\Lang::get('xAPIValidation.errors.langMap', array( - 'key' => 'display', - 'section' => 'attachment' - )), trim($results['errors'][0]) - ); - } - - public function testSha2Valid() - { - $results = $this->exec($this->getFixturePath() . '/Invalid/Attachment/sha2-is-not-valid.json'); - $this->assertEquals('failed', $results['status']); - $this->assertEquals(\Lang::get('xAPIValidation.errors.base64', array( - 'key' => 'sha2', - 'section' => 'attachment' - )), trim($results['errors'][0]) - ); - } - - public function testUsageType() - { - $results = $this->exec($this->getFixturePath() . '/Invalid/Attachment/missing-usage-type.json'); - $this->assertEquals('failed', $results['status']); - $this->assertEquals( - \Lang::get('xAPIValidation.errors.required', array( - 'key' => 'usageType', - 'section' => 'attachment' - )), trim($results['errors'][0]) - ); - } - -} diff --git a/app/tests/xAPI/Statement/Validation/StatementValidationAuthorityTest.php b/app/tests/xAPI/Statement/Validation/StatementValidationAuthorityTest.php deleted file mode 100644 index 0a6a53364c..0000000000 --- a/app/tests/xAPI/Statement/Validation/StatementValidationAuthorityTest.php +++ /dev/null @@ -1,15 +0,0 @@ -<?php - -require_once __DIR__ . '/BaseStatementValidationTest.php'; - -class StatementValidationAuthorityTest extends BaseStatementValidationTest -{ - - public function testAuthority() - { - $results = $this->exec($this->getFixturePath() . '/Invalid/Authority/Member/wrong-object-type.json'); - $this->assertEquals('failed', $results['status']); - $this->assertEquals(\Lang::get('xAPIValidation.errors.group.groups'), trim($results['errors'][0])); - } - -} diff --git a/app/tests/xAPI/Statement/Validation/StatementValidationObjectTest.php b/app/tests/xAPI/Statement/Validation/StatementValidationObjectTest.php deleted file mode 100644 index 25aa9e01ae..0000000000 --- a/app/tests/xAPI/Statement/Validation/StatementValidationObjectTest.php +++ /dev/null @@ -1,6 +0,0 @@ -<?php - -/** - * @todo An LRS MUST NOT take action in the event it perceives an activity id is - * being used by multiple authors and/or organizations. - */ diff --git a/app/tests/xAPI/Statement/Validation/StatementValidationTest.php b/app/tests/xAPI/Statement/Validation/StatementValidationTest.php deleted file mode 100644 index b83160532c..0000000000 --- a/app/tests/xAPI/Statement/Validation/StatementValidationTest.php +++ /dev/null @@ -1,43 +0,0 @@ -<?php - -require_once __DIR__ . '/BaseStatementValidationTest.php'; - -class StatementValidationTest extends BaseStatementValidationTest -{ - - /** - * @dataProvider dataProviderSimple - */ - public function testSimple($path) - { - $results = $this->exec($path); - $this->assertEquals('passed', $results['status']); - $this->assertEmpty($results['errors']); - - $short_path = substr($path, strpos($path, '/Fixtures/Statements/Valid/') + 27, -5); - $extra_method = str_replace(['//', '/', '-'], ' ', trim($short_path, '/')); - $extra_method = 'extraChecking' . str_replace(' ', '', ucwords($extra_method)); - if (method_exists($this, $extra_method)) { - $this->{$extra_method}($results); - } - } - - protected function extraCheckingActorGroup($results) - { - $this->assertEquals($this->json_input['actor']['member'], $results['statement']['actor']['member']); - } - - public function dataProviderSimple() - { - $data = []; - - foreach (['', 'Actor', 'Object', 'Verb/Display'] as $k) { - foreach (glob($this->getFixturePath() . "/Valid/{$k}/*.json") as $file) { - $data[][] = $file; - } - } - - return $data; - } - -}