Added proof of concept for pagination and search

This commit is contained in:
Marcin Kurczewski 2014-09-03 19:07:53 +02:00
parent 48016bea13
commit ee9fde5402
28 changed files with 643 additions and 23 deletions

View file

@ -10,3 +10,4 @@ minPasswordLength = 5
[users] [users]
minUserNameLength = 1 minUserNameLength = 1
maxUserNameLength = 32 maxUserNameLength = 32
usersPerPage = 20

View file

@ -23,3 +23,7 @@ a {
color: #6a2; color: #6a2;
text-decoration: none; text-decoration: none;
} }
a:hover {
color: #7b3;
}

27
public_html/css/pager.css Normal file
View file

@ -0,0 +1,27 @@
.pager {
text-align: center;
margin-top: 1em;
}
.pager ul {
list-style-type: none;
padding: 0;
margin: 0 auto;
display: inline-block;
}
.pager li {
display: inline-block;
}
.pager li a {
display: inline-block;
padding: 0.4em 1.2em;
}
.pager li:hover a {
background: #efa;
}
.pager li.active a {
background: #faffca;
}

View file

@ -0,0 +1,27 @@
#user-list {
min-width: 20em;
}
#user-list ul {
list-style-type: none;
padding: 0;
margin: 0;
}
#user-list ul.order {
margin: -0.5em -0.5em 0.5em -0.5em;
}
#user-list ul.order li {
display: inline-block;
margin: 0 0.5em;
}
#user-list ul.order li.active a {
background: #faffca;
}
#user-list ul.order a {
cursor: pointer;
display: inline-block;
padding: 0.2em 0.5em;
}

View file

@ -12,8 +12,10 @@
<link rel="stylesheet" type="text/css" href="/css/forms.css"/> <link rel="stylesheet" type="text/css" href="/css/forms.css"/>
<link rel="stylesheet" type="text/css" href="/css/messages.css"/> <link rel="stylesheet" type="text/css" href="/css/messages.css"/>
<link rel="stylesheet" type="text/css" href="/css/top-navigation.css"/> <link rel="stylesheet" type="text/css" href="/css/top-navigation.css"/>
<link rel="stylesheet" type="text/css" href="/css/pager.css"/>
<link rel="stylesheet" type="text/css" href="/css/login-form.css"/> <link rel="stylesheet" type="text/css" href="/css/login-form.css"/>
<link rel="stylesheet" type="text/css" href="/css/registration-form.css"/> <link rel="stylesheet" type="text/css" href="/css/registration-form.css"/>
<link rel="stylesheet" type="text/css" href="/css/user-list.css"/>
</head> </head>
<body> <body>
@ -31,6 +33,7 @@
<script type="text/javascript" src="/js/Auth.js"></script> <script type="text/javascript" src="/js/Auth.js"></script>
<script type="text/javascript" src="/js/Util.js"></script> <script type="text/javascript" src="/js/Util.js"></script>
<script type="text/javascript" src="/js/Presenters/TopNavigationPresenter.js"></script> <script type="text/javascript" src="/js/Presenters/TopNavigationPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/PagedCollectionPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/LoginPresenter.js"></script> <script type="text/javascript" src="/js/Presenters/LoginPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/LogoutPresenter.js"></script> <script type="text/javascript" src="/js/Presenters/LogoutPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/MessagePresenter.js"></script> <script type="text/javascript" src="/js/Presenters/MessagePresenter.js"></script>

View file

