6 Commits
7 changed files with 372 additions and 28 deletions
+2 -1
View File
@@ -21,7 +21,8 @@
"slim/psr7": "^1.3", "slim/psr7": "^1.3",
"php-di/slim-bridge": "^3.1.0", "php-di/slim-bridge": "^3.1.0",
"jetbrains/phpstorm-attributes": "^1.0.0", "jetbrains/phpstorm-attributes": "^1.0.0",
"myclabs/php-enum": "^1.8.0" "myclabs/php-enum": "^1.8.0",
"ext-pdo": "*"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^0.12.80", "phpstan/phpstan": "^0.12.80",
@@ -12,6 +12,7 @@ use Slim\Exception\HttpNotFoundException;
use Slim\Psr7\Request; use Slim\Psr7\Request;
use Slim\Psr7\Response; use Slim\Psr7\Response;
use TorstenHettstedt\TimekeepingApi\Models\WorkingHours; use TorstenHettstedt\TimekeepingApi\Models\WorkingHours;
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryBadWhereDataException;
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordAlreadyExistException; use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordAlreadyExistException;
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException; use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException;
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursRepository; use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursRepository;
@@ -50,13 +51,20 @@ class WorkingHoursController extends AbstractController
* @return Response * @return Response
* *
* @throws HttpInternalServerErrorException * @throws HttpInternalServerErrorException
* @throws HttpBadRequestException
* @noinspection PhpUnusedParameterInspection * @noinspection PhpUnusedParameterInspection
*/ */
public function browse(Request $request, Response $response, array $args): Response public function browse(Request $request, Response $response, array $args): Response
{ {
$repository = new WorkingHoursRepository($this->databases); $repository = new WorkingHoursRepository($this->databases);
$queryParams = $request->getQueryParams();
try { try {
return $this->printResponse($response, $repository->findAll(), StatusCodeInterface::STATUS_OK); return $this->printResponse($response, $repository->findFiltered(
$queryParams['start-date'] ?? null,
$queryParams['end-date'] ?? null
), StatusCodeInterface::STATUS_OK);
} catch (RepositoryBadWhereDataException $exception) {
throw new HttpBadRequestException($request, 'Ein Wert für das Datum im Query ist ungültig', $exception);
} catch (Exception $exception) { } catch (Exception $exception) {
throw new HttpInternalServerErrorException($request, 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception); throw new HttpInternalServerErrorException($request, 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception);
} }
@@ -0,0 +1,10 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Repositories;
class RepositoryBadWhereDataException extends RepositoryException
{
}
@@ -10,6 +10,7 @@ use Exception;
use InvalidArgumentException; use InvalidArgumentException;
use PDO; use PDO;
use PDOException; use PDOException;
use PDOStatement;
use TorstenHettstedt\TimekeepingApi\Models\ModelInterface; use TorstenHettstedt\TimekeepingApi\Models\ModelInterface;
use TorstenHettstedt\TimekeepingApi\Models\WorkingHours; use TorstenHettstedt\TimekeepingApi\Models\WorkingHours;
@@ -42,10 +43,33 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri
* @throws PDOException * @throws PDOException
*/ */
public function findAll(): array public function findAll(): array
{
return $this->findFiltered(null, null);
}
/**
*
* Gibt alle vorhandenen Einträge zurück
*
* @param string|null $start
* @param string|null $end
*
* @return WorkingHours[]
* @throws Exception
*/
public function findFiltered(?string $start, ?string $end): array
{ {
$models = []; $models = [];
$stmt = $this->database->prepare('select "Datum" as "workingDay", "Arbeitszeit" as "workingTime" from public."Arbeitszeiten" order by "Datum"'); $stmt = $this->buildFindFilteredStatement($start, $end);
$stmt->execute(); try {
$stmt->execute();
} catch (PDOException $exception) {
if ($exception->getCode() === '22007' || $exception->getCode() === '22008') {
throw new RepositoryBadWhereDataException($exception);
}
throw $exception;
}
while ($row = $stmt->fetch()) { while ($row = $stmt->fetch()) {
$model = new WorkingHours(); $model = new WorkingHours();
$model $model
@@ -56,6 +80,35 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri
return $models; return $models;
} }
protected function buildFilterString(?string $start, ?string $end): string
{
if (is_string($start) && is_string($end)) {
return 'where "Datum" >= :start AND "Datum" <= :end ';
}
if (is_string($start)) {
return 'where "Datum" >= :start ';
}
if (is_string($end)) {
return 'where "Datum" <= :end ';
}
return '';
}
protected function buildFindFilteredStatement(?string $start, ?string $end): ?PDOStatement
{
$query = 'select "Datum" as "workingDay", "Arbeitszeit" as "workingTime" from public."Arbeitszeiten" ';
$query .= $this->buildFilterString($start, $end);
$query .= 'order by "Datum"';
$stmt = $this->database->prepare($query);
if (is_string($start)) {
$stmt->bindParam(':start', $start);
}
if (is_string($end)) {
$stmt->bindParam(':end', $end);
}
return $stmt ?? null;
}
/** /**
* Gibt den Eintrag zurück, der durch die ID * Gibt den Eintrag zurück, der durch die ID
* *
@@ -21,13 +21,28 @@ class BrowseWorkingHoursCest
$I->seeHttpHeader('Access-Control-Max-Age'); $I->seeHttpHeader('Access-Control-Max-Age');
} }
/**
* @param ApiTester $I
*
* @dataProvider validWorkingHoursFilterProvider
*/
public function browseEmptyWorkingHours(ApiTester $I): void
{
$I->haveHttpHeader('Origin', Api::TEST_ORIGIN);
$I->sendGet('/working-hours', ['start-date' => '2020-04-01', 'end-date' => '2020-04-30']);
$I->seeResponseCodeIs(HttpCode::OK);
$I->canSeeHttpHeader('Access-Control-Allow-Origin', Api::TEST_ORIGIN);
$I->seeResponseIsJson();
$I->seeResponseEquals('[]');
}
public function validWorkingHoursFilterProvider(): array public function validWorkingHoursFilterProvider(): array
{ {
return [ return [
[[null]], [[null]],
[['start-date' => '2020-01-01', 'end-date' => '2020-01-08']], [['start-date' => '2020-01-01', 'end-date' => '2020-01-08']],
[['start-date' => '2020-01-01', 'end-date' => '2020-04-30']], [['start-date' => '2020-01-01', 'end-date' => '2020-04-30']],
[['start-date' => '2020-04-01', 'end-date' => '2020-04-30']], [['start-date' => '2020-03-01', 'end-date' => '2020-03-07']],
]; ];
} }
@@ -2,6 +2,7 @@
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Controller; namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Controller;
use Codeception\Example;
use Codeception\Test\Unit; use Codeception\Test\Unit;
use Exception; use Exception;
use PDO; use PDO;
@@ -25,8 +26,8 @@ class WorkingHoursControllerTest extends Unit
const NEW_INTERVAL = '07:59:00'; const NEW_INTERVAL = '07:59:00';
protected ContainerInterface $container; protected ContainerInterface $container;
protected Request $request; protected Request $request;
protected Response $response; protected Response $response;
/** /**
* @throws Exception * @throws Exception
@@ -37,16 +38,16 @@ class WorkingHoursControllerTest extends Unit
/** @noinspection SpellCheckingInspection */ /** @noinspection SpellCheckingInspection */
$this->container = $this->makeEmpty(ContainerInterface::class, [ $this->container = $this->makeEmpty(ContainerInterface::class, [
'has' => true, 'has' => true,
'get' => new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass') 'get' => new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass'),
]); ]);
$this->request = $this->makeEmpty(Request::class, [ $this->request = $this->makeEmpty(Request::class, [
'getParsedBody' => [ 'getParsedBody' => [
'workingDay' => self::EXISTING_DATE, 'workingDay' => self::EXISTING_DATE,
'workingTime' => self::EXISTING_INTERVAL, 'workingTime' => self::EXISTING_INTERVAL,
] ],
]); ]);
$this->response = $this->makeEmpty(Response::class, [ $this->response = $this->makeEmpty(Response::class, [
'getBody' => $this->makeEmpty(StreamInterface::class, [ 'getBody' => $this->makeEmpty(StreamInterface::class, [
'write' => function (mixed $data) { 'write' => function (mixed $data) {
$this->assertIsString($data); $this->assertIsString($data);
$this->assertJson($data); $this->assertJson($data);
@@ -65,23 +66,202 @@ class WorkingHoursControllerTest extends Unit
public function testConstructWithNonDatabase(): void public function testConstructWithNonDatabase(): void
{ {
$this->container = $this->makeEmpty(ContainerInterface::class, [ $this->container = $this->makeEmpty(ContainerInterface::class, [
'has' => false 'has' => false,
]); ]);
$this->expectException(NotDatabasesException::class); $this->expectException(NotDatabasesException::class);
new WorkingHoursController($this->container); new WorkingHoursController($this->container);
} }
/** /**
* @return array<int, array<int, array<string, string>>>
*/
public function valideFilterDataProvider(): array
{
return [
[
[],
],
[
[
'start-date' => '2020-03-01',
'end-date' => '2020-03-31',
],
],
[
[
'start-date' => '2020-03-01',
],
],
[
[
'end-date' => '2020-03-31',
],
],
];
}
/**
* @param array<string, string> $queryParams
*
* @dataProvider valideFilterDataProvider
*
* @throws HttpInternalServerErrorException
* @throws NotDatabasesException * @throws NotDatabasesException
* @throws Exception * @throws Exception
*/ */
public function testBrowse(): void public function testBrowseWithValideParameter(array $queryParams): void
{ {
$this->request = $this->makeEmpty(Request::class, [
'getQueryParams' => $queryParams,
]);
$controller = new WorkingHoursController($this->container); $controller = new WorkingHoursController($this->container);
$response = $controller->browse($this->request, $this->response, []); $response = $controller->browse($this->request, $this->response, []);
$this->assertInstanceOf(Response::class, $response); $this->assertInstanceOf(Response::class, $response);
} }
/**
* @return array<int, array<int, array<string, string|null>>>
*/
public function invalideFilterDataProvider(): array
{
return [
[
[
'start-date' => '',
'end-date' => '',
],
],
[
[
'start-date' => '',
],
],
[
[
'end-date' => '',
],
],
[
[
'start-date' => '2020-03-01',
'end-date' => '',
],
],
[
[
'start-date' => '',
'end-date' => '2020-03-31',
],
],
//
[
[
'start-date' => '2020-03-00',
'end-date' => '2020-04-31',
],
],
[
[
'start-date' => '2020-03-00',
],
],
[
[
'end-date' => '2020-04-31',
],
],
[
[
'start-date' => '2020-03-01',
'end-date' => '2020-04-31',
],
],
[
[
'start-date' => '2020-03-00',
'end-date' => '2020-03-31',
],
],
//
[
[
'start-date' => '2020-03',
'end-date' => '2020-03',
],
],
[
[
'start-date' => '2020-03',
],
],
[
[
'end-date' => '2020-03',
],
],
[
[
'start-date' => '2020-03-01',
'end-date' => '2020-03',
],
],
[
[
'start-date' => '2020-03',
'end-date' => '2020-03-31',
],
],
//
[
[
'start-date' => '2020',
'end-date' => '2020',
],
],
[
[
'start-date' => '2020',
],
],
[
[
'end-date' => '2020',
],
],
[
[
'start-date' => '2020-03-01',
'end-date' => '2020',
],
],
[
[
'start-date' => '2020',
'end-date' => '2020-03-31',
],
],
];
}
/**
* @param array<string, string> $queryParams
*
* @dataProvider invalideFilterDataProvider
*
* @throws HttpInternalServerErrorException
* @throws NotDatabasesException
* @throws Exception
*/
public function testBrowseWithInvalideParameter(array $queryParams): void
{
$this->request = $this->makeEmpty(Request::class, [
'getQueryParams' => $queryParams,
]);
$controller = new WorkingHoursController($this->container);
$this->expectException(HttpBadRequestException::class);
$controller->browse($this->request, $this->response, []);
}
/** /**
* @throws NotDatabasesException * @throws NotDatabasesException
* @throws HttpBadRequestException * @throws HttpBadRequestException
@@ -91,7 +271,7 @@ class WorkingHoursControllerTest extends Unit
{ {
$controller = new WorkingHoursController($this->container); $controller = new WorkingHoursController($this->container);
$response = $controller->update($this->request, $this->response, [ $response = $controller->update($this->request, $this->response, [
'id' => self::EXISTING_DATE 'id' => self::EXISTING_DATE,
]); ]);
$this->assertInstanceOf(Response::class, $response); $this->assertInstanceOf(Response::class, $response);
} }
@@ -106,23 +286,23 @@ class WorkingHoursControllerTest extends Unit
$controller = new WorkingHoursController($this->container); $controller = new WorkingHoursController($this->container);
$this->expectException(HttpNotFoundException::class); $this->expectException(HttpNotFoundException::class);
$controller->update($this->request, $this->response, [ $controller->update($this->request, $this->response, [
'id' => self::NEW_DATE 'id' => self::NEW_DATE,
]); ]);
} }
/** /**
* @return string[][] * @return string[][]
*/ */
public function invalidDataProvider(): array public function invalidCreatDataProvider(): array
{ {
return [ return [
[ [
self::NEW_DATE, self::NEW_DATE,
'07:59' '07:59',
], ],
[ [
'2020-13-33', '2020-13-33',
self::NEW_INTERVAL self::NEW_INTERVAL,
], ],
]; ];
} }
@@ -131,7 +311,7 @@ class WorkingHoursControllerTest extends Unit
* @param string $date * @param string $date
* @param string $time * @param string $time
* *
* @dataProvider invalidDataProvider * @dataProvider invalidCreatDataProvider
* *
* @throws HttpBadRequestException * @throws HttpBadRequestException
* @throws HttpConflictRequestException * @throws HttpConflictRequestException
@@ -145,7 +325,7 @@ class WorkingHoursControllerTest extends Unit
'getParsedBody' => [ 'getParsedBody' => [
'workingDay' => $date, 'workingDay' => $date,
'workingTime' => $time, 'workingTime' => $time,
] ],
]); ]);
$controller = new WorkingHoursController($this->container); $controller = new WorkingHoursController($this->container);
$this->expectException(HttpBadRequestException::class); $this->expectException(HttpBadRequestException::class);
@@ -165,7 +345,7 @@ class WorkingHoursControllerTest extends Unit
'getParsedBody' => [ 'getParsedBody' => [
'workingDay' => self::NEW_DATE, 'workingDay' => self::NEW_DATE,
'workingTime' => self::NEW_INTERVAL, 'workingTime' => self::NEW_INTERVAL,
] ],
]); ]);
$controller = new WorkingHoursController($this->container); $controller = new WorkingHoursController($this->container);
$response = $controller->creat($this->request, $this->response, []); $response = $controller->creat($this->request, $this->response, []);
@@ -185,7 +365,7 @@ class WorkingHoursControllerTest extends Unit
'getParsedBody' => [ 'getParsedBody' => [
'workingDay' => self::EXISTING_DATE, 'workingDay' => self::EXISTING_DATE,
'workingTime' => self::EXISTING_INTERVAL, 'workingTime' => self::EXISTING_INTERVAL,
] ],
]); ]);
$controller = new WorkingHoursController($this->container); $controller = new WorkingHoursController($this->container);
$this->expectException(HttpConflictRequestException::class); $this->expectException(HttpConflictRequestException::class);
@@ -201,7 +381,7 @@ class WorkingHoursControllerTest extends Unit
{ {
$controller = new WorkingHoursController($this->container); $controller = new WorkingHoursController($this->container);
$response = $controller->read($this->request, $this->response, [ $response = $controller->read($this->request, $this->response, [
'id' => self::EXISTING_DATE 'id' => self::EXISTING_DATE,
]); ]);
$this->assertInstanceOf(Response::class, $response); $this->assertInstanceOf(Response::class, $response);
} }
@@ -216,7 +396,7 @@ class WorkingHoursControllerTest extends Unit
$controller = new WorkingHoursController($this->container); $controller = new WorkingHoursController($this->container);
$this->expectException(HttpNotFoundException::class); $this->expectException(HttpNotFoundException::class);
$controller->read($this->request, $this->response, [ $controller->read($this->request, $this->response, [
'id' => self::NEW_DATE 'id' => self::NEW_DATE,
]); ]);
} }
} }
@@ -2,6 +2,7 @@
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Repositories; namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Repositories;
use Codeception\Example;
use Codeception\Test\Unit; use Codeception\Test\Unit;
use DateInterval; use DateInterval;
use DateTime; use DateTime;
@@ -10,6 +11,7 @@ use InvalidArgumentException;
use PDO; use PDO;
use TorstenHettstedt\TimekeepingApi\Models\ModelInterface; use TorstenHettstedt\TimekeepingApi\Models\ModelInterface;
use TorstenHettstedt\TimekeepingApi\Models\WorkingHours; use TorstenHettstedt\TimekeepingApi\Models\WorkingHours;
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryBadWhereDataException;
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordAlreadyExistException; use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordAlreadyExistException;
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException; use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException;
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursRepository; use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursRepository;
@@ -22,7 +24,7 @@ class WorkingHoursRepositoryTest extends Unit
const NEW_INTERVAL = 'PT7H59M'; const NEW_INTERVAL = 'PT7H59M';
const RECORDS_COUNT = 61; const RECORDS_COUNT = 61;
protected PDO $pdoObject; protected PDO $pdoObject;
protected function _before(): void protected function _before(): void
{ {
@@ -34,7 +36,7 @@ class WorkingHoursRepositoryTest extends Unit
/** /**
* @throws Exception * @throws Exception
*/ */
public function testFindAll(): void public function testFindAllWithBlankParameter(): void
{ {
$repository = new WorkingHoursRepository($this->pdoObject); $repository = new WorkingHoursRepository($this->pdoObject);
$models = $repository->findAll(); $models = $repository->findAll();
@@ -43,6 +45,81 @@ class WorkingHoursRepositoryTest extends Unit
$this->assertCount(self::RECORDS_COUNT, $models); $this->assertCount(self::RECORDS_COUNT, $models);
} }
/**
* @return array<int, array<int|string|null>>
*/
public function valideFilterParameter(): array
{
return [
[null, null, 61,],
['2020-04-01', null, 0,],
['2020-04-01', '2020-04-30', 0,],
[null, '2020-04-30', 61,],
['2020-03-01', null, 22,],
['2020-03-01', '2020-03-31', 22,],
['2020-03-01', '2020-03-07', 5,],
];
}
/**
* @param string|null $start
* @param string|null $ende
* @param int $count
*
* @dataProvider valideFilterParameter
*
* @throws Exception
*/
public function testFindAllWithValideParameter(?string $start, ?string $ende, int $count): void
{
$repository = new WorkingHoursRepository($this->pdoObject);
$models = $repository->findFiltered($start, $ende);
$this->assertIsArray($models);
$this->assertContainsOnly(WorkingHours::class, $models);
$this->assertCount($count, $models);
}
/**
* @return array<int, array<int|string|null>>
*/
public function invalideFilterParameter(): array
{
return [
['2020-04-31',null],
[null, '2020-04-31'],
['2020-04-00','2020-04-31'],
['2020-04-01','2020-04-31'],
['2020-04-00','2020-04-30'],
//
['2020-04', null],
[null, '2020-04'],
['2020-04', '2020-04'],
['2020-04-01','2020-04'],
['2020-04', '2020-04-30'],
//
['2020', null],
[null, '2020'],
['2020', '2020'],
['2020-04-01','2020'],
['2020', '2020-04-30'],
];
}
/**
* @param string|null $start
* @param string|null $ende
*
* @dataProvider invalideFilterParameter
*
* @throws Exception
*/
public function testFindAllWithInvalideParameter(?string $start, ?string $ende): void
{
$repository = new WorkingHoursRepository($this->pdoObject);
$this->expectException(RepositoryBadWhereDataException::class);
$repository->findFiltered($start, $ende);
}
/** /**
* @throws RepositoryRecordNotFoundException * @throws RepositoryRecordNotFoundException
*/ */