Next major version 2 with PHP 8.1, Symfony 6, Tabler UI, 2FA ... (#2902)
This commit is contained in:
@@ -65,7 +65,20 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
return '/' . ltrim($url, '/');
|
||||
}
|
||||
|
||||
protected function assertRequestIsSecured(HttpKernelBrowser $client, string $url, $method = 'GET')
|
||||
protected function assertPagination(Response $response, int $page, int $pageSize, int $totalPages, int $totalResults): void
|
||||
{
|
||||
$this->assertTrue($response->headers->has('X-Page'), 'Missing "X-Page" header');
|
||||
$this->assertTrue($response->headers->has('X-Total-Count'), 'Missing "X-Total-Count" header');
|
||||
$this->assertTrue($response->headers->has('X-Total-Pages'), 'Missing "X-Total-Pages" header');
|
||||
$this->assertTrue($response->headers->has('X-Per-Page'), 'Missing "X-Per-Page" header');
|
||||
|
||||
$this->assertEquals($page, $response->headers->get('X-Page'));
|
||||
$this->assertEquals($totalResults, $response->headers->get('X-Total-Count'));
|
||||
$this->assertEquals($totalPages, $response->headers->get('X-Total-Pages'));
|
||||
$this->assertEquals($pageSize, $response->headers->get('X-Per-Page'));
|
||||
}
|
||||
|
||||
protected function assertRequestIsSecured(HttpKernelBrowser $client, string $url, $method = 'GET'): void
|
||||
{
|
||||
$this->request($client, $url, $method);
|
||||
$this->assertResponseIsSecured($client->getResponse(), $url);
|
||||
@@ -75,9 +88,9 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
* @param Response $response
|
||||
* @param string $url
|
||||
*/
|
||||
protected function assertResponseIsSecured(Response $response, string $url)
|
||||
protected function assertResponseIsSecured(Response $response, string $url): void
|
||||
{
|
||||
$data = ['message' => 'Authentication required, missing headers: X-AUTH-USER, X-AUTH-TOKEN'];
|
||||
$data = ['message' => 'Authentication required, missing user header: X-AUTH-USER'];
|
||||
|
||||
self::assertEquals(
|
||||
$data,
|
||||
@@ -97,7 +110,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
* @param string $url
|
||||
* @param string $method
|
||||
*/
|
||||
protected function assertUrlIsSecuredForRole(string $role, string $url, string $method = 'GET')
|
||||
protected function assertUrlIsSecuredForRole(string $role, string $url, string $method = 'GET'): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser($role);
|
||||
$client->request($method, $this->createUrl($url));
|
||||
@@ -108,107 +121,110 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
);
|
||||
|
||||
$this->assertApiException($client->getResponse(), [
|
||||
'code' => 403,
|
||||
'message' => 'Access denied.'
|
||||
'code' => Response::HTTP_FORBIDDEN,
|
||||
'message' => 'Forbidden'
|
||||
]);
|
||||
}
|
||||
|
||||
protected function request(HttpKernelBrowser $client, string $url, $method = 'GET', array $parameters = [], string $content = null): Crawler
|
||||
public function request(HttpKernelBrowser $client, string $url, $method = 'GET', array $parameters = [], string $content = null): Crawler
|
||||
{
|
||||
$server = ['HTTP_CONTENT_TYPE' => 'application/json', 'CONTENT_TYPE' => 'application/json'];
|
||||
|
||||
return $client->request($method, $this->createUrl($url), $parameters, [], $server, $content);
|
||||
}
|
||||
|
||||
protected function assertEntityNotFound(string $role, string $url, string $method = 'GET', ?string $message = null)
|
||||
protected function assertEntityNotFound(string $role, string $url, string $method = 'GET', ?string $message = null): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser($role);
|
||||
$this->request($client, $url, $method);
|
||||
$this->assertApiException($client->getResponse(), [
|
||||
'code' => 404,
|
||||
'message' => $message ?? 'Not found'
|
||||
'code' => Response::HTTP_NOT_FOUND,
|
||||
'message' => 'Not Found'
|
||||
]);
|
||||
}
|
||||
|
||||
protected function assertNotFoundForDelete(HttpKernelBrowser $client, string $url)
|
||||
protected function assertNotFoundForDelete(HttpKernelBrowser $client, string $url): void
|
||||
{
|
||||
$this->assertExceptionForMethod($client, $url, 'DELETE', [], [
|
||||
'code' => 404,
|
||||
'message' => 'Not found'
|
||||
'code' => Response::HTTP_NOT_FOUND,
|
||||
'message' => 'Not Found'
|
||||
]);
|
||||
}
|
||||
|
||||
protected function assertEntityNotFoundForDelete(string $role, string $url, ?string $message = null)
|
||||
{
|
||||
$this->assertExceptionForDeleteAction($role, $url, [], [
|
||||
'code' => 404,
|
||||
'message' => $message ?? 'Not found'
|
||||
]);
|
||||
}
|
||||
|
||||
protected function assertEntityNotFoundForPatch(string $role, string $url, array $data, ?string $message = null)
|
||||
protected function assertEntityNotFoundForPatch(string $role, string $url, array $data): void
|
||||
{
|
||||
$this->assertExceptionForPatchAction($role, $url, $data, [
|
||||
'code' => 404,
|
||||
'message' => $message ?? 'Not found',
|
||||
'code' => Response::HTTP_NOT_FOUND,
|
||||
'message' => 'Not Found',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function assertEntityNotFoundForPost(string $role, string $url, array $data, ?string $message = null)
|
||||
protected function assertEntityNotFoundForPost(HttpKernelBrowser $client, string $url, array $data = []): void
|
||||
{
|
||||
$this->assertExceptionForPostAction($role, $url, $data, [
|
||||
'code' => 404,
|
||||
'message' => $message ?? 'Not found',
|
||||
$this->assertExceptionForMethod($client, $url, 'POST', $data, [
|
||||
'code' => Response::HTTP_NOT_FOUND,
|
||||
'message' => 'Not Found',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function assertExceptionForDeleteAction(string $role, string $url, array $data, array $expectedErrors)
|
||||
protected function assertExceptionForDeleteAction(string $role, string $url, array $data, array $expectedErrors): void
|
||||
{
|
||||
$this->assertExceptionForRole($role, $url, 'DELETE', $data, $expectedErrors);
|
||||
}
|
||||
|
||||
protected function assertExceptionForPatchAction(string $role, string $url, array $data, array $expectedErrors)
|
||||
protected function assertExceptionForPatchAction(string $role, string $url, array $data, array $expectedErrors): void
|
||||
{
|
||||
$this->assertExceptionForRole($role, $url, 'PATCH', $data, $expectedErrors);
|
||||
}
|
||||
|
||||
protected function assertExceptionForPostAction(string $role, string $url, array $data, array $expectedErrors)
|
||||
protected function assertExceptionForPostAction(string $role, string $url, array $data, array $expectedErrors): void
|
||||
{
|
||||
$this->assertExceptionForRole($role, $url, 'POST', $data, $expectedErrors);
|
||||
}
|
||||
|
||||
protected function assertExceptionForMethod(HttpKernelBrowser $client, string $url, string $method, array $data, array $expectedErrors)
|
||||
protected function assertExceptionForMethod(HttpKernelBrowser $client, string $url, string $method, array $data, array $expectedErrors): void
|
||||
{
|
||||
$this->request($client, $url, $method, [], json_encode($data));
|
||||
$this->assertApiException($client->getResponse(), $expectedErrors);
|
||||
}
|
||||
|
||||
protected function assertApiException(Response $response, array $expectedErrors)
|
||||
protected function assertApiException(Response $response, array $expectedErrors): void
|
||||
{
|
||||
self::assertFalse($response->isSuccessful());
|
||||
self::assertEquals($expectedErrors['code'], $response->getStatusCode());
|
||||
self::assertEquals($expectedErrors, json_decode($response->getContent(), true));
|
||||
}
|
||||
|
||||
protected function assertExceptionForRole(string $role, string $url, string $method, array $data, array $expectedErrors)
|
||||
protected function assertExceptionForRole(string $role, string $url, string $method, array $data, array $expectedErrors): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser($role);
|
||||
$this->assertExceptionForMethod($client, $url, $method, $data, $expectedErrors);
|
||||
}
|
||||
|
||||
protected function assertApi500Exception(Response $response, string $message)
|
||||
protected function assertApi500Exception(Response $response, string $message): void
|
||||
{
|
||||
$this->assertApiException($response, ['code' => 500, 'message' => $message]);
|
||||
$this->assertApiException($response, ['code' => Response::HTTP_INTERNAL_SERVER_ERROR, 'message' => $message]);
|
||||
}
|
||||
|
||||
protected function assertApiAccessDenied(HttpKernelBrowser $client, string $url, string $message)
|
||||
protected function assertBadRequest(HttpKernelBrowser $client, string $url, string $method): void
|
||||
{
|
||||
$this->assertExceptionForMethod($client, $url, $method, [], [
|
||||
'code' => Response::HTTP_BAD_REQUEST,
|
||||
'message' => 'Bad Request'
|
||||
]);
|
||||
}
|
||||
|
||||
protected function assertApiAccessDenied(HttpKernelBrowser $client, string $url, string $message = 'Forbidden'): void
|
||||
{
|
||||
$this->request($client, $url);
|
||||
$this->assertApiResponseAccessDenied($client->getResponse(), $message);
|
||||
}
|
||||
|
||||
protected function assertApiResponseAccessDenied(Response $response, string $message)
|
||||
protected function assertApiResponseAccessDenied(Response $response, string $message = 'Forbidden'): void
|
||||
{
|
||||
// APP_DEBUG = 1 means "real exception messages" - it is always overwritten
|
||||
$message = 'Forbidden';
|
||||
|
||||
$this->assertApiException($response, [
|
||||
'code' => Response::HTTP_FORBIDDEN,
|
||||
'message' => $message
|
||||
@@ -219,17 +235,24 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
* @param Response $response
|
||||
* @param array<int, string>|array<string, mixed> $failedFields
|
||||
* @param bool $extraFields test for the error "This form should not contain extra fields"
|
||||
* @param array<int, string>|array<string, mixed> $globalError
|
||||
*/
|
||||
protected function assertApiCallValidationError(Response $response, array $failedFields, bool $extraFields = false)
|
||||
protected function assertApiCallValidationError(Response $response, array $failedFields, bool $extraFields = false, array $globalError = []): void
|
||||
{
|
||||
self::assertFalse($response->isSuccessful());
|
||||
$result = json_decode($response->getContent(), true);
|
||||
|
||||
self::assertArrayHasKey('errors', $result);
|
||||
|
||||
if ($extraFields) {
|
||||
self::assertArrayHasKey('errors', $result['errors']);
|
||||
self::assertEquals($result['errors']['errors'][0], 'This form should not contain extra fields.');
|
||||
self::assertEquals('This form should not contain extra fields.', $result['errors']['errors'][0]);
|
||||
}
|
||||
|
||||
if (\count($globalError) > 0) {
|
||||
self::assertArrayHasKey('errors', $result['errors']);
|
||||
foreach ($globalError as $err) {
|
||||
self::assertTrue(\in_array($err, $result['errors']['errors']), 'Missing global validation error: ' . $err);
|
||||
}
|
||||
}
|
||||
|
||||
self::assertArrayHasKey('children', $result['errors']);
|
||||
@@ -275,6 +298,16 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
protected static function getExpectedResponseStructure(string $type): array
|
||||
{
|
||||
switch ($type) {
|
||||
case 'PageActionItem':
|
||||
return [
|
||||
'id' => 'string',
|
||||
'title' => '@string',
|
||||
'url' => '@string',
|
||||
'class' => '@string',
|
||||
'attr' => 'array',
|
||||
'divider' => 'bool'
|
||||
];
|
||||
|
||||
case 'TagEntity':
|
||||
return [
|
||||
'id' => 'int',
|
||||
@@ -282,7 +315,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'color' => '@string',
|
||||
];
|
||||
|
||||
// embedded meta data
|
||||
// embedded meta data
|
||||
case 'UserPreference':
|
||||
return [
|
||||
'name' => 'string',
|
||||
@@ -298,9 +331,9 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'value' => 'string',
|
||||
];
|
||||
|
||||
// if a user is embedded in other objects
|
||||
// if a user is embedded in other objects
|
||||
case 'User':
|
||||
// if a list of users is loaded
|
||||
// if a list of users is loaded
|
||||
case 'UserCollection':
|
||||
return [
|
||||
'id' => 'int',
|
||||
@@ -309,9 +342,11 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'color' => '@string',
|
||||
'alias' => '@string',
|
||||
'accountNumber' => '@string',
|
||||
'initials' => '@string',
|
||||
'title' => '@string',
|
||||
];
|
||||
|
||||
// if a user is loaded explicitly
|
||||
// if a user is loaded explicitly
|
||||
case 'UserEntity':
|
||||
return [
|
||||
'id' => 'int',
|
||||
@@ -323,6 +358,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'color' => '@string',
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
'roles' => ['result' => 'array', 'type' => 'string'],
|
||||
'initials' => 'string',
|
||||
'language' => 'string',
|
||||
'timezone' => 'string',
|
||||
'accountNumber' => '@string',
|
||||
@@ -330,9 +366,9 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'preferences' => ['result' => 'array', 'type' => 'UserPreference'],
|
||||
];
|
||||
|
||||
// if a team is embedded
|
||||
// if a team is embedded
|
||||
case 'Team':
|
||||
// if a collection of teams is requested
|
||||
// if a collection of teams is requested
|
||||
case 'TeamCollection':
|
||||
return [
|
||||
'id' => 'int',
|
||||
@@ -340,35 +376,33 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'color' => '@string',
|
||||
];
|
||||
|
||||
// explicitly requested team
|
||||
// explicitly requested team
|
||||
case 'TeamEntity':
|
||||
return [
|
||||
'id' => 'int',
|
||||
'name' => 'string',
|
||||
'color' => '@string',
|
||||
'teamlead' => ['result' => 'object', 'type' => '@User'],
|
||||
'members' => ['result' => 'array', 'type' => 'TeamMember'],
|
||||
'users' => ['result' => 'array', 'type' => 'User'],
|
||||
'customers' => ['result' => 'array', 'type' => '@Customer'],
|
||||
'projects' => ['result' => 'array', 'type' => '@Project'],
|
||||
'activities' => ['result' => 'array', 'type' => '@Activity'],
|
||||
];
|
||||
|
||||
// if the team is used inside the team context
|
||||
// if the team is used inside the team context
|
||||
case 'TeamMember':
|
||||
return [
|
||||
'user' => ['result' => 'object', 'type' => 'User'],
|
||||
'teamlead' => 'bool',
|
||||
];
|
||||
|
||||
// if the team is used inside the user context
|
||||
// if the team is used inside the user context
|
||||
case 'TeamMembership':
|
||||
return [
|
||||
'team' => ['result' => 'object', 'type' => 'Team'],
|
||||
'teamlead' => 'bool',
|
||||
];
|
||||
|
||||
// if a customer is embedded in other objects
|
||||
// if a customer is embedded in other objects
|
||||
case 'Customer':
|
||||
return [
|
||||
'id' => 'int',
|
||||
@@ -380,7 +414,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'comment' => '@string',
|
||||
];
|
||||
|
||||
// if a list of customers is loaded
|
||||
// if a list of customers is loaded
|
||||
case 'CustomerCollection':
|
||||
return [
|
||||
'id' => 'int',
|
||||
@@ -395,7 +429,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'currency' => 'string', // since 1.10
|
||||
];
|
||||
|
||||
// if a customer is loaded explicitly
|
||||
// if a customer is loaded explicitly
|
||||
case 'CustomerEntity':
|
||||
return [
|
||||
'id' => 'int',
|
||||
@@ -424,7 +458,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'budgetType' => '@string', // since 1.15
|
||||
];
|
||||
|
||||
// if a project is embedded
|
||||
// if a project is embedded
|
||||
case 'Project':
|
||||
return [
|
||||
'id' => 'int',
|
||||
@@ -437,7 +471,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'comment' => '@string',
|
||||
];
|
||||
|
||||
// if a project is embedded in an expanded collection (here timesheet)
|
||||
// if a project is embedded in an expanded collection (here timesheet)
|
||||
case 'ProjectExpanded':
|
||||
return [
|
||||
'id' => 'int',
|
||||
@@ -450,7 +484,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'comment' => '@string',
|
||||
];
|
||||
|
||||
// if a collection of projects is loaded
|
||||
// if a collection of projects is loaded
|
||||
case 'ProjectCollection':
|
||||
return [
|
||||
'id' => 'int',
|
||||
@@ -468,7 +502,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'comment' => '@string',
|
||||
];
|
||||
|
||||
// if a project is explicitly loaded
|
||||
// if a project is explicitly loaded
|
||||
case 'ProjectEntity':
|
||||
return [
|
||||
'id' => 'int',
|
||||
@@ -479,19 +513,19 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'color' => '@string',
|
||||
'metaFields' => ['result' => 'array', 'type' => 'ProjectMeta'],
|
||||
'parentTitle' => 'string',
|
||||
'start' => '@datetime',
|
||||
'end' => '@datetime',
|
||||
'start' => '@date',
|
||||
'end' => '@date',
|
||||
'globalActivities' => 'bool',
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
'comment' => '@string',
|
||||
'budget' => 'float',
|
||||
'timeBudget' => 'int',
|
||||
'orderNumber' => '@string',
|
||||
'orderDate' => '@datetime',
|
||||
'orderDate' => '@date',
|
||||
'budgetType' => '@string', // since 1.15
|
||||
];
|
||||
|
||||
// embedded activities
|
||||
// embedded activities
|
||||
case 'Activity':
|
||||
return [
|
||||
'id' => 'int',
|
||||
@@ -514,7 +548,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'comment' => '@string',
|
||||
];
|
||||
|
||||
// collection of activities
|
||||
// collection of activities
|
||||
case 'ActivityCollection':
|
||||
return [
|
||||
'id' => 'int',
|
||||
@@ -529,7 +563,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'teams' => ['result' => 'array', 'type' => 'Team'],
|
||||
];
|
||||
|
||||
// if a activity is explicitly loaded
|
||||
// if a activity is explicitly loaded
|
||||
case 'ActivityEntity':
|
||||
return [
|
||||
'id' => 'int',
|
||||
@@ -565,10 +599,10 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'billable' => 'bool',
|
||||
'fixedRate' => '@float',
|
||||
'hourlyRate' => '@float',
|
||||
// TODO new fields: billable, category
|
||||
// TODO new fields: category
|
||||
];
|
||||
|
||||
case 'TimesheetEntityFull':
|
||||
case 'TimesheetExpanded':
|
||||
return [
|
||||
'id' => 'int',
|
||||
'begin' => 'DateTime',
|
||||
@@ -579,14 +613,14 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'activity' => ['result' => 'object', 'type' => 'ActivityExpanded'],
|
||||
'project' => ['result' => 'object', 'type' => 'ProjectExpanded'],
|
||||
'tags' => ['result' => 'array', 'type' => 'string'],
|
||||
'user' => 'int',
|
||||
'user' => ['result' => 'object', 'type' => 'User'],
|
||||
'metaFields' => ['result' => 'array', 'type' => 'TimesheetMeta'],
|
||||
'internalRate' => 'float',
|
||||
'exported' => 'bool',
|
||||
'billable' => 'bool',
|
||||
'fixedRate' => '@float',
|
||||
'hourlyRate' => '@float',
|
||||
// TODO new fields: billable, category
|
||||
// TODO new fields: category
|
||||
];
|
||||
|
||||
case 'TimesheetCollection':
|
||||
@@ -618,7 +652,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'activity' => ['result' => 'object', 'type' => 'Activity'],
|
||||
'project' => ['result' => 'object', 'type' => 'ProjectExpanded'],
|
||||
'tags' => ['result' => 'array', 'type' => 'string'],
|
||||
'user' => 'int',
|
||||
'user' => ['result' => 'object', 'type' => 'User'],
|
||||
'metaFields' => ['result' => 'array', 'type' => 'TimesheetMeta'],
|
||||
'internalRate' => 'float',
|
||||
'exported' => 'bool',
|
||||
@@ -637,7 +671,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
* @param array $result
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function assertApiResponseTypeStructure(string $type, array $result)
|
||||
protected function assertApiResponseTypeStructure(string $type, array $result): void
|
||||
{
|
||||
$expected = self::getExpectedResponseStructure($type);
|
||||
$expectedKeys = array_keys($expected);
|
||||
@@ -704,7 +738,12 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
}
|
||||
|
||||
if (strtolower($value) === 'datetime') {
|
||||
// TODO
|
||||
$date = \DateTime::createFromFormat('Y-m-d\TH:i:sO', $result[$key]);
|
||||
self::assertInstanceOf(\DateTime::class, $date, sprintf('Field "%s" was expected to be a Date with the format "Y-m-dTH:i:sO", but found: %s', $key, $result[$key]));
|
||||
$value = 'string';
|
||||
} elseif (strtolower($value) === 'date') {
|
||||
$date = \DateTime::createFromFormat('Y-m-d', $result[$key]);
|
||||
self::assertInstanceOf(\DateTime::class, $date, sprintf('Field "%s" was expected to be a Date with the format "Y-m-d", but found: %s', $key, $result[$key]));
|
||||
$value = 'string';
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user