Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e070db18b5 | ||
|
|
a6675605e4 | ||
|
|
520db45d6d | ||
|
|
2d3a1074f6 | ||
|
|
a1e8b1aa51 | ||
|
|
f25c9d8994 | ||
|
|
3854737622 | ||
|
|
d16a4e427d | ||
|
|
4871ff14dc | ||
|
|
ac99e079ee | ||
|
|
09ffadc596 | ||
|
|
4da5c1242c | ||
|
|
e0b8deae8b | ||
|
|
d3df8bdc7e | ||
|
|
b53b1cdd3c | ||
|
|
0054ab5afc |
@@ -105,4 +105,3 @@ Temporary Items
|
||||
/api/vendor/
|
||||
/api/tests/_*
|
||||
/api/tests/*.suite.yml
|
||||
/api/html/swagger/
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
## Datenbank-Einstellungen
|
||||
DATABASES_HOST=psql.torsten-hettstedt.net
|
||||
DATABASES_NAME=torsten
|
||||
DATABASES_USER=web_user
|
||||
DATABASES_PASS=V6ZGhtdXEbxH8oWD
|
||||
@@ -1,5 +1,9 @@
|
||||
FROM php:8.0-apache
|
||||
|
||||
#RUN apt update && apt install -y postgresql postgresql-client
|
||||
RUN apt update && apt install -y libpq-dev
|
||||
RUN docker-php-ext-install -j$(nproc) pdo pdo_pgsql
|
||||
|
||||
WORKDIR /var/www
|
||||
|
||||
RUN a2enmod rewrite
|
||||
+6
-2
@@ -19,6 +19,7 @@
|
||||
"slim/slim": "^4.7.1",
|
||||
"vlucas/phpdotenv": "^4.2",
|
||||
"slim/psr7": "^1.3",
|
||||
"php-di/slim-bridge": "^3.1.0",
|
||||
"jetbrains/phpstorm-attributes": "^1.0.0",
|
||||
"myclabs/php-enum": "^1.8.0"
|
||||
},
|
||||
@@ -27,7 +28,8 @@
|
||||
"codeception/codeception": "^4.1.18",
|
||||
"codeception/module-phpbrowser": "^1.0.0",
|
||||
"codeception/module-asserts": "^1.0.0",
|
||||
"codeception/module-db": "^1.1.0"
|
||||
"codeception/module-db": "^1.1.0",
|
||||
"codeception/module-rest": "^1.2.8"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
@@ -36,7 +38,9 @@
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"TorstenHettstedt\\TimekeepingApi\\Tests\\Unit\\": "tests/unit"
|
||||
"TorstenHettstedt\\TimekeepingApi\\Tests\\Unit\\": "tests/unit",
|
||||
"TorstenHettstedt\\TimekeepingApi\\Tests\\Api\\": "tests/api",
|
||||
"Helper\\": "tests/_support/Helper"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
# absolute physical path to the directory that contains this htaccess file.
|
||||
# RewriteBase /
|
||||
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [QSA,L]
|
||||
</IfModule>
|
||||
+34
-2
@@ -1,12 +1,44 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
|
||||
$dotenv->load();
|
||||
|
||||
use DI\Container;
|
||||
use Slim\Factory\AppFactory;
|
||||
use Slim\Routing\RouteCollectorProxy;
|
||||
use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursController;
|
||||
use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursViewController;
|
||||
use TorstenHettstedt\TimekeepingApi\Middleware\ErrorHandler;
|
||||
use TorstenHettstedt\TimekeepingApi\Middleware\JsonBodyParserMiddleware;
|
||||
|
||||
$container = new Container();
|
||||
|
||||
$container->set('databases', function () {
|
||||
$dsn = "pgsql:host=${_ENV['DATABASES_HOST']};port=5432;dbname=${_ENV['DATABASES_NAME']}";
|
||||
return new PDO($dsn, $_ENV['DATABASES_USER'], $_ENV['DATABASES_PASS']);
|
||||
});
|
||||
|
||||
AppFactory::setContainer($container);
|
||||
$app = AppFactory::create();
|
||||
$app->add(new JsonBodyParserMiddleware());
|
||||
|
||||
$app->get('/', 'TorstenHettstedt\TimekeepingApi\Controller\HelloWorldController:read');
|
||||
$app->group('/views/working-hours', function (RouteCollectorProxy $group) {
|
||||
$group->get('/weekly', WorkingHoursViewController::class . ':browseWeekly');
|
||||
$group->get('/monthly', WorkingHoursViewController::class . ':browseMonthly');
|
||||
$group->get('/yearly', WorkingHoursViewController::class . ':browseYearly');
|
||||
});
|
||||
|
||||
$app->addErrorMiddleware(true, true, true);
|
||||
$app->group('/working-hours', function (RouteCollectorProxy $group) {
|
||||
$group->get('', WorkingHoursController::class . ':browse');
|
||||
$group->post('', WorkingHoursController::class . ':creat');
|
||||
$group->group('/{id:\d\d\d\d-\d\d-\d\d}', function (RouteCollectorProxy $group) {
|
||||
$group->get('', WorkingHoursController::class . ':read');
|
||||
$group->put('', WorkingHoursController::class . ':update');
|
||||
});
|
||||
});
|
||||
|
||||
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
|
||||
$errorMiddleware->setDefaultErrorHandler(new ErrorHandler($app));
|
||||
|
||||
$app->run();
|
||||
@@ -82,6 +82,7 @@
|
||||
"tags": [
|
||||
"Eintragungen Arbeitszeiten"
|
||||
],
|
||||
"description": "Angabe des Datum im Request-Body werden ignoriert",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/date"
|
||||
@@ -167,14 +168,17 @@
|
||||
"description": "Eintrag der Arbeitszeit für einen Tag",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": {
|
||||
"workingDay": {
|
||||
"description": "Datum eines Arbeitstages",
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
"format": "date",
|
||||
"required": false
|
||||
},
|
||||
"workingHours": {
|
||||
"workingTime": {
|
||||
"description": "Arbeitszeit des Tages",
|
||||
"type": "integer"
|
||||
"type": "string",
|
||||
"pattern": "[0-5]\\d:[0-5]\\d:[0-5]\\d",
|
||||
"example": "08:01:00"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 665 B |
Binary file not shown.
|
After Width: | Height: | Size: 628 B |
@@ -0,0 +1,60 @@
|
||||
<!-- HTML for static distribution bundle build -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Swagger UI</title>
|
||||
<link rel="stylesheet" type="text/css" href="swagger-ui.css" >
|
||||
<link rel="icon" type="image/png" href="favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="favicon-16x16.png" sizes="16x16" />
|
||||
<style>
|
||||
html
|
||||
{
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after
|
||||
{
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body
|
||||
{
|
||||
margin:0;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<script src="swagger-ui-bundle.js"> </script>
|
||||
<script src="swagger-ui-standalone-preset.js"> </script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
// Begin Swagger UI call region
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "../openapi.json",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
})
|
||||
// End Swagger UI call region
|
||||
|
||||
window.ui = ui
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,68 @@
|
||||
<!doctype html>
|
||||
<html lang="en-US">
|
||||
<title>Swagger UI: OAuth2 Redirect</title>
|
||||
<body onload="run()">
|
||||
</body>
|
||||
</html>
|
||||
<script>
|
||||
'use strict';
|
||||
function run () {
|
||||
var oauth2 = window.opener.swaggerUIRedirectOauth2;
|
||||
var sentState = oauth2.state;
|
||||
var redirectUrl = oauth2.redirectUrl;
|
||||
var isValid, qp, arr;
|
||||
|
||||
if (/code|token|error/.test(window.location.hash)) {
|
||||
qp = window.location.hash.substring(1);
|
||||
} else {
|
||||
qp = location.search.substring(1);
|
||||
}
|
||||
|
||||
arr = qp.split("&")
|
||||
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';})
|
||||
qp = qp ? JSON.parse('{' + arr.join() + '}',
|
||||
function (key, value) {
|
||||
return key === "" ? value : decodeURIComponent(value)
|
||||
}
|
||||
) : {}
|
||||
|
||||
isValid = qp.state === sentState
|
||||
|
||||
if ((
|
||||
oauth2.auth.schema.get("flow") === "accessCode"||
|
||||
oauth2.auth.schema.get("flow") === "authorizationCode"
|
||||
) && !oauth2.auth.code) {
|
||||
if (!isValid) {
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "warning",
|
||||
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
|
||||
});
|
||||
}
|
||||
|
||||
if (qp.code) {
|
||||
delete oauth2.state;
|
||||
oauth2.auth.code = qp.code;
|
||||
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
|
||||
} else {
|
||||
let oauthErrorMsg
|
||||
if (qp.error) {
|
||||
oauthErrorMsg = "["+qp.error+"]: " +
|
||||
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
|
||||
(qp.error_uri ? "More info: "+qp.error_uri : "");
|
||||
}
|
||||
|
||||
oauth2.errCb({
|
||||
authId: oauth2.auth.name,
|
||||
source: "auth",
|
||||
level: "error",
|
||||
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
|
||||
});
|
||||
}
|
||||
} else {
|
||||
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
|
||||
}
|
||||
window.close();
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,30 +0,0 @@
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
|
||||
namespace TorstenHettstedt\TimekeepingApi\Controller;
|
||||
|
||||
use Slim\Psr7\Response;
|
||||
use Slim\Psr7\Request;
|
||||
|
||||
class HelloWorldController
|
||||
{
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param mixed[] $args
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @noinspection PhpUnusedParameterInspection
|
||||
*/
|
||||
public function read(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
$payload = json_encode('Hallo World');
|
||||
|
||||
$response->getBody()->write($payload);
|
||||
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withStatus(200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php /** @noinspection PhpMissingFieldTypeInspection */
|
||||
|
||||
namespace TorstenHettstedt\TimekeepingApi\Controller;
|
||||
|
||||
use Slim\Exception\HttpSpecializedException;
|
||||
|
||||
class HttpConflictRequestException extends HttpSpecializedException
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $code = 409;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $message = 'Conflict.';
|
||||
|
||||
protected $title = '409 Conflict';
|
||||
protected $description = 'The 409 (Conflict) status code indicates that the request could not be completed due to a conflict with the current state of the target resource.';
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace TorstenHettstedt\TimekeepingApi\Controller;
|
||||
|
||||
use Exception;
|
||||
|
||||
class NotDatabasesException extends Exception
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace TorstenHettstedt\TimekeepingApi\Controller;
|
||||
|
||||
|
||||
use DateInterval;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use PDO;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Slim\Exception\HttpBadRequestException;
|
||||
use Slim\Exception\HttpInternalServerErrorException;
|
||||
use Slim\Exception\HttpNotFoundException;
|
||||
use Slim\Psr7\Request;
|
||||
use Slim\Psr7\Response;
|
||||
use TorstenHettstedt\TimekeepingApi\Models\WorkingHours;
|
||||
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryModelAlreadyExists;
|
||||
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException;
|
||||
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursRepository;
|
||||
|
||||
class WorkingHoursController
|
||||
{
|
||||
|
||||
protected PDO $databases;
|
||||
protected ContainerInterface $container;
|
||||
|
||||
/**
|
||||
* WorkingHoursController constructor.
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
*
|
||||
* @throws NotDatabasesException
|
||||
*/
|
||||
public function __construct(ContainerInterface $container)
|
||||
{
|
||||
$this->container = $container;
|
||||
if ($this->container->has('databases') === false) {
|
||||
throw new NotDatabasesException('Datenbank ist nicht Vorhanden');
|
||||
}
|
||||
$this->databases = $this->container->get('databases');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param mixed[] $args
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws HttpBadRequestException
|
||||
* @throws HttpNotFoundException
|
||||
*/
|
||||
public function update(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
$body = $request->getParsedBody();
|
||||
$repository = new WorkingHoursRepository($this->databases);
|
||||
$model = $this->buildModel($request, $args['id'], $body['workingTime']);
|
||||
try {
|
||||
$repository->update($model);
|
||||
} catch (RepositoryRecordNotFoundException $exception) {
|
||||
throw new HttpNotFoundException($request, 'Der Eintrag ist nicht vorhanden.', $exception);
|
||||
}
|
||||
return $this->printResponse($response, $model, StatusCodeInterface::STATUS_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param mixed[] $args
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws Exception
|
||||
* @noinspection PhpUnusedParameterInspection
|
||||
*/
|
||||
public function browse(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
$repository = new WorkingHoursRepository($this->databases);
|
||||
return $this->printResponse($response, $repository->findAll(), StatusCodeInterface::STATUS_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param mixed[] $args
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws HttpNotFoundException
|
||||
* @throws HttpInternalServerErrorException
|
||||
*/
|
||||
public function read(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
$repository = new WorkingHoursRepository($this->databases);
|
||||
try {
|
||||
return $this->printResponse($response, $repository->findByKey($args['id']), StatusCodeInterface::STATUS_OK);
|
||||
} catch (RepositoryRecordNotFoundException $exception) {
|
||||
throw new HttpNotFoundException($request, 'Der Eintrag ist nicht vorhanden.', $exception);
|
||||
} catch (Exception $exception) {
|
||||
throw new HttpInternalServerErrorException($request, 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param mixed[] $args
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws HttpBadRequestException
|
||||
* @throws HttpConflictRequestException
|
||||
* @throws HttpInternalServerErrorException
|
||||
* @noinspection PhpUnusedParameterInspection
|
||||
*/
|
||||
public function creat(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
$body = $request->getParsedBody();
|
||||
$repository = new WorkingHoursRepository($this->databases);
|
||||
$model = $this->buildModel($request, $body['workingDay'], $body['workingTime']);
|
||||
try {
|
||||
$repository->insert($model);
|
||||
} catch (RepositoryModelAlreadyExists $exception) {
|
||||
throw new HttpConflictRequestException($request, 'Der Eintrag ist schon vorhanden.', $exception);
|
||||
} catch (Exception $exception) {
|
||||
throw new HttpInternalServerErrorException($request, 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception);
|
||||
}
|
||||
return $this->printResponse($response, $model, StatusCodeInterface::STATUS_CREATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Response $response
|
||||
* @param mixed $data
|
||||
* @param int $status_code
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
protected function printResponse(Response $response, mixed $data, int $status_code): Response
|
||||
{
|
||||
$payload = json_encode($data);
|
||||
|
||||
$response->getBody()->write($payload);
|
||||
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withStatus($status_code);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param string $date
|
||||
* @param string $time
|
||||
*
|
||||
* @return WorkingHours
|
||||
* @throws HttpBadRequestException
|
||||
*/
|
||||
protected function buildModel(Request $request, string $date, string $time): WorkingHours
|
||||
{
|
||||
try {
|
||||
$workingDay = new DateTime($date);
|
||||
} catch (Exception $exception) {
|
||||
throw new HttpBadRequestException($request, 'Der Wert für das Datum ist falsch', $exception);
|
||||
}
|
||||
try {
|
||||
$workingTime = new DateInterval('P0000-00-00T' . $time);
|
||||
} catch (Exception $exception) {
|
||||
throw new HttpBadRequestException($request, 'Der Wert für die Zeit ist falsch', $exception);
|
||||
}
|
||||
return (new WorkingHours())->setWorkingDay($workingDay)->setWorkingTime($workingTime);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace TorstenHettstedt\TimekeepingApi\Controller;
|
||||
|
||||
|
||||
use Exception;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Slim\Psr7\Request;
|
||||
use Slim\Psr7\Response;
|
||||
use TorstenHettstedt\TimekeepingApi\Models\WorkingHoursView;
|
||||
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursMonthlyViewRepository;
|
||||
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursWeeklyViewRepository;
|
||||
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursYearlyViewRepository;
|
||||
|
||||
class WorkingHoursViewController
|
||||
{
|
||||
/**
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
protected ContainerInterface $container;
|
||||
|
||||
public function __construct(ContainerInterface $container) {
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param mixed[] $args
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @noinspection PhpUnusedParameterInspection
|
||||
* @throws NotDatabasesException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function browseWeekly(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
if ($this->container->has('databases') === false) {
|
||||
throw new NotDatabasesException('Datenbank ist nicht Vorhanden');
|
||||
}
|
||||
$repository = new WorkingHoursWeeklyViewRepository($this->container->get('databases'));
|
||||
return $this->printResponse($response, $repository->findAll());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param mixed[] $args
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @noinspection PhpUnusedParameterInspection
|
||||
* @throws Exception
|
||||
*/
|
||||
public function browseMonthly(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
if ($this->container->has('databases') === false) {
|
||||
throw new NotDatabasesException('Datenbank ist nicht Vorhanden');
|
||||
}
|
||||
$repository = new WorkingHoursMonthlyViewRepository($this->container->get('databases'));
|
||||
return $this->printResponse($response, $repository->findAll());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param mixed[] $args
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @noinspection PhpUnusedParameterInspection
|
||||
* @throws Exception
|
||||
*/
|
||||
public function browseYearly(Request $request, Response $response, array $args): Response
|
||||
{
|
||||
if ($this->container->has('databases') === false) {
|
||||
throw new NotDatabasesException('Datenbank ist nicht Vorhanden');
|
||||
}
|
||||
$repository = new WorkingHoursYearlyViewRepository($this->container->get('databases'));
|
||||
return $this->printResponse($response, $repository->findAll());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Response $response
|
||||
* @param WorkingHoursView[] $data
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
protected function printResponse( Response $response, array $data): Response
|
||||
{
|
||||
$payload = json_encode($data);
|
||||
|
||||
$response->getBody()->write($payload);
|
||||
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withStatus(200);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php /** @noinspection PhpUndefinedClassInspection */
|
||||
|
||||
|
||||
namespace TorstenHettstedt\TimekeepingApi\Middleware;
|
||||
|
||||
|
||||
use JetBrains\PhpStorm\ArrayShape;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Slim\App;
|
||||
use Slim\Exception\HttpException;
|
||||
use Throwable;
|
||||
|
||||
class ErrorHandler
|
||||
{
|
||||
protected App $app;
|
||||
|
||||
/**
|
||||
* ErrorHandler constructor.
|
||||
*
|
||||
* @param App $app
|
||||
*/
|
||||
public function __construct(App $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Throwable $exception
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
#[ArrayShape([
|
||||
'timestamp' => "false|string",
|
||||
'status' => "int",
|
||||
"error" => "string",
|
||||
'message' => "string",
|
||||
'path' => "string"
|
||||
])] protected function buildJsonArray(Throwable $exception): array
|
||||
{
|
||||
$payload = [
|
||||
'timestamp' => date('Y-m-d\TH:m:sP'),
|
||||
'status' => $exception->getCode(),
|
||||
"error" => '',
|
||||
'message' => $exception->getMessage(),
|
||||
'path' => $exception->getFile()
|
||||
];
|
||||
|
||||
if ($exception instanceof HttpException) {
|
||||
$payload['error'] = $exception->getTitle();
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @param Throwable $exception
|
||||
* @param bool $displayErrorDetails
|
||||
* @param bool $logErrors
|
||||
* @param bool $logErrorDetails
|
||||
* @param LoggerInterface|null $logger
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function __invoke(
|
||||
ServerRequestInterface $request,
|
||||
Throwable $exception,
|
||||
bool $displayErrorDetails,
|
||||
bool $logErrors,
|
||||
bool $logErrorDetails,
|
||||
?LoggerInterface $logger = null
|
||||
): ResponseInterface
|
||||
{
|
||||
if ($logger !== null && $logErrors === true) {
|
||||
$logger->error($exception->getMessage());
|
||||
}
|
||||
|
||||
$payload = $this->buildJsonArray($exception);
|
||||
|
||||
$response = $this->app->getResponseFactory()->createResponse($exception->getCode());
|
||||
$response->getBody()->write(
|
||||
json_encode($payload, JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace TorstenHettstedt\TimekeepingApi\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||
|
||||
class JsonBodyParserMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, RequestHandler $handler): Response
|
||||
{
|
||||
$contentType = $request->getHeaderLine('Content-Type');
|
||||
|
||||
if (strstr($contentType, 'application/json')) {
|
||||
$contents = json_decode(file_get_contents('php://input'), true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$request = $request->withParsedBody($contents);
|
||||
}
|
||||
}
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
namespace Helper;
|
||||
|
||||
// here you can define custom actions
|
||||
// all public methods declared in helper class will be available in $I
|
||||
|
||||
use Codeception\Module;
|
||||
|
||||
class Api extends Module
|
||||
{
|
||||
|
||||
const FORMAT_TIME = 'string:regex(/\d+:[0-5]\d:[0-5]\d/)';
|
||||
const FORMAT_DATE = 'string:regex(/\d{4}-\d{2}-\d{2}/)';
|
||||
|
||||
const WORKING_HOURS_JSON_FORMAT = [
|
||||
'workingDay' => self::FORMAT_DATE,
|
||||
'workingTime' => self::FORMAT_TIME,
|
||||
];
|
||||
|
||||
const WORKING_HOURS_VIEW_JSON_FORMAT = [
|
||||
"period" => "string",
|
||||
"periodDesignation" => "string",
|
||||
"totalHours" => self::FORMAT_TIME,
|
||||
"workingDays" => 'integer:>0',
|
||||
"overtime" => self::FORMAT_TIME,
|
||||
];
|
||||
|
||||
const ERROR_JSON_FORMAT = [
|
||||
"timestamp" => "string:date",
|
||||
"status" => "integer",
|
||||
"error" => "string",
|
||||
"message" => "string",
|
||||
"path" => "string",
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
actor: ApiTester
|
||||
modules:
|
||||
enabled:
|
||||
- \Helper\Api
|
||||
- REST:
|
||||
url: http://localhost:8091/
|
||||
depends: PhpBrowser
|
||||
part: Json
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\Views;
|
||||
|
||||
use ApiTester;
|
||||
use Codeception\Util\HttpCode;
|
||||
use Helper\Api;
|
||||
|
||||
class BrowseWorkingHoursMonthlyCest
|
||||
{
|
||||
/**
|
||||
* @param ApiTester $I
|
||||
*/
|
||||
public function browseWorkingHoursView(ApiTester $I): void
|
||||
{
|
||||
$I->sendGet('/views/working-hours/monthly');
|
||||
$I->seeResponseCodeIs(HttpCode::OK);
|
||||
$I->seeResponseIsJson();
|
||||
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_VIEW_JSON_FORMAT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\Views;
|
||||
|
||||
use ApiTester;
|
||||
use Codeception\Util\HttpCode;
|
||||
use Helper\Api;
|
||||
|
||||
class BrowseWorkingHoursWeeklyCest
|
||||
{
|
||||
/**
|
||||
* @param ApiTester $I
|
||||
*/
|
||||
public function browseWorkingHoursView(ApiTester $I): void
|
||||
{
|
||||
$I->sendGet('/views/working-hours/weekly');
|
||||
$I->seeResponseCodeIs(HttpCode::OK);
|
||||
$I->seeResponseIsJson();
|
||||
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_VIEW_JSON_FORMAT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\Views;
|
||||
|
||||
use ApiTester;
|
||||
use Codeception\Util\HttpCode;
|
||||
use Helper\Api;
|
||||
|
||||
class BrowseWorkingHoursYearlyCest
|
||||
{
|
||||
/**
|
||||
* @param ApiTester $I
|
||||
*/
|
||||
public function browseWorkingHoursView(ApiTester $I): void
|
||||
{
|
||||
$I->sendGet('/views/working-hours/yearly');
|
||||
$I->seeResponseCodeIs(HttpCode::OK);
|
||||
$I->seeResponseIsJson();
|
||||
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_VIEW_JSON_FORMAT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\WorkingHours;
|
||||
|
||||
use ApiTester;
|
||||
use Codeception\Util\HttpCode;
|
||||
use Helper\Api;
|
||||
|
||||
class BrowseWorkingHoursCest
|
||||
{
|
||||
/**
|
||||
* @param ApiTester $I
|
||||
*/
|
||||
public function browseWorkingHours(ApiTester $I): void
|
||||
{
|
||||
$I->sendGet('/working-hours');
|
||||
$I->seeResponseCodeIs(HttpCode::OK);
|
||||
$I->seeResponseIsJson();
|
||||
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_JSON_FORMAT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\WorkingHours;
|
||||
|
||||
use ApiTester;
|
||||
use Codeception\Util\HttpCode;
|
||||
use Helper\Api;
|
||||
|
||||
class CreateWorkingHoursCest
|
||||
{
|
||||
public function createWorkingHoursWithNewValidRecord(ApiTester $I): void
|
||||
{
|
||||
$I->haveHttpHeader('accept', 'application/json');
|
||||
$I->haveHttpHeader('content-type', 'application/json');
|
||||
$I->sendPost('/working-hours', [
|
||||
'workingDay' => '2020-04-01',
|
||||
'workingTime' => '08:00:00',
|
||||
]);
|
||||
$I->seeResponseCodeIs(HttpCode::CREATED);
|
||||
$I->seeResponseIsJson();
|
||||
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_JSON_FORMAT);
|
||||
}
|
||||
|
||||
// tests
|
||||
public function createWorkingHoursWithNewInvalidRecord(ApiTester $I): void
|
||||
{
|
||||
$I->haveHttpHeader('accept', 'application/json');
|
||||
$I->haveHttpHeader('content-type', 'application/json');
|
||||
$I->sendPost('/working-hours', [
|
||||
'workingDay' => '2020-04-01',
|
||||
'workingTime' => 8.0,
|
||||
]);
|
||||
$I->seeResponseCodeIs(HttpCode::BAD_REQUEST);
|
||||
$I->seeResponseIsJson();
|
||||
$I->seeResponseMatchesJsonType(Api::ERROR_JSON_FORMAT);
|
||||
}
|
||||
|
||||
// tests
|
||||
public function createWorkingHoursWithExistingValidRecord(ApiTester $I): void
|
||||
{
|
||||
$I->haveHttpHeader('accept', 'application/json');
|
||||
$I->haveHttpHeader('content-type', 'application/json');
|
||||
$I->sendPost('/working-hours', [
|
||||
'workingDay' => '2020-01-15',
|
||||
'workingTime' => '08:00:00',
|
||||
]);
|
||||
$I->seeResponseCodeIs(HttpCode::CONFLICT);
|
||||
$I->seeResponseIsJson();
|
||||
$I->seeResponseMatchesJsonType(Api::ERROR_JSON_FORMAT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\WorkingHours;
|
||||
|
||||
use ApiTester;
|
||||
use Codeception\Util\HttpCode;
|
||||
use Helper\Api;
|
||||
|
||||
class ReadWorkingHoursCest
|
||||
{
|
||||
public function readExistingWorkingHours(ApiTester $I): void
|
||||
{
|
||||
$I->sendGet('/working-hours/2020-01-15');
|
||||
$I->seeResponseCodeIs(HttpCode::OK);
|
||||
$I->seeResponseIsJson();
|
||||
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_JSON_FORMAT);
|
||||
}
|
||||
|
||||
public function readNotExistingWorkingHours(ApiTester $I): void
|
||||
{
|
||||
$I->sendGet('/working-hours/2020-04-15');
|
||||
$I->seeResponseCodeIs(HttpCode::NOT_FOUND);
|
||||
$I->seeResponseIsJson();
|
||||
$I->seeResponseMatchesJsonType(Api::ERROR_JSON_FORMAT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php /** @noinspection PhpUnused */
|
||||
|
||||
namespace TorstenHettstedt\TimekeepingApi\Tests\Api\WorkingHours;
|
||||
|
||||
use ApiTester;
|
||||
use Codeception\Util\HttpCode;
|
||||
use Helper\Api;
|
||||
|
||||
class UpdateWorkingHoursCest
|
||||
{
|
||||
public function updateWorkingHoursWithExistingValidRecord(ApiTester $I): void
|
||||
{
|
||||
$I->haveHttpHeader('accept', 'application/json');
|
||||
$I->haveHttpHeader('content-type', 'application/json');
|
||||
$I->sendPut('/working-hours/2020-01-15', [
|
||||
'workingDay' => '2020-01-15',
|
||||
'workingTime' => '10:00:00',
|
||||
]);
|
||||
$I->seeResponseCodeIs(HttpCode::OK);
|
||||
$I->seeResponseIsJson();
|
||||
$I->seeResponseMatchesJsonType(Api::WORKING_HOURS_JSON_FORMAT);
|
||||
}
|
||||
|
||||
public function updateWorkingHoursWithNewValidRecord(ApiTester $I): void
|
||||
{
|
||||
$I->haveHttpHeader('accept', 'application/json');
|
||||
$I->haveHttpHeader('content-type', 'application/json');
|
||||
$I->sendPut('/working-hours/2020-04-15', [
|
||||
'workingDay' => '2020-04-15',
|
||||
'workingTime' => '10:00:00',
|
||||
]);
|
||||
$I->seeResponseCodeIs(HttpCode::NOT_FOUND);
|
||||
$I->seeResponseIsJson();
|
||||
$I->seeResponseMatchesJsonType(Api::ERROR_JSON_FORMAT);
|
||||
}
|
||||
|
||||
public function updateWorkingHoursWithNewInvalidRecord(ApiTester $I): void
|
||||
{
|
||||
$I->haveHttpHeader('accept', 'application/json');
|
||||
$I->haveHttpHeader('content-type', 'application/json');
|
||||
$I->sendPut('/working-hours/2020-01-15', [
|
||||
'workingDay' => '2020-01-15',
|
||||
'workingTime' => 8.0,
|
||||
]);
|
||||
$I->seeResponseCodeIs(HttpCode::BAD_REQUEST);
|
||||
$I->seeResponseIsJson();
|
||||
$I->seeResponseMatchesJsonType(Api::ERROR_JSON_FORMAT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Controller;
|
||||
|
||||
use Codeception\Test\Unit;
|
||||
use Exception;
|
||||
use PDO;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Slim\Exception\HttpBadRequestException;
|
||||
use Slim\Exception\HttpInternalServerErrorException;
|
||||
use Slim\Exception\HttpNotFoundException;
|
||||
use Slim\Psr7\Request;
|
||||
use Slim\Psr7\Response;
|
||||
use TorstenHettstedt\TimekeepingApi\Controller\HttpConflictRequestException;
|
||||
use TorstenHettstedt\TimekeepingApi\Controller\NotDatabasesException;
|
||||
use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursController;
|
||||
|
||||
class WorkingHoursControllerTest extends Unit
|
||||
{
|
||||
|
||||
const EXISTING_DATE = '2020-01-28';
|
||||
const EXISTING_INTERVAL = '07:20:00';
|
||||
const NEW_DATE = '2020-04-01';
|
||||
const NEW_INTERVAL = '07:59:00';
|
||||
|
||||
protected ContainerInterface $container;
|
||||
protected Request $request;
|
||||
protected Response $response;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function _before(): void
|
||||
{
|
||||
parent::_before();
|
||||
/** @noinspection SpellCheckingInspection */
|
||||
$this->container = $this->makeEmpty(ContainerInterface::class, [
|
||||
'has' => true,
|
||||
'get' => new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass')
|
||||
]);
|
||||
$this->request = $this->makeEmpty(Request::class, [
|
||||
'getParsedBody' => [
|
||||
'workingDay' => self::EXISTING_DATE,
|
||||
'workingTime' => self::EXISTING_INTERVAL,
|
||||
]
|
||||
]);
|
||||
$this->response = $this->makeEmpty(Response::class, [
|
||||
'getBody' => $this->makeEmpty(StreamInterface::class, [
|
||||
'write' => function (mixed $data) {
|
||||
$this->assertIsString($data);
|
||||
$this->assertJson($data);
|
||||
},
|
||||
]),
|
||||
'withHeader' => $this->makeEmpty(Response::class, [
|
||||
'withStatus' => $this->makeEmpty(Response::class),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotDatabasesException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function testConstructWithNonDatabase(): void
|
||||
{
|
||||
$this->container = $this->makeEmpty(ContainerInterface::class, [
|
||||
'has' => false
|
||||
]);
|
||||
$this->expectException(NotDatabasesException::class);
|
||||
new WorkingHoursController($this->container);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotDatabasesException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function testBrowse(): void
|
||||
{
|
||||
$controller = new WorkingHoursController($this->container);
|
||||
$response = $controller->browse($this->request, $this->response, []);
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotDatabasesException
|
||||
* @throws HttpBadRequestException
|
||||
* @throws HttpNotFoundException
|
||||
*/
|
||||
public function testUpdateExistRecord(): void
|
||||
{
|
||||
$controller = new WorkingHoursController($this->container);
|
||||
$response = $controller->update($this->request, $this->response, [
|
||||
'id' => self::EXISTING_DATE
|
||||
]);
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NotDatabasesException
|
||||
* @throws HttpBadRequestException
|
||||
* @throws HttpNotFoundException
|
||||
*/
|
||||
public function testUpdateNotExistRecord(): void
|
||||
{
|
||||
$controller = new WorkingHoursController($this->container);
|
||||
$this->expectException(HttpNotFoundException::class);
|
||||
$controller->update($this->request, $this->response, [
|
||||
'id' => self::NEW_DATE
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[][]
|
||||
*/
|
||||
public function invalidDataProvider(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
self::NEW_DATE,
|
||||
'07:59'
|
||||
],
|
||||
[
|
||||
'2020-13-33',
|
||||
self::NEW_INTERVAL
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $date
|
||||
* @param string $time
|
||||
*
|
||||
* @dataProvider invalidDataProvider
|
||||
*
|
||||
* @throws HttpBadRequestException
|
||||
* @throws HttpConflictRequestException
|
||||
* @throws HttpInternalServerErrorException
|
||||
* @throws NotDatabasesException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function testCreatInvalidData(string $date, string $time): void
|
||||
{
|
||||
$this->request = $this->makeEmpty(Request::class, [
|
||||
'getParsedBody' => [
|
||||
'workingDay' => $date,
|
||||
'workingTime' => $time,
|
||||
]
|
||||
]);
|
||||
$controller = new WorkingHoursController($this->container);
|
||||
$this->expectException(HttpBadRequestException::class);
|
||||
$controller->creat($this->request, $this->response, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws HttpBadRequestException
|
||||
* @throws NotDatabasesException
|
||||
* @throws HttpInternalServerErrorException
|
||||
* @throws HttpConflictRequestException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function testCreatNewRecord(): void
|
||||
{
|
||||
$this->request = $this->makeEmpty(Request::class, [
|
||||
'getParsedBody' => [
|
||||
'workingDay' => self::NEW_DATE,
|
||||
'workingTime' => self::NEW_INTERVAL,
|
||||
]
|
||||
]);
|
||||
$controller = new WorkingHoursController($this->container);
|
||||
$response = $controller->creat($this->request, $this->response, []);
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws HttpBadRequestException
|
||||
* @throws HttpConflictRequestException
|
||||
* @throws HttpInternalServerErrorException
|
||||
* @throws NotDatabasesException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function testCreatExistRecord(): void
|
||||
{
|
||||
$this->request = $this->makeEmpty(Request::class, [
|
||||
'getParsedBody' => [
|
||||
'workingDay' => self::EXISTING_DATE,
|
||||
'workingTime' => self::EXISTING_INTERVAL,
|
||||
]
|
||||
]);
|
||||
$controller = new WorkingHoursController($this->container);
|
||||
$this->expectException(HttpConflictRequestException::class);
|
||||
$controller->creat($this->request, $this->response, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws HttpInternalServerErrorException
|
||||
* @throws HttpNotFoundException
|
||||
* @throws NotDatabasesException
|
||||
*/
|
||||
public function testReadExistRecord(): void
|
||||
{
|
||||
$controller = new WorkingHoursController($this->container);
|
||||
$response = $controller->read($this->request, $this->response, [
|
||||
'id' => self::EXISTING_DATE
|
||||
]);
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws HttpInternalServerErrorException
|
||||
* @throws HttpNotFoundException
|
||||
* @throws NotDatabasesException
|
||||
*/
|
||||
public function testReadNotExistRecord(): void
|
||||
{
|
||||
$controller = new WorkingHoursController($this->container);
|
||||
$this->expectException(HttpNotFoundException::class);
|
||||
$controller->read($this->request, $this->response, [
|
||||
'id' => self::NEW_DATE
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Controller;
|
||||
|
||||
use Codeception\Test\Unit;
|
||||
use Exception;
|
||||
use PDO;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Slim\Psr7\Request;
|
||||
use Slim\Psr7\Response;
|
||||
use TorstenHettstedt\TimekeepingApi\Controller\NotDatabasesException;
|
||||
use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursViewController;
|
||||
|
||||
class WorkingHoursViewControllerTest extends Unit
|
||||
{
|
||||
protected ContainerInterface $container;
|
||||
protected Request $request;
|
||||
protected Response $response;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function _before(): void
|
||||
{
|
||||
parent::_before();
|
||||
/** @noinspection SpellCheckingInspection */
|
||||
$this->container = $this->makeEmpty(ContainerInterface::class, [
|
||||
'has' => true,
|
||||
'get' => new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass')
|
||||
]);
|
||||
$this->request = $this->makeEmpty(Request::class, []);
|
||||
$this->response = $this->makeEmpty(Response::class, [
|
||||
'getBody' => $this->makeEmpty(StreamInterface::class, [
|
||||
'write' => function (mixed $data) {
|
||||
$this->assertIsString($data);
|
||||
$this->assertJson($data);
|
||||
},
|
||||
]),
|
||||
'withHeader' => $this->makeEmpty(Response::class, [
|
||||
'withStatus' => $this->makeEmpty(Response::class),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function testBrowseMonthlyWithNonDatabase(): void
|
||||
{
|
||||
$this->container = $this->makeEmpty(ContainerInterface::class, [
|
||||
'has' => false
|
||||
]);
|
||||
$controller = new WorkingHoursViewController($this->container);
|
||||
$this->expectException(NotDatabasesException::class);
|
||||
$controller->browseMonthly($this->request, $this->response, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function testBrowseMonthly(): void
|
||||
{
|
||||
$controller = new WorkingHoursViewController($this->container);
|
||||
$response = $controller->browseMonthly($this->request, $this->response, []);
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function testBrowseYearlyWithNonDatabase(): void
|
||||
{
|
||||
$this->container = $this->makeEmpty(ContainerInterface::class, [
|
||||
'has' => false
|
||||
]);
|
||||
$controller = new WorkingHoursViewController($this->container);
|
||||
$this->expectException(NotDatabasesException::class);
|
||||
$controller->browseYearly($this->request, $this->response, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function testBrowseYearly(): void
|
||||
{
|
||||
$controller = new WorkingHoursViewController($this->container);
|
||||
$response = $controller->browseYearly($this->request, $this->response, []);
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function testBrowseWeeklyWithNonDatabase(): void
|
||||
{
|
||||
$this->container = $this->makeEmpty(ContainerInterface::class, [
|
||||
'has' => false
|
||||
]);
|
||||
$controller = new WorkingHoursViewController($this->container);
|
||||
$this->expectException(NotDatabasesException::class);
|
||||
$controller->browseWeekly($this->request, $this->response, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function testBrowseWeekly(): void
|
||||
{
|
||||
$controller = new WorkingHoursViewController($this->container);
|
||||
$response = $controller->browseWeekly($this->request, $this->response, []);
|
||||
$this->assertInstanceOf(Response::class, $response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Middleware;
|
||||
|
||||
use Codeception\Stub\Expected;
|
||||
use Codeception\Test\Unit;
|
||||
use Exception;
|
||||
use Psr\Http\Message\ResponseFactoryInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Slim\App;
|
||||
use Slim\Exception\HttpException;
|
||||
use TorstenHettstedt\TimekeepingApi\Middleware\ErrorHandler;
|
||||
|
||||
class ErrorHandlerTest extends Unit
|
||||
{
|
||||
|
||||
protected App $app;
|
||||
protected ServerRequestInterface $request;
|
||||
protected Exception $exception;
|
||||
protected LoggerInterface $logger;
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function _before(): void
|
||||
{
|
||||
parent::_before();
|
||||
$this->app = $this->makeEmpty(App::class, [
|
||||
'getResponseFactory' => $this->makeEmpty(ResponseFactoryInterface::class, [
|
||||
'createResponse' => $this->makeEmpty(ResponseInterface::class, [
|
||||
'getBody' => $this->makeEmpty(StreamInterface::class, [
|
||||
'write' => function (mixed $data) {
|
||||
$this->assertIsString($data);
|
||||
$this->assertJson($data);
|
||||
},
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
$this->request = $this->makeEmpty(ServerRequestInterface::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function testUseWithLogger(): void
|
||||
{
|
||||
$this->exception = $this->make(Exception::class, [
|
||||
'message' => '',
|
||||
'code' => 400,
|
||||
'file' => '/path(to/file',
|
||||
]);
|
||||
$this->logger = $this->makeEmpty(LoggerInterface::class, [
|
||||
'error' => Expected::once(),
|
||||
]);
|
||||
|
||||
$middleWare = new ErrorHandler($this->app);
|
||||
$middleWare($this->request, $this->exception, true, true, true, $this->logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function testUseWithHttpException(): void
|
||||
{
|
||||
$this->exception = $this->make(HttpException::class, [
|
||||
'message' => '',
|
||||
'code' => 400,
|
||||
'file' => '/path(to/file',
|
||||
'getTitle' => Expected::once('The Title'),
|
||||
]);
|
||||
$this->logger = $this->makeEmpty(LoggerInterface::class, [
|
||||
'error' => Expected::once(),
|
||||
]);
|
||||
|
||||
$middleWare = new ErrorHandler($this->app);
|
||||
$middleWare($this->request, $this->exception, true, true, true, $this->logger);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
public function testUseWithGeneralException(): void
|
||||
{
|
||||
$this->exception = $this->make(Exception::class, [
|
||||
'message' => '',
|
||||
'code' => 400,
|
||||
'file' => '/path(to/file',
|
||||
]);
|
||||
$this->logger = $this->makeEmpty(LoggerInterface::class, [
|
||||
'error' => Expected::once(),
|
||||
]);
|
||||
|
||||
$middleWare = new ErrorHandler($this->app);
|
||||
$middleWare($this->request, $this->exception, true, true, true, $this->logger);
|
||||
}
|
||||
}
|
||||
+13
@@ -8,6 +8,19 @@ services:
|
||||
docker: "true"
|
||||
ports:
|
||||
- 8090:80
|
||||
volumes:
|
||||
- ./api:/var/www
|
||||
- logs:/var/www/logs
|
||||
test-api:
|
||||
build: ./api/
|
||||
environment:
|
||||
docker: "true"
|
||||
DATABASES_HOST: psql.torsten-hettstedt.net
|
||||
DATABASES_NAME: testdb
|
||||
DATABASES_USER: bruce
|
||||
DATABASES_PASS: mypass
|
||||
ports:
|
||||
- 8091:80
|
||||
volumes:
|
||||
- ./api:/var/www
|
||||
- logs:/var/www/logs
|
||||
Reference in New Issue
Block a user