test(tests): UT for JsonResponse class

Signed-off-by: Rubén D <nuxsmin@syspass.org>
This commit is contained in:
Rubén D
2024-05-08 19:58:15 +02:00
parent c758b7f0b6
commit 9b170b2fb8
13 changed files with 264 additions and 116 deletions

View File

@@ -24,8 +24,6 @@
namespace SP\Modules\Web\Controllers;
defined('APP_ROOT') || die();
use Exception;
use SP\Core\Application;
use SP\Core\Bootstrap\BootstrapBase;
@@ -36,9 +34,9 @@ use SP\Domain\Auth\Services\AuthException;
use SP\Domain\Config\Ports\ConfigDataInterface;
use SP\Domain\Config\Ports\ConfigFileService;
use SP\Domain\Core\Acl\AclInterface;
use SP\Domain\Core\Bootstrap\RouteContextData;
use SP\Domain\Core\Bootstrap\UriContextInterface;
use SP\Domain\Core\Context\SessionContext;
use SP\Domain\Core\Exceptions\FileNotFoundException;
use SP\Domain\Core\Exceptions\SessionTimeout;
use SP\Domain\Core\Exceptions\SPException;
use SP\Domain\Core\PhpExtensionCheckerService;
@@ -51,7 +49,6 @@ use SP\Modules\Web\Controllers\Traits\WebControllerTrait;
use SP\Mvc\Controller\WebControllerHelper;
use SP\Mvc\View\TemplateInterface;
use function SP\__;
use function SP\logger;
use function SP\processException;
@@ -70,22 +67,23 @@ abstract class ControllerBase
protected readonly ThemeInterface $theme;
protected readonly AclInterface $acl;
protected readonly ConfigDataInterface $configData;
protected readonly RequestService $request;
protected readonly RequestService $request;
protected readonly PhpExtensionCheckerService $extensionChecker;
protected readonly TemplateInterface $view;
protected readonly LayoutHelper $layoutHelper;
protected readonly UriContextInterface $uriContext;
protected ?UserDataDto $userData = null;
protected ?ProfileData $userProfileData = null;
protected bool $isAjax;
protected string $actionName;
protected readonly ?UserDataDto $userData;
protected readonly ProfileData $userProfileData;
protected readonly bool $isAjax;
protected readonly string $actionName;
protected readonly RouteContextData $routeContextData;
protected readonly string $controllerName;
private readonly BrowserAuthService $browser;
public function __construct(
Application $application,
WebControllerHelper $webControllerHelper
) {
$this->controllerName = $this->getControllerName();
public function __construct(Application $application, WebControllerHelper $webControllerHelper)
{
$this->routeContextData = $webControllerHelper->getRouteContextData();
$this->controllerName = $this->routeContextData->getController();
$this->config = $application->getConfig();
$this->configData = $this->config->getConfigData();
$this->eventDispatcher = $application->getEventDispatcher();
@@ -125,8 +123,9 @@ abstract class ControllerBase
$this->view->assign('timeStart', $this->request->getServer('REQUEST_TIME_FLOAT'));
$this->view->assign('queryTimeStart', microtime());
$this->view->assign('isDemo', $this->configData->isDemoEnabled());
$this->view->assign('themeUri', $this->view->getTheme()->getUri());
$this->view->assign('themeUri', $this->theme->getUri());
$this->view->assign('configData', $this->configData);
$this->view->assign('action', $this->actionName);
if ($loggedIn) {
$this->view->assignWithScope('userId', $this->userData->getId(), 'ctx');
@@ -134,8 +133,6 @@ abstract class ControllerBase
$this->view->assignWithScope('userIsAdminApp', $this->userData->getIsAdminApp(), 'ctx');
$this->view->assignWithScope('userIsAdminAcc', $this->userData->getIsAdminAcc(), 'ctx');
}
$this->view->assign('action', $this->actionName);
}
/**
@@ -143,13 +140,7 @@ abstract class ControllerBase
*/
protected function view(): void
{
try {
$this->router->response()->body($this->view->render())->send();
} catch (FileNotFoundException $e) {
processException($e);
$this->router->response()->body(__($e->getMessage()))->send(true);
}
$this->router->response()->body($this->view->render())->send();
}
/**
@@ -166,7 +157,7 @@ abstract class ControllerBase
protected function upgradeView(?string $page = null): void
{
$this->view->upgrade();
$this->view->assign('contentPage', $page ?: strtolower($this->getViewBaseName()));
$this->view->assign('contentPage', $page ?: strtolower($this->routeContextData->getActionName()));
try {
$this->layoutHelper->getFullLayout('main', $this->acl);

View File

@@ -31,6 +31,7 @@ use SP\Core\PhpExtensionChecker;
use SP\Domain\Config\Ports\ConfigDataInterface;
use SP\Domain\Config\Services\ConfigFile;
use SP\Domain\Core\Acl\UnauthorizedPageException;
use SP\Domain\Core\Bootstrap\RouteContextData;
use SP\Domain\Core\Bootstrap\UriContextInterface;
use SP\Domain\Core\Context\Context;
use SP\Domain\Core\Exceptions\SessionTimeout;
@@ -56,6 +57,8 @@ abstract class SimpleControllerBase
protected readonly PhpExtensionChecker $extensionChecker;
protected readonly ConfigDataInterface $configData;
protected readonly UriContextInterface $uriContext;
protected readonly RouteContextData $routeContextData;
protected string $controllerName;
/**
* @throws SessionTimeout
@@ -70,7 +73,8 @@ abstract class SimpleControllerBase
$this->request = $simpleControllerHelper->getRequest();
$this->extensionChecker = $simpleControllerHelper->getExtensionChecker();
$this->uriContext = $simpleControllerHelper->getUriContext();
$this->controllerName = $this->getControllerName();
$this->routeContextData = $simpleControllerHelper->getRouteContextData();
$this->controllerName = $this->routeContextData->getController();
$this->config = $application->getConfig();
$this->configData = $this->config->getConfigData();
$this->eventDispatcher = $application->getEventDispatcher();

View File

@@ -36,7 +36,7 @@ final class IndexController extends ControllerBase
{
public function indexAction(): void
{
$this->layoutHelper->getCustomLayout('request', strtolower($this->getViewBaseName()));
$this->layoutHelper->getCustomLayout('request', strtolower($this->routeContextData->getActionName()));
if (!$this->configData->isMailEnabled()) {
ErrorUtil::showErrorInView($this->view, self::ERR_UNAVAILABLE, true, 'request');

View File

@@ -39,7 +39,7 @@ final class ResetController extends ControllerBase
*/
public function resetAction(?string $hash = null): void
{
$this->layoutHelper->getCustomLayout('reset', strtolower($this->getViewBaseName()));
$this->layoutHelper->getCustomLayout('reset', strtolower($this->routeContextData->getActionName()));
if ($hash !== null && $this->configData->isMailEnabled()) {
$this->view->assign('hash', $hash);

View File

@@ -70,7 +70,7 @@ abstract class UserPassResetSaveBase extends ControllerBase
$this->mailService = $mailService;
$this->trackService = $trackService;
$this->trackRequest = $this->trackService->buildTrackRequest($this->getViewBaseName());
$this->trackRequest = $this->trackService->buildTrackRequest($this->routeContextData->getActionName());
}
/**

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* sysPass
@@ -27,8 +28,8 @@ namespace SP\Domain\Api\Services;
use Exception;
use SP\Domain\Api\Dtos\ApiResponse;
use SP\Domain\Common\Adapters\Serde;
use SP\Domain\Core\Exceptions\SPException;
use SP\Domain\Http\Services\JsonResponse;
/**
* Class JsonRpcResponse
@@ -53,11 +54,14 @@ final class JsonRpcResponse
ApiResponse $apiResponse,
int $id
): string {
return JsonResponse::buildJsonFrom([
'jsonrpc' => '2.0',
'result' => $apiResponse->getResponse(),
'id' => $id,
], JSON_UNESCAPED_SLASHES);
return Serde::serializeJson(
[
'jsonrpc' => '2.0',
'result' => $apiResponse->getResponse(),
'id' => $id,
],
JSON_UNESCAPED_SLASHES
);
}
public static function getResponseException(Exception $e, int $id): string

View File

@@ -34,7 +34,7 @@ use SP\Domain\Http\Dtos\JsonMessage;
interface JsonResponseService
{
/**
* Devuelve una respuesta en formato JSON
* Return a response with JSON headers
*
* @param string $data JSON string
*
@@ -43,7 +43,7 @@ interface JsonResponseService
public function sendRaw(string $data): bool;
/**
* Devuelve una respuesta en formato JSON con el estado y el mensaje.
* Return a JSON formatted response
*
* @param JsonMessage $jsonMessage
*

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* sysPass
@@ -25,15 +26,13 @@ declare(strict_types=1);
namespace SP\Domain\Http\Services;
use JsonException;
use Klein\Response;
use SP\Domain\Common\Adapters\Serde;
use SP\Domain\Core\Exceptions\SPException;
use SP\Domain\Http\Dtos\JsonMessage;
use SP\Domain\Http\Header;
use SP\Domain\Http\Ports\JsonResponseService;
use function SP\__u;
/**
* Class JsonResponse
*/
@@ -52,11 +51,7 @@ final readonly class JsonResponse implements JsonResponseService
}
/**
* Devuelve una respuesta en formato JSON
*
* @param string $data JSON string
*
* @return bool
* @inheritDoc
*/
public function sendRaw(string $data): bool
{
@@ -68,44 +63,24 @@ final readonly class JsonResponse implements JsonResponseService
}
/**
* Devuelve una respuesta en formato JSON con el estado y el mensaje.
*
* @param JsonMessage $jsonMessage
*
* @return bool
* @throws SPException
* @inheritDoc
*/
public function send(JsonMessage $jsonMessage): bool
{
$this->response->header(Header::CONTENT_TYPE->value, Header::CONTENT_TYPE_JSON->value);
try {
$this->response->body(self::buildJsonFrom($jsonMessage));
$this->response->body(Serde::serializeJson($jsonMessage));
} catch (SPException $e) {
$jsonMessage = new JsonMessage($e->getMessage());
$jsonMessage->addMessage($e->getHint());
$this->response->body(self::buildJsonFrom($jsonMessage));
if ($e->getHint()) {
$jsonMessage->addMessage($e->getHint());
}
$this->response->body(Serde::serializeJson($jsonMessage));
}
return $this->response->send(true)->isSent();
}
/**
* Devuelve una cadena en formato JSON
*
* @param mixed $data
* @param int $flags JSON_* flags
*
* @return string
* @throws SPException
*/
public static function buildJsonFrom(mixed $data, int $flags = 0): string
{
try {
return json_encode($data, JSON_THROW_ON_ERROR | $flags);
} catch (JsonException $e) {
throw new SPException(__u('Encoding error'), SPException::ERROR, $e->getMessage());
}
}
}

