* [ADD] Unit testing. Work in progress.

* [MOD] Improved accounts search filtering by using an operator (AND/OR) for searching filters. The operator will work on any filter field set (text, client, category and tags).
This commit is contained in:
nuxsmin
2018-05-31 01:38:55 +02:00
parent 6d5d823681
commit ea8baebf37
8 changed files with 337 additions and 77 deletions

View File

@@ -74,7 +74,7 @@ class AccountSearchFilter
/**
* @var array
*/
private $tagsId = [];
private $tagsId;
/**
* @var int
*/
@@ -265,7 +265,7 @@ class AccountSearchFilter
*/
public function getTagsId()
{
return $this->tagsId;
return $this->tagsId ?: [];
}
/**
@@ -281,12 +281,20 @@ class AccountSearchFilter
return $this;
}
/**
* @return bool
*/
public function hasTags()
{
return !empty($this->tagsId);
}
/**
* @return QueryCondition
*/
public function getStringFilters()
{
return $this->stringFilters;
return $this->stringFilters ?: new QueryCondition();
}
/**
@@ -394,7 +402,7 @@ class AccountSearchFilter
*/
public function getFilterOperator()
{
return $this->filterOperator;
return $this->filterOperator ?: QueryCondition::CONDITION_AND;
}
/**
@@ -404,4 +412,24 @@ class AccountSearchFilter
{
$this->filterOperator = $filterOperator;
}
/**
* Resets internal variables
*/
public function reset()
{
self::$queryNumRows = null;
$this->categoryId = null;
$this->clientId = null;
$this->filterOperator = null;
$this->globalSearch = false;
$this->txtSearch = null;
$this->cleanTxtSearch = null;
$this->tagsId = null;
$this->limitCount = null;
$this->sortViews = null;
$this->searchFavorites = false;
$this->sortOrder = self::SORT_DEFAULT;
$this->sortKey = self::SORT_DIR_ASC;
}
}

View File

@@ -69,7 +69,7 @@ class QueryCondition
throw new \RuntimeException(__u('Tipo de filtro inválido'));
}
return $this->hasFilters() ? implode($type, $this->query) : null;
return $this->hasFilters() ? '(' . implode($type, $this->query) . ')' : null;
}
/**

View File

@@ -0,0 +1,90 @@
<?php
/**
* sysPass
*
* @author nuxsmin
* @link https://syspass.org
* @copyright 2012-2018, Rubén Domínguez nuxsmin@$syspass.org
*
* This file is part of sysPass.
*
* sysPass is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* sysPass is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
namespace SP\Mvc\Model;
/**
* Class QueryJoin
*
* @package SP\Mvc\Model
*/
class QueryJoin
{
/**
* @var array
*/
protected $join = [];
/**
* @var array
*/
protected $param = [];
/**
* @param string $join
* @param array $params
* @return QueryJoin
*/
public function addJoin($join, array $params = null)
{
$this->join[] = $join;
if ($params !== null) {
$this->param = array_merge($this->param, $params);
}
return $this;
}
/**
* @return string|null
*/
public function getJoins()
{
return $this->hasJoins() ? implode(PHP_EOL, $this->join) : null;
}
/**
* @return bool
*/
public function hasJoins()
{
return !empty($this->join);
}
/**
* @return array
*/
public function getParams()
{
return $this->param;
}
/**
* @return int
*/
public function getJoinsCount()
{
return count($this->join);
}
}

View File