@ -0,0 +1,114 @@
var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.PagedCollectionPresenter = function(api, util) {
var searchOrder;
var searchQuery;
var pageNumber;
var baseUri;
var backendUri;
var renderCallback;
var template;
var pageSize;
var totalRecords;
function init(args) {
parseSearchArgs(args.searchArgs);
baseUri = args.baseUri;
backendUri = args.backendUri;
renderCallback = args.renderCallback;
util.loadTemplate('pager').then(function(html) {
template = _.template(html);
//renderCallback({entities: [], totalRecords: 0});
changePage(pageNumber);
});
}
function changePage(newPageNumber) {
pageNumber = newPageNumber;
api.get(backendUri, {
order: searchOrder,
query: searchQuery,
page: pageNumber
}).then(function(response) {
totalRecords = response.json.totalRecords;
pageSize = response.json.pageSize;
renderCallback({
entities: response.json.data,
totalRecords: response.json.totalRecords});
}).catch(function(response) {
console.log(Error(response.json && response.json.error || response));
});
}
function render($target) {
var totalPages = Math.ceil(totalRecords / pageSize);
var pages = [1, totalPages];
var pagesAroundCurrent = 2;
for (var i = -pagesAroundCurrent; i <= pagesAroundCurrent; i ++)
if (pageNumber + i >= 1 && pageNumber + i <= totalPages)
pages.push(pageNumber + i);
if (pageNumber - pagesAroundCurrent - 1 == 2)
pages.push(2);
if (pageNumber + pagesAroundCurrent + 1 == totalPages - 1)
pages.push(totalPages - 1);
pages = pages.sort(function(a, b) { return a - b; }).filter(function(item, pos) {
return !pos || item != pages[pos - 1];
});
$target.html(template({
pages: pages,
pageNumber: pageNumber,
link: getPageChangeLink,
}));
}
function getSearchQueryChangeLink(newSearchQuery) {
return util.compileComplexRouteArgs(baseUri, {
page: 1,
order: searchOrder,
query: newSearchQuery,
});
}
function getSearchOrderChangeLink(newSearchOrder) {
return util.compileComplexRouteArgs(baseUri, {
page: 1,
order: newSearchOrder,
query: searchQuery,
});
}
function getPageChangeLink(newPageNumber) {
return util.compileComplexRouteArgs(baseUri, {
page: newPageNumber,
order: searchOrder,
query: searchQuery,
});
}
function parseSearchArgs(searchArgs) {
var args = util.parseComplexRouteArgs(searchArgs);
pageNumber = parseInt(args.page) || 1;
searchOrder = args.order;
searchQuery = args.query;
}
return {
init: init,
render: render,
changePage: changePage,
getSearchQueryChangeLink: getSearchQueryChangeLink,
getSearchOrderChangeLink: getSearchOrderChangeLink,
getPageChangeLink: getPageChangeLink
};
};
App.DI.register('pagedCollectionPresenter', App.Presenters.PagedCollectionPresenter);

View file