View File

@@ -27,7 +27,6 @@ declare(strict_types=1);
namespace SP\Mvc\Controller;
use Closure;
use JetBrains\PhpStorm\NoReturn;
use Klein\Klein;
use SP\Domain\Config\Ports\ConfigDataInterface;
use SP\Domain\Core\Exceptions\SPException;
@@ -44,20 +43,7 @@ use function SP\processException;
*/
trait ControllerTrait
{
protected Klein $router;
protected string $controllerName;
protected function getControllerName(): string
{
$class = static::class;
return substr($class, strrpos($class, '\\') + 1, -strlen('Controller')) ?: '';
}
protected function getViewBaseName(): string
{
return strtolower(array_slice(explode('\\', static::class), -2, 1)[0]);
}
protected Klein $router;
/**
* Logout from current session
@@ -110,7 +96,7 @@ trait ControllerTrait
/**
* Realiza el proceso de logout.
*/
#[NoReturn] private static function logout(): void
private static function logout(): never
{
exit('<script>sysPassApp.actions.main.logout();</script>');
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* sysPass
@@ -28,6 +29,7 @@ namespace SP\Mvc\Controller;
use Klein\Klein;
use SP\Core\PhpExtensionChecker;
use SP\Domain\Core\Acl\AclInterface;
use SP\Domain\Core\Bootstrap\RouteContextData;
use SP\Domain\Core\Bootstrap\UriContextInterface;
use SP\Domain\Core\UI\ThemeInterface;
use SP\Domain\Http\Ports\RequestService;
@@ -42,9 +44,10 @@ final readonly class SimpleControllerHelper
private ThemeInterface $theme,
private Klein $router,
private AclInterface $acl,
private RequestService $request,
private RequestService $request,
private PhpExtensionChecker $extensionChecker,
private UriContextInterface $uriContext
private UriContextInterface $uriContext,
private RouteContextData $routeContextData
) {
}
@@ -77,4 +80,9 @@ final readonly class SimpleControllerHelper
{
return $this->uriContext;
}
public function getRouteContextData(): RouteContextData
{
return $this->routeContextData;
}
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* sysPass
@@ -29,6 +30,7 @@ use Klein\Klein;
use SP\Core\PhpExtensionChecker;
use SP\Domain\Auth\Providers\Browser\BrowserAuthService;
use SP\Domain\Core\Acl\AclInterface;
use SP\Domain\Core\Bootstrap\RouteContextData;
use SP\Domain\Core\Bootstrap\UriContextInterface;
use SP\Domain\Core\UI\ThemeInterface;
use SP\Domain\Http\Ports\RequestService;
@@ -40,55 +42,42 @@ use SP\Mvc\View\TemplateInterface;
*/
final readonly class WebControllerHelper
{
private ThemeInterface $theme;
private Klein $router;
private AclInterface $acl;
private RequestService $request;
private PhpExtensionChecker $extensionChecker;
private UriContextInterface $uriContext;
public function __construct(
SimpleControllerHelper $simpleControllerHelper,
private TemplateInterface $template,
private BrowserAuthService $browser,
private LayoutHelper $layoutHelper
private SimpleControllerHelper $simpleControllerHelper,
private TemplateInterface $template,
private BrowserAuthService $browser,
private LayoutHelper $layoutHelper
) {
$this->theme = $simpleControllerHelper->getTheme();
$this->router = $simpleControllerHelper->getRouter();
$this->acl = $simpleControllerHelper->getAcl();
$this->request = $simpleControllerHelper->getRequest();
$this->extensionChecker = $simpleControllerHelper->getExtensionChecker();
$this->uriContext = $simpleControllerHelper->getUriContext();
}
public function getTheme(): ThemeInterface
{
return $this->theme;
return $this->simpleControllerHelper->getTheme();
}
public function getRouter(): Klein
{
return $this->router;
return $this->simpleControllerHelper->getRouter();
}
public function getAcl(): AclInterface
{
return $this->acl;
return $this->simpleControllerHelper->getAcl();
}
public function getRequest(): RequestService
{
return $this->request;
return $this->simpleControllerHelper->getRequest();
}
public function getExtensionChecker(): PhpExtensionChecker
{
return $this->extensionChecker;
return $this->simpleControllerHelper->getExtensionChecker();
}
public function getUriContext(): UriContextInterface
{
return $this->uriContext;
return $this->simpleControllerHelper->getUriContext();
}
public function getTemplate(): TemplateInterface
@@ -105,4 +94,9 @@ final readonly class WebControllerHelper
{
return $this->layoutHelper;
}
public function getRouteContextData(): RouteContextData
{
return $this->simpleControllerHelper->getRouteContextData();
}
}

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* sysPass
@@ -26,9 +27,9 @@ declare(strict_types=1);
namespace SP\Mvc\View\Components;
use RuntimeException;
use SP\Domain\Common\Adapters\Serde;
use SP\Domain\Common\Models\ItemWithIdAndNameModel;
use SP\Domain\Core\Exceptions\SPException;
use SP\Domain\Http\Services\JsonResponse;
use function SP\__u;
@@ -65,7 +66,7 @@ final readonly class SelectItemAdapter implements ItemAdapterInterface
*/
public function getJsonItemsFromModel(): string
{
return JsonResponse::buildJsonFrom(
return Serde::serializeJson(
array_map(
static fn(ItemWithIdAndNameModel $item) => ['id' => $item->getId(), 'name' => $item->getName()],
array_filter($this->items, static fn(mixed $item) => $item instanceof ItemWithIdAndNameModel)
@@ -86,7 +87,7 @@ final readonly class SelectItemAdapter implements ItemAdapterInterface
$out[] = ['id' => $key, 'name' => $value];
}
return JsonResponse::buildJsonFrom($out);
return Serde::serializeJson($out);
}
/**

View File

@@ -0,0 +1,185 @@
<?php
/**
* sysPass
*
* @author nuxsmin
* @link https://syspass.org
* @copyright 2012-2024, 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/>.
*/
declare(strict_types=1);
namespace SP\Tests\Domain\Http\Services;
use Klein\Response;
use PHPUnit\Framework\Attributes\DoesNotPerformAssertions;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\MockObject\Stub\ConsecutiveCalls;
use PHPUnit\Framework\MockObject\Stub\ReturnSelf;
use PHPUnit\Framework\TestCase;
use SP\Domain\Core\Exceptions\SPException;
use SP\Domain\Http\Dtos\JsonMessage;
use SP\Domain\Http\Services\JsonResponse;
use SP\Tests\PHPUnitHelper;
/**
* Class JsonResponseTest
*/
#[Group('unitary')]
class JsonResponseTest extends TestCase
{
use PHPUnitHelper;
private MockObject|Response $response;
private JsonResponse $jsonResponse;
#[TestWith([true])]
#[TestWith([false])]
public function testSendRaw(bool $isSent)
{
$this->response
->expects($this->once())
->method('header')
->with('Content-type', 'application/json; charset=utf-8')
->willReturnSelf();
$this->response
->expects($this->once())
->method('body')
->with('a_response')
->willReturnSelf();
$this->response
->expects($this->once())
->method('send')
->with(true)
->willReturnSelf();
$this->response
->expects($this->once())
->method('isSent')
->willReturn($isSent);
$this->assertEquals($isSent, $this->jsonResponse->sendRaw('a_response'));
}
#[DoesNotPerformAssertions]
public function testFactory()
{
JsonResponse::factory($this->response);
}
/**
* @throws SPException
*/
#[TestWith([true])]
#[TestWith([false])]
public function testSend(bool $isSent)
{
$message = new JsonMessage('a_test');
$message->setData(['test' => 'a_data']);
$message->addMessage('a_message');
$this->response
->expects($this->once())
->method('header')
->with('Content-type', 'application/json; charset=utf-8')
->willReturnSelf();
$this->response
->expects($this->once())
->method('body')
->with('{"status":1,"description":"a_test","data":{"test":"a_data"},"messages":["a_message"]}')
->willReturnSelf();
$this->response
->expects($this->once())
->method('send')
->with(true)
->willReturnSelf();
$this->response
->expects($this->once())
->method('isSent')
->willReturn($isSent);
$this->assertEquals($isSent, $this->jsonResponse->send($message));
}
/**
* @throws SPException
*/
#[TestWith([true])]
#[TestWith([false])]
public function testSendWithException(bool $isSent)
{
$message = new JsonMessage('a_test');
$message->setData(['test' => 'a_data']);
$message->addMessage('a_message');
$this->response
->expects($this->once())
->method('header')
->with('Content-type', 'application/json; charset=utf-8')
->willReturnSelf();
$bodyOk = '{"status":1,"description":"a_test","data":{"test":"a_data"},"messages":["a_message"]}';
$bodyError = '{"status":1,"description":"test","data":[],"messages":["a_hint"]}';
$this->response
->expects($this->exactly(2))
->method('body')
->with(...self::withConsecutive([$bodyOk], [$bodyError]))
->will(
new ConsecutiveCalls(
[
new \PHPUnit\Framework\MockObject\Stub\Exception(SPException::error('test', 'a_hint')),
new ReturnSelf(),
]
)
);
$this->response
->expects($this->once())
->method('send')
->with(true)
->willReturnSelf();
$this->response
->expects($this->once())
->method('isSent')
->willReturn($isSent);
$this->assertEquals($isSent, $this->jsonResponse->send($message));
}
/**
* @throws Exception
*/
protected function setUp(): void
{
parent::setUp();
$this->response = $this->createMock(Response::class);
$this->jsonResponse = new JsonResponse($this->response);
}
}