@@ -39,6 +39,7 @@ use SP\DataModel\ItemData;
use SP\DataModel\ItemSearchData;
use SP\Mvc\Model\QueryAssignment;
use SP\Mvc\Model\QueryCondition;
use SP\Mvc\Model\QueryJoin;
use SP\Repositories\Repository;
use SP\Repositories\RepositoryItemInterface;
use SP\Repositories\RepositoryItemTrait;
@@ -579,68 +580,61 @@ class AccountRepository extends Repository implements RepositoryItemInterface
*/
public function getByFilter(AccountSearchFilter $accountSearchFilter)
{
$queryFilterCommon = new QueryCondition();
$queryFilterSelect = new QueryCondition();
$queryFilters = new QueryCondition();
// Sets the search text depending on if special search filters are being used
$searchText = $accountSearchFilter->getCleanTxtSearch();
if (!empty($searchText)) {
$searchText = '%' . $searchText . '%';
$queryFilterCommon->addFilter('A.name LIKE ? OR A.login LIKE ? OR A.url LIKE ? OR A.notes LIKE ?', [$searchText, $searchText, $searchText, $searchText]);
$queryFilters->addFilter('A.name LIKE ? OR A.login LIKE ? OR A.url LIKE ? OR A.notes LIKE ?', array_fill(0, 4, '%' . $searchText . '%'));
}
// Gets special search filters
$stringFilters = $accountSearchFilter->getStringFilters();
if ($stringFilters->hasFilters()) {
$queryFilterCommon->addFilter($stringFilters->getFilters(), $stringFilters->getParams());
$queryFilters->addFilter($stringFilters->getFilters(), $stringFilters->getParams());
}
if (!empty($accountSearchFilter->getCategoryId())) {
$queryFilterSelect->addFilter('A.categoryId = ?', [$accountSearchFilter->getCategoryId()]);
$queryFilters->addFilter('A.categoryId = ?', [$accountSearchFilter->getCategoryId()]);
}
if (!empty($accountSearchFilter->getClientId())) {
$queryFilterSelect->addFilter('A.clientId = ?', [$accountSearchFilter->getClientId()]);
}
$tagsId = $accountSearchFilter->getTagsId();
$numTags = count($tagsId);
if ($numTags > 0) {
$queryFilterSelect->addFilter('A.id IN (SELECT accountId FROM AccountToTag WHERE tagId IN (' . str_repeat('?,', $numTags - 1) . '?' . '))', $tagsId);
$queryFilters->addFilter('A.clientId = ?', [$accountSearchFilter->getClientId()]);
}
$where = [];
if ($queryFilterCommon->hasFilters()) {
$where[] = $queryFilterCommon->getFilters($accountSearchFilter->getFilterOperator());
}
if ($queryFilterSelect->hasFilters()) {
$where[] = $queryFilterSelect->getFilters();
}
$queryFilterUser = AccountUtil::getAccountFilterUser($this->context, $accountSearchFilter->getGlobalSearch());
if ($queryFilterUser->hasFilters()) {
$where[] = $queryFilterUser->getFilters();
}
$join = ['query' => [], 'param' => []];
$queryJoins = new QueryJoin();
if ($accountSearchFilter->isSearchFavorites() === true) {
$join['query'][] = 'INNER JOIN AccountToFavorite AF ON (AF.accountId = A.id AND AF.userId = ?)';
$join['param'][] = $this->context->getUserData()->getId();
$queryJoins->addJoin('INNER JOIN AccountToFavorite AF ON (AF.accountId = A.id AND AF.userId = ?)', [$this->context->getUserData()->getId()]);
}
if ($accountSearchFilter->hasTags()) {
$queryJoins->addJoin('INNER JOIN AccountToTag AT ON AT.accountId = A.id');
foreach ($accountSearchFilter->getTagsId() as $tag) {
$queryFilters->addFilter('AT.tagId = ?', [$tag]);
}
}
if ($queryFilters->hasFilters()) {
$where[] = $queryFilters->getFilters($accountSearchFilter->getFilterOperator());
}
$queryData = new QueryData();
$queryData->setWhere($where);
$queryData->setParams(array_merge($join['param'], $queryFilterCommon->getParams(), $queryFilterSelect->getParams(), $queryFilterUser->getParams()));
$queryData->setParams(array_merge($queryJoins->getParams(), $queryFilterUser->getParams(), $queryFilters->getParams()));
$queryData->setSelect('*');
$queryData->setFrom('account_search_v A ' . implode(PHP_EOL, $join['query']));
$queryData->setFrom('account_search_v A ' . $queryJoins->getJoins());
$queryData->setOrder($accountSearchFilter->getOrderString());
if ($accountSearchFilter->getLimitCount() > 0) {

View File

@@ -120,7 +120,7 @@ class AccountSearchService extends Service
/**
* @var string
*/
private $filterOperator = QueryCondition::CONDITION_OR;
private $filterOperator;
/**
* Procesar los resultados de la búsqueda y crear la variable que contiene los datos de cada cuenta

View File

@@ -56,7 +56,7 @@ class Installer extends Service
*/
const VERSION = [3, 0, 0];
const VERSION_TEXT = '3.0-beta';
const BUILD = 18053001;
const BUILD = 18053101;
/**
* @var ConfigService

View File

@@ -24,25 +24,22 @@
namespace SP\Tests;
use DI\ContainerBuilder;
use DI\DependencyException;
use Doctrine\Common\Cache\ArrayCache;
use PHPUnit\DbUnit\Database\Connection;
use PHPUnit\DbUnit\Database\DefaultConnection;
use PHPUnit\DbUnit\DataSet\IDataSet;
use PHPUnit\DbUnit\TestCaseTrait;
use PHPUnit\Framework\TestCase;
use SP\Account\AccountRequest;
use SP\Config\ConfigData;
use SP\Core\Context\ContextInterface;
use SP\Account\AccountSearchFilter;
use SP\Core\Crypt\Crypt;
use SP\Core\Exceptions\SPException;
use SP\DataModel\AccountVData;
use SP\DataModel\Dto\AccountSearchResponse;
use SP\DataModel\ItemSearchData;
use SP\Mvc\Model\QueryCondition;
use SP\Repositories\Account\AccountRepository;
use SP\Services\Account\AccountPasswordRequest;
use SP\Services\User\UserLoginResponse;
use SP\Storage\DatabaseConnectionData;
use SP\Storage\MySQLHandler;
@@ -82,33 +79,10 @@ class AccountRepositoryTest extends TestCase
*/
public static function setUpBeforeClass()
{
// Instancia del contenedor de dependencias con las definiciones de los objetos necesarios
// para la aplicación
$builder = new ContainerBuilder();
$builder->setDefinitionCache(new ArrayCache());
$builder->addDefinitions(APP_ROOT . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'Definitions.php');
$dic = $builder->build();
$dic = setupContext();
// Inicializar el contexto
$context = $dic->get(ContextInterface::class);
$context->initialize();
$context->setConfig(new ConfigData());
$userData = new UserLoginResponse();
$userData->setId(1);
$userData->setUserGroupId(1);
$userData->setIsAdminApp(1);
$context->setUserData($userData);
self::$databaseConnectionData = (new DatabaseConnectionData())
->setDbHost('172.17.0.3')
->setDbName('syspass')
->setDbUser('root')
->setDbPass('syspass');
$dic->set(ConfigData::class, $context->getConfig());
$dic->set(DatabaseConnectionData::class, self::$databaseConnectionData);
// Datos de conexión a la BBDD
self::$databaseConnectionData = $dic->get(DatabaseConnectionData::class);
// Inicializar el repositorio
self::$accountRepository = $dic->get(AccountRepository::class);
@@ -270,30 +244,32 @@ class AccountRepositoryTest extends TestCase
*/
public function testSearch()
{
// Comprobar búsqueda con el texto Google Inc
$itemSearchData = new ItemSearchData();
$itemSearchData->setSeachString('Google');
$itemSearchData->setSeachString('Google Inc');
$itemSearchData->setLimitCount(10);
$search = self::$accountRepository->search($itemSearchData);
$this->assertCount(3, $search);
$this->assertCount(2, $search);
$this->assertArrayHasKey('count', $search);
$this->assertEquals(2, $search['count']);
$this->assertEquals(1, $search['count']);
$this->assertInstanceOf(\stdClass::class, $search[0]);
$this->assertEquals(1, $search[0]->id);
$this->assertEquals('Google', $search[0]->name);
// Comprobar búsqueda con el texto Apple
$itemSearchData = new ItemSearchData();
$itemSearchData->setSeachString('Google');
$itemSearchData->setSeachString('Apple');
$itemSearchData->setLimitCount(1);
$search = self::$accountRepository->search($itemSearchData);
$this->assertCount(2, $search);
$this->assertArrayHasKey('count', $search);
$this->assertEquals(2, $search['count']);
$this->assertEquals(1, $search['count']);
$this->assertInstanceOf(\stdClass::class, $search[0]);
$this->assertEquals(1, $search[0]->id);
$this->assertEquals('Google', $search[0]->name);
$this->assertEquals(2, $search[0]->id);
$this->assertEquals('Apple', $search[0]->name);
}
/**
@@ -389,24 +365,67 @@ class AccountRepositoryTest extends TestCase
$this->markTestSkipped();
}
/**
* Comprobar las cuentas devueltas para un filtro de usuario
*/
public function testGetForUser()
{
// self::$accountRepository->getForUser();
$queryCondition = new QueryCondition();
$queryCondition->addFilter('A.isPrivate = 1');
$this->assertCount(0, self::$accountRepository->getForUser($queryCondition));
}
/**
* Comprobar las cuentas devueltas para obtener los datos de las claves
*/
public function testGetAccountsPassData()
{
$this->assertCount(2, self::$accountRepository->getAccountsPassData());
}
/**
* Comprobar la creación de una cuenta
*
* @throws SPException
* @throws \Defuse\Crypto\Exception\CryptoException
* @throws \SP\Core\Exceptions\ConstraintException
* @throws \SP\Core\Exceptions\QueryException
*/
public function testCreate()
{
$accountRequest = new AccountRequest();
$accountRequest->name = 'Prueba 2';
$accountRequest->login = 'admin';
$accountRequest->url = 'http://syspass.org';
$accountRequest->notes = 'notas';
$accountRequest->userEditId = 1;
$accountRequest->passDateChange = time() + 3600;
$accountRequest->clientId = 1;
$accountRequest->categoryId = 1;
$accountRequest->isPrivate = 0;
$accountRequest->isPrivateGroup = 0;
$accountRequest->parentId = 0;
$accountRequest->userId = 1;
$accountRequest->userGroupId = 2;
$accountRequest->key = Crypt::makeSecuredKey(self::SECURE_KEY_PASSWORD);
$accountRequest->pass = Crypt::encrypt('1234', $accountRequest->key, self::SECURE_KEY_PASSWORD);
// Comprobar registros iniciales
$this->assertEquals(2, $this->conn->getRowCount('Account'));
self::$accountRepository->create($accountRequest);
// Comprobar registros finales
$this->assertEquals(3, $this->conn->getRowCount('Account'));
}
/**
* No implementado
*/
public function testGetByIdBatch()
{
$this->markTestSkipped();
}
/**
@@ -417,14 +436,90 @@ class AccountRepositoryTest extends TestCase
$this->markTestSkipped();
}
/**
* No implementado
*/
public function testGetPasswordHistoryForId()
{
$this->markTestSkipped();
}
/**
* Comprobar la búsqueda de cuentas mediante filtros
*/
public function testGetByFilter()
{
$searchFilter = new AccountSearchFilter();
$searchFilter->setLimitCount(10);
$searchFilter->setCategoryId(1);
// Comprobar un Id de categoría
$response = self::$accountRepository->getByFilter($searchFilter);
$this->assertInstanceOf(AccountSearchResponse::class, $response);
$this->assertEquals(1, $response->getCount());
$this->assertCount(1, $response->getData());
// Comprobar un Id de categoría no existente
$searchFilter->reset();
$searchFilter->setLimitCount(10);
$searchFilter->setCategoryId(10);
$response = self::$accountRepository->getByFilter($searchFilter);
$this->assertInstanceOf(AccountSearchResponse::class, $response);
$this->assertEquals(0, $response->getCount());
$this->assertCount(0, $response->getData());
// Comprobar un Id de cliente
$searchFilter->reset();
$searchFilter->setLimitCount(10);
$searchFilter->setClientId(1);
$response = self::$accountRepository->getByFilter($searchFilter);
$this->assertInstanceOf(AccountSearchResponse::class, $response);
$this->assertEquals(1, $response->getCount());
$this->assertCount(1, $response->getData());
// Comprobar un Id de cliente no existente
$searchFilter->reset();
$searchFilter->setLimitCount(10);
$searchFilter->setClientId(10);
$response = self::$accountRepository->getByFilter($searchFilter);
$this->assertInstanceOf(AccountSearchResponse::class, $response);
$this->assertEquals(0, $response->getCount());
$this->assertCount(0, $response->getData());
// Comprobar una cadena de texto
$searchFilter->reset();
$searchFilter->setLimitCount(10);
$searchFilter->setCleanTxtSearch('apple.com');
$response = self::$accountRepository->getByFilter($searchFilter);
$this->assertInstanceOf(AccountSearchResponse::class, $response);
$this->assertEquals(1, $response->getCount());
$this->assertCount(1, $response->getData());
$this->assertEquals(2, $response->getData()[0]->getId());
// Comprobar los favoritos
$searchFilter->reset();
$searchFilter->setLimitCount(10);
$searchFilter->setSearchFavorites(true);
$response = self::$accountRepository->getByFilter($searchFilter);
$this->assertInstanceOf(AccountSearchResponse::class, $response);
$this->assertEquals(0, $response->getCount());
$this->assertCount(0, $response->getData());
// Comprobar las etiquetas
$searchFilter->reset();
$searchFilter->setLimitCount(10);
$searchFilter->setTagsId([1]);
$response = self::$accountRepository->getByFilter($searchFilter);
$this->assertInstanceOf(AccountSearchResponse::class, $response);
$this->assertEquals(1, $response->getCount());
$this->assertCount(1, $response->getData());
$this->assertEquals(1, $response->getData()[0]->getId());
}
/**

View File

@@ -22,6 +22,15 @@
* along with sysPass. If not, see <http://www.gnu.org/licenses/>.
*/
namespace SP\Tests;
use DI\ContainerBuilder;
use Doctrine\Common\Cache\ArrayCache;
use SP\Config\ConfigData;
use SP\Core\Context\ContextInterface;
use SP\Services\User\UserLoginResponse;
use SP\Storage\DatabaseConnectionData;
define('APP_MODULE', 'tests');
define('APP_ROOT', dirname(__DIR__));
@@ -48,4 +57,48 @@ if (!function_exists('gettext')) {
{
return $str;
}
}
/**
* Configura el contexto de la aplicación para los tests
*
* @throws \DI\DependencyException
* @throws \DI\NotFoundException
* @throws \SP\Core\Context\ContextException
* @return \DI\Container
*/
function setupContext()
{
// Instancia del contenedor de dependencias con las definiciones de los objetos necesarios
// para la aplicación
$builder = new ContainerBuilder();
$builder->setDefinitionCache(new ArrayCache());
$builder->addDefinitions(APP_ROOT . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'Definitions.php');
$dic = $builder->build();
// Inicializar el contexto
$context = $dic->get(ContextInterface::class);
$context->initialize();
$context->setConfig(new ConfigData());
$userData = new UserLoginResponse();
$userData->setId(1);
$userData->setUserGroupId(1);
$userData->setIsAdminApp(1);
$context->setUserData($userData);
$databaseConnectionData = (new DatabaseConnectionData())
->setDbHost('172.17.0.2')
->setDbName('syspass')
->setDbUser('root')
->setDbPass('syspass');
// Inicializar la configuración
$dic->set(ConfigData::class, $context->getConfig());
// Inicializar los datos de conexión a la BBDD
$dic->set(DatabaseConnectionData::class, $databaseConnectionData);
return $dic;
}