@ -1,19 +1,56 @@
var App = App || {}; var App = App || {};
App.Presenters = App.Presenters || {}; App.Presenters = App.Presenters || {};
App.Presenters.UserListPresenter = function(jQuery, topNavigationPresenter, appState) { App.Presenters.UserListPresenter = function(
jQuery,
util,
router,
pagedCollectionPresenter,
topNavigationPresenter) {
var $el = jQuery('#content'); var $el = jQuery('#content');
var template;
var userList = [];
var activeSearchOrder;
function init() { function init(args) {
topNavigationPresenter.select('users'); topNavigationPresenter.select('users');
activeSearchOrder = util.parseComplexRouteArgs(args.searchArgs).order;
util.loadTemplate('user-list').then(function(html) {
template = _.template(html);
pagedCollectionPresenter.init({
searchArgs: args.searchArgs,
baseUri: '#/users',
backendUri: '/users',
renderCallback: function updateCollection(data) {
userList = data.entities;
render(); render();
}});
});
} }
function render() { function render() {
$el.html('Logged in: ' + appState.get('loggedIn')); $el.html(template({
userList: userList,
}));
$el.find('.order a').click(orderLinkClicked);
$el.find('.order [data-order="' + activeSearchOrder + '"]').parent('li').addClass('active');
console.log(activeSearchOrder);
var $pager = $el.find('.pager');
pagedCollectionPresenter.render($pager);
}; };
function orderLinkClicked(e)
{
e.preventDefault();
var $orderLink = jQuery(this);
activeSearchOrder = $orderLink.attr('data-order');
router.navigate(pagedCollectionPresenter.getSearchOrderChangeLink(activeSearchOrder));
}
return { return {
init: init, init: init,
render: render render: render

View file

@ -23,7 +23,8 @@ App.Router = function(jQuery, util) {
inject('#/logout', function() { return App.DI.get('logoutPresenter'); }); inject('#/logout', function() { return App.DI.get('logoutPresenter'); });
inject('#/register', function() { return App.DI.get('registrationPresenter'); }); inject('#/register', function() { return App.DI.get('registrationPresenter'); });
inject('#/users', function() { return App.DI.get('userListPresenter'); }); inject('#/users', function() { return App.DI.get('userListPresenter'); });
inject('#/users/:userName', function() { return App.DI.get('userPresenter'); }); inject('#/users/:searchArgs', function() { return App.DI.get('userListPresenter'); });
inject('#/user/:userName', function() { return App.DI.get('userPresenter'); });
setRoot('#/users'); setRoot('#/users');
}; };

View file

@ -4,6 +4,30 @@ App.Util = (function(jQuery) {
var templateCache = {}; var templateCache = {};
function parseComplexRouteArgs(args) {
var result = {};
args = (args || '').split(/;/);
for (var i = 0; i < args.length; i ++) {
var arg = args[i];
if (!arg)
continue;
kv = arg.split(/=/);
result[kv[0]] = kv[1];
}
return result;
}
function compileComplexRouteArgs(baseUri, args) {
var result = baseUri + '/';
_.each(args, function(v, k) {
if (typeof(v) == 'undefined')
return;
result += k + '=' + v + ';'
});
result = result.slice(0, -1);
return result;
}
function loadTemplate(templateName) { function loadTemplate(templateName) {
return loadTemplateFromCache(templateName) return loadTemplateFromCache(templateName)
|| loadTemplateFromDOM(templateName) || loadTemplateFromDOM(templateName)
@ -62,6 +86,8 @@ App.Util = (function(jQuery) {
loadTemplate: loadTemplate, loadTemplate: loadTemplate,
initPresenter : initPresenter, initPresenter : initPresenter,
initContentPresenter: initContentPresenter, initContentPresenter: initContentPresenter,
parseComplexRouteArgs: parseComplexRouteArgs,
compileComplexRouteArgs: compileComplexRouteArgs,
}; };
}); });

View file

@ -0,0 +1,15 @@
<ul class="pager">
<% _.each(pages, function(page) { %>
<% if (page == pageNumber) { %>
<li class="active">
<% } else { %>
<li>
<% } %>
<a href="<%= link(page) %>">
<%= page %>
</a>
</li>
<% }); %>
</ul>

View file

@ -12,7 +12,7 @@
</li> </li>
<% } else { %> <% } else { %>
<li class="my-account"> <li class="my-account">
<a href="#/users/<%= user.name %>"><%= user.name %></a> <a href="#/user/<%= user.name %>"><%= user.name %></a>
</li> </li>
<li class="logout"> <li class="logout">
<a href="#/logout">Logout</a> <a href="#/logout">Logout</a>

View file

@ -0,0 +1,24 @@
<div id="user-list">
<ul class="order">
<li>
<a data-order="name">Sort A&rarr;Z</a>
</li>
<li>
<a data-order="name,desc">Sort Z&rarr;A</a>
</li>
<li>
<a data-order="registrationTime">Sort old&rarr;new</a>
</li>
<li>
<a data-order="registrationTime,desc">Sort new&rarr;old</a>
</li>
</ul>
<% _.each(userList, function(user) { %>
<div class="user">
User name: <%= user.name %>
</div>
<% }); %>
<div class="pager"></div>
</div>

View file

@ -1,7 +1,7 @@
<?php <?php
namespace Szurubooru; namespace Szurubooru;
final class Config extends \ArrayObject class Config extends \ArrayObject
{ {
public function __construct(array $configPaths = []) public function __construct(array $configPaths = [])
{ {

View file

@ -17,15 +17,36 @@ final class UserController extends AbstractController
public function registerRoutes(\Szurubooru\Router $router) public function registerRoutes(\Szurubooru\Router $router)
{ {
$router->post('/api/users', [$this, 'register']); $router->post('/api/users', [$this, 'register']);
$router->get('/api/users', [$this, 'getAll']); $router->get('/api/users', [$this, 'getFiltered']);
$router->get('/api/users/:id', [$this, 'getById']); $router->get('/api/users/:id', [$this, 'getById']);
$router->put('/api/users/:id', [$this, 'update']); $router->put('/api/users/:id', [$this, 'update']);
$router->delete('/api/users/:id', [$this, 'delete']); $router->delete('/api/users/:id', [$this, 'delete']);
} }
public function getFiltered()
{
//todo: move this to form data constructor
$searchFormData = new \Szurubooru\FormData\SearchFormData;
$searchFormData->query = $this->inputReader->query;
$searchFormData->order = $this->inputReader->order;
$searchFormData->pageNumber = $this->inputReader->page;
$searchResult = $this->userService->getFiltered($searchFormData);
$entities = array_map(function($user) { return new \Szurubooru\ViewProxies\User($user); }, $searchResult->entities);
return [
'data' => $entities,
'pageSize' => $searchResult->filter->pageSize,
'totalRecords' => $searchResult->totalRecords];
}
public function getById($id)
{
throw new \BadMethodCallException('Not implemented');
}
public function register() public function register()
{ {
$input = new \Szurubooru\FormData\RegistrationFormData; $input = new \Szurubooru\FormData\RegistrationFormData;
//todo: move this to form data constructor
$input->name = $this->inputReader->userName; $input->name = $this->inputReader->userName;
$input->password = $this->inputReader->password; $input->password = $this->inputReader->password;
$input->email = $this->inputReader->email; $input->email = $this->inputReader->email;
@ -40,16 +61,6 @@ final class UserController extends AbstractController
throw new \BadMethodCallException('Not implemented'); throw new \BadMethodCallException('Not implemented');
} }
public function getAll()
{
throw new \BadMethodCallException('Not implemented');
}
public function getById($id)
{
throw new \BadMethodCallException('Not implemented');
}
public function delete($id) public function delete($id)
{ {
throw new \BadMethodCallException('Not implemented'); throw new \BadMethodCallException('Not implemented');

View file

@ -19,6 +19,16 @@ abstract class AbstractDao implements ICrudDao
$this->entityName = $entityName; $this->entityName = $entityName;
} }
public function getCollection()
{
return $this->collection;
}
public function getEntityConverter()
{
return $this->entityConverter;
}
public function save(&$entity) public function save(&$entity)
{ {
$arrayEntity = $this->entityConverter->toArray($entity); $arrayEntity = $this->entityConverter->toArray($entity);

21
src/Dao/SearchFilter.php Normal file
View file

@ -0,0 +1,21 @@
<?php
namespace Szurubooru\Dao;
class SearchFilter
{
public $order;
public $query;
public $pageNumber;
public $pageSize;
public function __construct($pageSize, \Szurubooru\FormData\SearchFormData $searchFormData = null)
{
$this->pageSize = $pageSize;
if ($searchFormData)
{
$this->query = $searchFormData->query;
$this->order = $searchFormData->order;
$this->pageNumber = $searchFormData->pageNumber;
}
}
}

17
src/Dao/SearchResult.php Normal file
View file

@ -0,0 +1,17 @@
<?php
namespace Szurubooru\Dao;
class SearchResult
{
public $filter;
public $entities;
public $totalRecords;
public function __construct(SearchFilter $filter, $entities, $totalRecords)
{
$this->filter = $filter;
$this->entities = $entities;
$this->totalRecords = $totalRecords;
}
}

View file

@ -0,0 +1,122 @@
<?php
namespace Szurubooru\Dao\Services;
abstract class AbstractSearchService
{
const ORDER_DESC = -1;
const ORDER_ASC = 1;
private $collection;
private $entityConverter;
public function __construct(
\Szurubooru\Dao\AbstractDao $dao)
{
$this->collection = $dao->getCollection();
$this->entityConverter = $dao->getEntityConverter();
}
public function getFiltered(
\Szurubooru\Dao\SearchFilter $searchFilter)
{
list ($basicTokens, $complexTokens) = $this->tokenize($searchFilter->query);
$filter = [];
if ($basicTokens)
$this->decorateFilterWithBasicTokens($filter, $basicTokens);
if ($complexTokens)
$this->decorateFilterWithComplexTokens($filter, $complexTokens);
$order = $this->getOrder($searchFilter->order);
$pageSize = min(100, max(1, $searchFilter->pageSize));
$pageNumber = max(1, $searchFilter->pageNumber) - 1;
$cursor = $this->collection->find($filter);
$totalRecords = $cursor->count();
$cursor->sort($order);
$cursor->skip($pageSize * $pageNumber);
$cursor->limit($pageSize);
$entities = [];
foreach ($cursor as $arrayEntity)
$entities []= $this->entityConverter->toEntity($arrayEntity);
return new \Szurubooru\Dao\SearchResult($searchFilter, $entities, $totalRecords);
}
protected function decorateFilterWithBasicTokens(&$filter, $basicTokens)
{
throw new \BadMethodCallException('Not supported');
}
protected function decorateFilterWithComplexTokens(&$filter, $complexTokens)
{
throw new \BadMethodCallException('Not supported');
}
protected function getOrderColumn($token)
{
throw new \BadMethodCallException('Not supported');
}
protected function getDefaultOrderColumn()
{
return '_id';
}
protected function getDefaultOrderDir()
{
return self::ORDER_DESC;
}
private function getOrder($query)
{
$order = [];
$tokens = array_filter(preg_split('/\s+/', $query));
foreach ($tokens as $token)
{
$token = preg_split('/,|\s+/', $token);
if (count($token) == 2)
{
$orderDir = $token[1] == 'desc' ? self::ORDER_DESC : self::ORDER_ASC;
$orderToken = $token[0];
}
else
{
$orderDir = self::ORDER_ASC;
$orderToken = $token;
}
$orderColumn = $this->getOrderColumn($token[0]);
if ($orderColumn === null)
throw new \InvalidArgumentException('Invalid search order token: ' . $token);
$order[$orderColumn] = $orderDir;
}
$defaultOrderColumn = $this->getDefaultOrderColumn();
$defaultOrderDir = $this->getDefaultOrderDir();
if ($defaultOrderColumn)
$order[$defaultOrderColumn] = $defaultOrderDir;
return $order;
}
private function tokenize($query)
{
$basicTokens = [];
$complexTokens = [];
$tokens = array_filter(preg_split('/\s+/', $query));
foreach ($tokens as $token)
{
if (strpos($token, ':') !== false)
{
list ($key, $value) = explode(':', $token, 1);
$complexTokens[$key] = $value;
}
else
{
$basicTokens []= $token;
}
}
return [$basicTokens, $complexTokens];
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Szurubooru\Dao\Services;
class UserSearchService extends AbstractSearchService
{
public function __construct(\Szurubooru\Dao\UserDao $userDao)
{
parent::__construct($userDao);
}
protected function getOrderColumn($token)
{
switch ($token)
{
case 'name':
return 'name';
case 'registrationDate':
case 'registrationTime':
case 'registered':
case 'joinDate':
case 'joinTime':
case 'joined':
return 'registrationTime';
default:
return null;
}
}
}

View file

@ -3,7 +3,8 @@ namespace Szurubooru\Dao;
class UserDao extends AbstractDao implements ICrudDao class UserDao extends AbstractDao implements ICrudDao
{ {
public function __construct(\Szurubooru\DatabaseConnection $databaseConnection) public function __construct(
\Szurubooru\DatabaseConnection $databaseConnection)
{ {
parent::__construct($databaseConnection, 'users', '\Szurubooru\Entities\User'); parent::__construct($databaseConnection, 'users', '\Szurubooru\Entities\User');
} }

View file

@ -0,0 +1,9 @@
<?php
namespace Szurubooru\FormData;
class SearchFormData
{
public $query;
public $order;
public $pageNumber;
}

View file

@ -30,6 +30,8 @@ class HttpHelper
public function getRequestUri() public function getRequestUri()
{ {
return $_SERVER['REQUEST_URI']; $requestUri = $_SERVER['REQUEST_URI'];
$requestUri = preg_replace('/\?.*$/', '', $requestUri);
return $requestUri;
} }
} }

View file

@ -3,26 +3,40 @@ namespace Szurubooru\Services;
class UserService class UserService
{ {
private $config;
private $validator; private $validator;
private $userDao; private $userDao;
private $userSearchService;
private $passwordService; private $passwordService;
private $emailService; private $emailService;
private $timeService; private $timeService;
public function __construct( public function __construct(
\Szurubooru\Config $config,
\Szurubooru\Validator $validator, \Szurubooru\Validator $validator,
\Szurubooru\Dao\UserDao $userDao, \Szurubooru\Dao\UserDao $userDao,
\Szurubooru\Dao\Services\UserSearchService $userSearchService,
\Szurubooru\Services\PasswordService $passwordService, \Szurubooru\Services\PasswordService $passwordService,
\Szurubooru\Services\EmailService $emailService, \Szurubooru\Services\EmailService $emailService,
\Szurubooru\Services\TimeService $timeService) \Szurubooru\Services\TimeService $timeService)
{ {
$this->config = $config;
$this->validator = $validator; $this->validator = $validator;
$this->userDao = $userDao; $this->userDao = $userDao;
$this->userSearchService = $userSearchService;
$this->passwordService = $passwordService; $this->passwordService = $passwordService;
$this->emailService = $emailService; $this->emailService = $emailService;
$this->timeService = $timeService; $this->timeService = $timeService;
} }
public function getFiltered(\Szurubooru\FormData\SearchFormData $formData)
{
$pageSize = intval($this->config->users->usersPerPage);
$this->validator->validateNumber($formData->page);
$searchFilter = new \Szurubooru\Dao\SearchFilter($pageSize, $formData);
return $this->userSearchService->getFiltered($searchFilter);
}
public function register(\Szurubooru\FormData\RegistrationFormData $formData) public function register(\Szurubooru\FormData\RegistrationFormData $formData)
{ {
$this->validator->validateUserName($formData->name); $this->validator->validateUserName($formData->name);

View file

@ -10,6 +10,10 @@ class Validator
$this->config = $config; $this->config = $config;
} }
public function validateNumber(&$subject) {
$subject = intval($subject);
}
public function validateNonEmpty($subject, $subjectName = 'Object') public function validateNonEmpty($subject, $subjectName = 'Object')
{ {
if (!$subject) if (!$subject)

View file

@ -8,3 +8,5 @@ abstract class AbstractTestCase extends \PHPUnit_Framework_TestCase
return $this->getMockBuilder($className)->disableOriginalConstructor()->getMock(); return $this->getMockBuilder($className)->disableOriginalConstructor()->getMock();
} }
} }
date_default_timezone_set('UTC');

View file

@ -0,0 +1,67 @@
<?php
namespace Szurubooru\Tests\Dao\Services;
class UserSearchServiceTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
{
private $userDao;
public function setUp()
{
parent::setUp();
$this->userDao = new \Szurubooru\Dao\UserDao($this->databaseConnection);
}
public function testNothing()
{
$searchFilter = new \Szurubooru\Dao\SearchFilter(1);
$expected = new \Szurubooru\Dao\SearchResult($searchFilter, [], 0);
$userSearchService = $this->getUserSearchService();
$actual = $userSearchService->getFiltered($searchFilter);
$this->assertEquals($expected, $actual);
}
public function testSorting()
{
$user1 = new \Szurubooru\Entities\User();
$user1->name = 'reginald';
$user1->registrationTime = date('c', mktime(3, 2, 1));
$user2 = new \Szurubooru\Entities\User();
$user2->name = 'beartato';
$user2->registrationTime = date('c', mktime(1, 2, 3));
$this->userDao->save($user1);
$this->userDao->save($user2);
$userSearchService = $this->getUserSearchService();
$searchFilter = new \Szurubooru\Dao\SearchFilter(1);
$expected = new \Szurubooru\Dao\SearchResult($searchFilter, [$user2], 2);
$actual = $userSearchService->getFiltered($searchFilter);
$this->assertEquals($expected, $actual);
$searchFilter->order = 'name,asc';
$expected = new \Szurubooru\Dao\SearchResult($searchFilter, [$user2], 2);
$actual = $userSearchService->getFiltered($searchFilter);
$this->assertEquals($expected, $actual);
$searchFilter->order = 'name,desc';
$expected = new \Szurubooru\Dao\SearchResult($searchFilter, [$user1], 2);
$actual = $userSearchService->getFiltered($searchFilter);
$this->assertEquals($expected, $actual);
$searchFilter->order = 'registrationTime,desc';
$expected = new \Szurubooru\Dao\SearchResult($searchFilter, [$user1], 2);
$actual = $userSearchService->getFiltered($searchFilter);
$this->assertEquals($expected, $actual);
$searchFilter->order = 'registrationTime';
$expected = new \Szurubooru\Dao\SearchResult($searchFilter, [$user2], 2);
$actual = $userSearchService->getFiltered($searchFilter);
$this->assertEquals($expected, $actual);
}
private function getUserSearchService()
{
return new \Szurubooru\Dao\Services\UserSearchService($this->userDao);
}
}

View file

@ -5,7 +5,7 @@ final class UserDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
{ {
public function testRetrievingByValidName() public function testRetrievingByValidName()
{ {
$userDao = new \Szurubooru\Dao\UserDao($this->databaseConnection); $userDao = $this->getUserDao();
$user = new \Szurubooru\Entities\User(); $user = new \Szurubooru\Entities\User();
$user->name = 'test'; $user->name = 'test';
@ -19,7 +19,7 @@ final class UserDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
public function testRetrievingByInvalidName() public function testRetrievingByInvalidName()
{ {
$userDao = new \Szurubooru\Dao\UserDao($this->databaseConnection); $userDao = $this->getUserDao();
$actual = $userDao->getByName('rubbish'); $actual = $userDao->getByName('rubbish');
@ -28,7 +28,7 @@ final class UserDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
public function testCheckingUserPresence() public function testCheckingUserPresence()
{ {
$userDao = new \Szurubooru\Dao\UserDao($this->databaseConnection); $userDao = $this->getUserDao();
$this->assertFalse($userDao->hasAnyUsers()); $this->assertFalse($userDao->hasAnyUsers());
@ -38,4 +38,9 @@ final class UserDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
$this->assertTrue($userDao->hasAnyUsers()); $this->assertTrue($userDao->hasAnyUsers());
} }
private function getUserDao()
{
return new \Szurubooru\Dao\UserDao($this->databaseConnection);
}
} }

View file

@ -3,21 +3,44 @@ namespace Szurubooru\Tests\Services;
final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
{ {
private $configMock;
private $validatorMock; private $validatorMock;
private $userDaoMock; private $userDaoMock;
private $userSearchServiceMock;
private $passwordServiceMock; private $passwordServiceMock;
private $emailServiceMock; private $emailServiceMock;
private $timeServiceMock; private $timeServiceMock;
public function setUp() public function setUp()
{ {
$this->configMock = $this->mock(\Szurubooru\Config::class);
$this->validatorMock = $this->mock(\Szurubooru\Validator::class); $this->validatorMock = $this->mock(\Szurubooru\Validator::class);
$this->userDaoMock = $this->mock(\Szurubooru\Dao\UserDao::class); $this->userDaoMock = $this->mock(\Szurubooru\Dao\UserDao::class);
$this->userSearchService = $this->mock(\Szurubooru\Dao\Services\UserSearchService::class);
$this->passwordServiceMock = $this->mock(\Szurubooru\Services\PasswordService::class); $this->passwordServiceMock = $this->mock(\Szurubooru\Services\PasswordService::class);
$this->emailServiceMock = $this->mock(\Szurubooru\Services\EmailService::class); $this->emailServiceMock = $this->mock(\Szurubooru\Services\EmailService::class);
$this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class); $this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class);
} }
public function testGettingFilteredUsers()
{
$mockUser = new \Szurubooru\Entities\User();
$mockUser->name = 'user';
$expected = [$mockUser];
$this->userSearchService->method('getFiltered')->willReturn($expected);
$this->configMock->users = new \StdClass;
$this->configMock->users->usersPerPage = 1;
$searchFormData = new \Szurubooru\FormData\SearchFormData;
$searchFormData->query = '';
$searchFormData->order = 'joined';
$searchFormData->page = 2;
$userService = $this->getUserService();
$actual = $userService->getFiltered($searchFormData);
$this->assertEquals($expected, $actual);
}
public function testValidRegistration() public function testValidRegistration()
{ {
$formData = new \Szurubooru\FormData\RegistrationFormData; $formData = new \Szurubooru\FormData\RegistrationFormData;
@ -76,8 +99,10 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
private function getUserService() private function getUserService()
{ {
return new \Szurubooru\Services\UserService( return new \Szurubooru\Services\UserService(
$this->configMock,
$this->validatorMock, $this->validatorMock,
$this->userDaoMock, $this->userDaoMock,
$this->userSearchService,
$this->passwordServiceMock, $this->passwordServiceMock,
$this->emailServiceMock, $this->emailServiceMock,
$this->timeServiceMock); $this->timeServiceMock);