2018-08-08 23:10:45 +02:00
< ? php
/*
* This file is part of the Kimai time - tracking app .
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
namespace App\Tests\API ;
use App\DataFixtures\UserFixtures ;
use App\Entity\User ;
use App\Tests\Controller\ControllerBaseTest ;
2020-07-17 20:30:16 +02:00
use PHPUnit\Framework\Constraint\IsType ;
2018-08-08 23:10:45 +02:00
use Symfony\Component\DomCrawler\Crawler ;
use Symfony\Component\HttpFoundation\Response ;
2020-02-13 16:47:20 +01:00
use Symfony\Component\HttpKernel\HttpKernelBrowser ;
2018-08-08 23:10:45 +02:00
/**
* Adds some useful functions for writing API integration tests .
*/
abstract class APIControllerBaseTest extends ControllerBaseTest
{
2024-04-05 23:51:16 +02:00
/**
* @ return array < string , string >
*/
private function getAuthHeader ( string $username , string $password ) : array
2018-08-08 23:10:45 +02:00
{
2024-04-05 23:51:16 +02:00
return [
'HTTP_AUTHORIZATION' => 'Bearer ' . $password ,
];
}
2018-08-08 23:10:45 +02:00
2024-04-05 23:51:16 +02:00
protected function getClientForAuthenticatedUser ( string $role = User :: ROLE_USER ) : HttpKernelBrowser
{
return match ( $role ) {
User :: ROLE_SUPER_ADMIN => self :: createClient ([], $this -> getAuthHeader ( UserFixtures :: USERNAME_SUPER_ADMIN , UserFixtures :: DEFAULT_API_TOKEN . '_super' )),
User :: ROLE_ADMIN => self :: createClient ([], $this -> getAuthHeader ( UserFixtures :: USERNAME_ADMIN , UserFixtures :: DEFAULT_API_TOKEN . '_admin' )),
User :: ROLE_TEAMLEAD => self :: createClient ([], $this -> getAuthHeader ( UserFixtures :: USERNAME_TEAMLEAD , UserFixtures :: DEFAULT_API_TOKEN . '_teamlead' )),
User :: ROLE_USER => self :: createClient ([], $this -> getAuthHeader ( UserFixtures :: USERNAME_USER , UserFixtures :: DEFAULT_API_TOKEN . '_user' )),
2024-08-04 18:22:27 +02:00
default => throw new \Exception ( \sprintf ( 'Unknown role "%s"' , $role )),
2024-04-05 23:51:16 +02:00
};
2018-08-08 23:10:45 +02:00
}
2021-04-08 18:07:57 +02:00
protected function createUrl ( string $url ) : string
2018-08-08 23:10:45 +02:00
{
2020-09-17 01:13:48 +02:00
return '/' . ltrim ( $url , '/' );
2018-08-08 23:10:45 +02:00
}
2022-12-31 21:19:55 +01:00
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' ));
}
2023-08-20 13:18:35 +02:00
protected function assertRequestIsSecured ( HttpKernelBrowser $client , string $url , string $method = 'GET' ) : void
2018-08-08 23:10:45 +02:00
{
$this -> request ( $client , $url , $method );
2024-04-05 23:51:16 +02:00
$response = $client -> getResponse ();
2018-08-08 23:10:45 +02:00
2024-04-05 23:51:16 +02:00
$data = [
'message' => 'Unauthorized' ,
'code' => 401
];
2018-08-08 23:10:45 +02:00
2019-09-19 20:28:31 +02:00
self :: assertEquals (
2018-08-08 23:10:45 +02:00
$data ,
json_decode ( $response -> getContent (), true ),
2024-08-04 18:22:27 +02:00
\sprintf ( 'The secure URL %s is not protected.' , $url )
2018-08-08 23:10:45 +02:00
);
2019-09-19 20:28:31 +02:00
self :: assertEquals (
2024-04-05 23:51:16 +02:00
Response :: HTTP_UNAUTHORIZED ,
2018-08-08 23:10:45 +02:00
$response -> getStatusCode (),
2024-08-04 18:22:27 +02:00
\sprintf ( 'The secure URL %s has the wrong status code %s.' , $url , $response -> getStatusCode ())
2018-08-08 23:10:45 +02:00
);
}
2022-12-31 21:19:55 +01:00
protected function assertUrlIsSecuredForRole ( string $role , string $url , string $method = 'GET' ) : void
2018-08-08 23:10:45 +02:00
{
$client = $this -> getClientForAuthenticatedUser ( $role );
$client -> request ( $method , $this -> createUrl ( $url ));
2019-09-19 20:28:31 +02:00
self :: assertFalse (
2018-08-08 23:10:45 +02:00
$client -> getResponse () -> isSuccessful (),
2024-08-04 18:22:27 +02:00
\sprintf ( 'The secure URL %s is not protected for role %s' , $url , $role )
2018-08-08 23:10:45 +02:00
);
2021-03-28 11:30:35 +02:00
$this -> assertApiException ( $client -> getResponse (), [
2022-12-31 21:19:55 +01:00
'code' => Response :: HTTP_FORBIDDEN ,
'message' => 'Forbidden'
2021-03-28 11:30:35 +02:00
]);
2018-08-08 23:10:45 +02:00
}
2023-08-20 13:18:35 +02:00
public function request ( HttpKernelBrowser $client , string $url , string $method = 'GET' , array $parameters = [], string $content = null ) : Crawler
2018-08-08 23:10:45 +02:00
{
2018-11-18 19:43:58 +01:00
$server = [ 'HTTP_CONTENT_TYPE' => 'application/json' , 'CONTENT_TYPE' => 'application/json' ];
return $client -> request ( $method , $this -> createUrl ( $url ), $parameters , [], $server , $content );
2018-08-08 23:10:45 +02:00
}
2024-09-22 16:17:45 +02:00
protected function assertEntityNotFound ( string $role , string $url ) : void
2018-08-08 23:10:45 +02:00
{
$client = $this -> getClientForAuthenticatedUser ( $role );
2024-09-22 16:17:45 +02:00
$this -> request ( $client , $url );
2021-03-28 11:30:35 +02:00
$this -> assertApiException ( $client -> getResponse (), [
2022-12-31 21:19:55 +01:00
'code' => Response :: HTTP_NOT_FOUND ,
'message' => 'Not Found'
2021-03-28 11:30:35 +02:00
]);
2018-08-08 23:10:45 +02:00
}
2018-11-18 19:43:58 +01:00
2022-12-31 21:19:55 +01:00
protected function assertNotFoundForDelete ( HttpKernelBrowser $client , string $url ) : void
2020-04-08 14:50:15 +02:00
{
2021-03-28 11:30:35 +02:00
$this -> assertExceptionForMethod ( $client , $url , 'DELETE' , [], [
2022-12-31 21:19:55 +01:00
'code' => Response :: HTTP_NOT_FOUND ,
'message' => 'Not Found'
2020-04-08 14:50:15 +02:00
]);
}
2022-12-31 21:19:55 +01:00
protected function assertEntityNotFoundForPatch ( string $role , string $url , array $data ) : void
2019-08-26 14:30:39 +02:00
{
2021-03-28 11:30:35 +02:00
$this -> assertExceptionForPatchAction ( $role , $url , $data , [
2022-12-31 21:19:55 +01:00
'code' => Response :: HTTP_NOT_FOUND ,
'message' => 'Not Found' ,
2019-08-26 14:30:39 +02:00
]);
}
2022-12-31 21:19:55 +01:00
protected function assertEntityNotFoundForPost ( HttpKernelBrowser $client , string $url , array $data = []) : void
2020-04-08 14:50:15 +02:00
{
2022-12-31 21:19:55 +01:00
$this -> assertExceptionForMethod ( $client , $url , 'POST' , $data , [
'code' => Response :: HTTP_NOT_FOUND ,
'message' => 'Not Found' ,
2020-04-08 14:50:15 +02:00
]);
}
2022-12-31 21:19:55 +01:00
protected function assertExceptionForDeleteAction ( string $role , string $url , array $data , array $expectedErrors ) : void
2020-04-08 14:50:15 +02:00
{
$this -> assertExceptionForRole ( $role , $url , 'DELETE' , $data , $expectedErrors );
}
2022-12-31 21:19:55 +01:00
protected function assertExceptionForPatchAction ( string $role , string $url , array $data , array $expectedErrors ) : void
2019-04-24 18:13:33 +02:00
{
2020-04-08 14:50:15 +02:00
$this -> assertExceptionForRole ( $role , $url , 'PATCH' , $data , $expectedErrors );
}
2019-04-24 18:13:33 +02:00
2022-12-31 21:19:55 +01:00
protected function assertExceptionForPostAction ( string $role , string $url , array $data , array $expectedErrors ) : void
2020-04-08 14:50:15 +02:00
{
$this -> assertExceptionForRole ( $role , $url , 'POST' , $data , $expectedErrors );
}
2022-12-31 21:19:55 +01:00
protected function assertExceptionForMethod ( HttpKernelBrowser $client , string $url , string $method , array $data , array $expectedErrors ) : void
2020-04-08 14:50:15 +02:00
{
$this -> request ( $client , $url , $method , [], json_encode ( $data ));
2021-03-28 11:30:35 +02:00
$this -> assertApiException ( $client -> getResponse (), $expectedErrors );
}
2019-05-13 17:32:01 +02:00
2022-12-31 21:19:55 +01:00
protected function assertApiException ( Response $response , array $expectedErrors ) : void
2021-03-28 11:30:35 +02:00
{
self :: assertFalse ( $response -> isSuccessful ());
self :: assertEquals ( $expectedErrors [ 'code' ], $response -> getStatusCode ());
self :: assertEquals ( $expectedErrors , json_decode ( $response -> getContent (), true ));
2019-05-13 17:32:01 +02:00
}
2022-12-31 21:19:55 +01:00
protected function assertExceptionForRole ( string $role , string $url , string $method , array $data , array $expectedErrors ) : void
2019-05-13 17:32:01 +02:00
{
$client = $this -> getClientForAuthenticatedUser ( $role );
2020-04-08 14:50:15 +02:00
$this -> assertExceptionForMethod ( $client , $url , $method , $data , $expectedErrors );
2019-04-24 18:13:33 +02:00
}
2022-12-31 21:19:55 +01:00
protected function assertApi500Exception ( Response $response , string $message ) : void
2019-05-10 13:45:09 +02:00
{
2022-12-31 21:19:55 +01:00
$this -> assertApiException ( $response , [ 'code' => Response :: HTTP_INTERNAL_SERVER_ERROR , 'message' => $message ]);
2019-05-10 13:45:09 +02:00
}
2022-12-31 21:19:55 +01:00
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
2019-04-24 18:13:33 +02:00
{
$this -> request ( $client , $url );
2019-05-10 13:45:09 +02:00
$this -> assertApiResponseAccessDenied ( $client -> getResponse (), $message );
}
2022-12-31 21:19:55 +01:00
protected function assertApiResponseAccessDenied ( Response $response , string $message = 'Forbidden' ) : void
2019-05-10 13:45:09 +02:00
{
2022-12-31 21:19:55 +01:00
// APP_DEBUG = 1 means "real exception messages" - it is always overwritten
$message = 'Forbidden' ;
2021-03-28 11:30:35 +02:00
$this -> assertApiException ( $response , [
'code' => Response :: HTTP_FORBIDDEN ,
'message' => $message
]);
2019-04-24 18:13:33 +02:00
}
2018-11-18 19:43:58 +01:00
/**
* @ param Response $response
2021-06-12 01:05:33 +02:00
* @ param array < int , string >| array < string , mixed > $failedFields
2020-07-17 20:30:16 +02:00
* @ param bool $extraFields test for the error " This form should not contain extra fields "
2022-12-31 21:19:55 +01:00
* @ param array < int , string >| array < string , mixed > $globalError
2018-11-18 19:43:58 +01:00
*/
2022-12-31 21:19:55 +01:00
protected function assertApiCallValidationError ( Response $response , array $failedFields , bool $extraFields = false , array $globalError = []) : void
2018-11-18 19:43:58 +01:00
{
2019-09-19 20:28:31 +02:00
self :: assertFalse ( $response -> isSuccessful ());
2018-11-18 19:43:58 +01:00
$result = json_decode ( $response -> getContent (), true );
2019-09-19 20:28:31 +02:00
self :: assertArrayHasKey ( 'errors' , $result );
if ( $extraFields ) {
self :: assertArrayHasKey ( 'errors' , $result [ 'errors' ]);
2022-12-31 21:19:55 +01:00
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 );
}
2019-09-19 20:28:31 +02:00
}
self :: assertArrayHasKey ( 'children' , $result [ 'errors' ]);
2018-11-18 19:43:58 +01:00
$data = $result [ 'errors' ][ 'children' ];
2021-08-07 18:05:41 +02:00
$foundErrors = [];
2021-06-12 01:05:33 +02:00
foreach ( $failedFields as $key => $value ) {
$messages = [];
$fieldName = $value ;
if ( \is_string ( $key )) {
$fieldName = $key ;
$messages = $value ;
if ( ! \is_array ( $messages )) {
$messages = [ $value ];
}
}
2021-08-07 18:05:41 +02:00
while ( stripos ( $fieldName , '.' ) !== false ) {
$parts = explode ( '.' , $fieldName );
$tmp = array_shift ( $parts );
2024-08-04 18:22:27 +02:00
self :: assertArrayHasKey ( $tmp , $data , \sprintf ( 'Could not find field "%s" in result' , $tmp ));
2021-08-07 18:05:41 +02:00
$data = $data [ $tmp ];
if ( \count ( $data ) === 1 && \array_key_exists ( 'children' , $data )) {
$data = $data [ 'children' ];
}
$fieldName = implode ( '.' , $parts );
}
2024-08-04 18:22:27 +02:00
self :: assertArrayHasKey ( $fieldName , $data , \sprintf ( 'Could not find validation error for field "%s" in list: %s' , $fieldName , implode ( ', ' , $failedFields )));
self :: assertArrayHasKey ( 'errors' , $data [ $fieldName ], \sprintf ( 'Field %s has no validation problem' , $fieldName ));
2021-06-12 01:05:33 +02:00
foreach ( $messages as $i => $message ) {
self :: assertEquals ( $message , $data [ $fieldName ][ 'errors' ][ $i ]);
}
2023-09-17 22:32:01 +02:00
if ( \array_key_exists ( 'errors' , ( array ) $data [ $fieldName ]) && \count ( $data [ $fieldName ][ 'errors' ]) > 0 ) {
2021-08-07 18:05:41 +02:00
$foundErrors [ $fieldName ] = \count ( $data [ $fieldName ][ 'errors' ]);
2020-07-17 20:30:16 +02:00
}
}
self :: assertEquals ( \count ( $failedFields ), \count ( $foundErrors ), 'Expected and actual validation error amount differs' );
}
protected static function getExpectedResponseStructure ( string $type ) : array
{
switch ( $type ) {
2024-09-22 16:17:45 +02:00
case 'Invoice' :
case 'InvoiceCollection' :
return [
'id' => 'int' ,
'comment' => '@string' ,
'createdAt' => 'datetime' ,
'currency' => 'string' ,
'customer' => [ 'result' => 'object' , 'type' => '@Customer' ],
'user' => [ 'result' => 'object' , 'type' => '@User' ],
'dueDays' => 'int' ,
'invoiceNumber' => 'string' ,
'metaFields' => 'array' ,
'paymentDate' => '@datetime' ,
'status' => 'string' ,
'tax' => 'float' ,
'total' => 'float' ,
'vat' => 'float' ,
];
2022-12-31 21:19:55 +01:00
case 'PageActionItem' :
return [
'id' => 'string' ,
'title' => '@string' ,
'url' => '@string' ,
'class' => '@string' ,
'attr' => 'array' ,
'divider' => 'bool'
];
2020-07-17 20:30:16 +02:00
case 'TagEntity' :
return [
'id' => 'int' ,
'name' => 'string' ,
2021-07-06 22:01:20 +02:00
'color' => '@string' ,
2024-06-16 13:15:49 +02:00
'color-safe' => 'string' ,
2023-06-09 17:07:49 +02:00
'visible' => 'bool' ,
2020-07-17 20:30:16 +02:00
];
2022-12-31 21:19:55 +01:00
// embedded meta data
2021-11-15 21:14:45 +01:00
case 'UserPreference' :
return [
'name' => 'string' ,
'value' => '@string' ,
];
2020-07-17 20:30:16 +02:00
case 'CustomerMeta' :
case 'ProjectMeta' :
case 'ActivityMeta' :
case 'TimesheetMeta' :
return [
'name' => 'string' ,
'value' => 'string' ,
];
2022-12-31 21:19:55 +01:00
// if a user is embedded in other objects
2020-07-17 20:30:16 +02:00
case 'User' :
2022-12-31 21:19:55 +01:00
// if a list of users is loaded
2020-07-17 20:30:16 +02:00
case 'UserCollection' :
return [
'id' => 'int' ,
'username' => 'string' ,
'enabled' => 'bool' ,
2023-10-19 11:21:50 +02:00
'apiToken' => 'bool' ,
2021-07-06 22:01:20 +02:00
'color' => '@string' ,
2020-07-17 20:30:16 +02:00
'alias' => '@string' ,
2021-07-19 00:13:39 +02:00
'accountNumber' => '@string' ,
2022-12-31 21:19:55 +01:00
'initials' => '@string' ,
'title' => '@string' ,
2020-07-17 20:30:16 +02:00
];
2022-12-31 21:19:55 +01:00
// if a user is loaded explicitly
2020-07-17 20:30:16 +02:00
case 'UserEntity' :
return [
'id' => 'int' ,
'username' => 'string' ,
'enabled' => 'bool' ,
2023-10-19 11:21:50 +02:00
'apiToken' => 'bool' ,
2020-07-17 20:30:16 +02:00
'alias' => '@string' ,
'title' => '@string' ,
2023-08-20 13:18:35 +02:00
'supervisor' => [ 'result' => 'object' , 'type' => '@UserEntity' ],
2020-07-17 20:30:16 +02:00
'avatar' => '@string' ,
2021-07-06 22:01:20 +02:00
'color' => '@string' ,
2020-07-17 20:30:16 +02:00
'teams' => [ 'result' => 'array' , 'type' => 'Team' ],
'roles' => [ 'result' => 'array' , 'type' => 'string' ],
2022-12-31 21:19:55 +01:00
'initials' => 'string' ,
2020-07-17 20:30:16 +02:00
'language' => 'string' ,
2024-01-30 00:09:53 +01:00
'locale' => 'string' ,
2020-07-17 20:30:16 +02:00
'timezone' => 'string' ,
2021-07-19 00:13:39 +02:00
'accountNumber' => '@string' ,
2021-08-07 18:05:41 +02:00
'memberships' => [ 'result' => 'array' , 'type' => 'TeamMembership' ],
2021-11-15 21:14:45 +01:00
'preferences' => [ 'result' => 'array' , 'type' => 'UserPreference' ],
2020-07-17 20:30:16 +02:00
];
2022-12-31 21:19:55 +01:00
// if a team is embedded
2020-07-17 20:30:16 +02:00
case 'Team' :
2022-12-31 21:19:55 +01:00
// if a collection of teams is requested
2020-07-17 20:30:16 +02:00
case 'TeamCollection' :
return [
'id' => 'int' ,
'name' => 'string' ,
2021-07-06 22:01:20 +02:00
'color' => '@string' ,
2020-07-17 20:30:16 +02:00
];
2022-12-31 21:19:55 +01:00
// explicitly requested team
2020-07-17 20:30:16 +02:00
case 'TeamEntity' :
return [
'id' => 'int' ,
'name' => 'string' ,
2021-07-06 22:01:20 +02:00
'color' => '@string' ,
2021-08-07 18:05:41 +02:00
'members' => [ 'result' => 'array' , 'type' => 'TeamMember' ],
2020-07-17 20:30:16 +02:00
'customers' => [ 'result' => 'array' , 'type' => '@Customer' ],
'projects' => [ 'result' => 'array' , 'type' => '@Project' ],
2020-08-08 18:50:04 +02:00
'activities' => [ 'result' => 'array' , 'type' => '@Activity' ],
2020-07-17 20:30:16 +02:00
];
2022-12-31 21:19:55 +01:00
// if the team is used inside the team context
2021-08-07 18:05:41 +02:00
case 'TeamMember' :
return [
'user' => [ 'result' => 'object' , 'type' => 'User' ],
'teamlead' => 'bool' ,
];
2022-12-31 21:19:55 +01:00
// if the team is used inside the user context
2021-08-07 18:05:41 +02:00
case 'TeamMembership' :
return [
'team' => [ 'result' => 'object' , 'type' => 'Team' ],
'teamlead' => 'bool' ,
];
2022-12-31 21:19:55 +01:00
// if a customer is embedded in other objects
2020-07-17 20:30:16 +02:00
case 'Customer' :
return [
'id' => 'int' ,
'name' => 'string' ,
'visible' => 'bool' ,
2022-03-18 22:31:50 +01:00
'billable' => 'bool' ,
2020-07-17 20:30:16 +02:00
'color' => '@string' ,
2021-10-13 18:31:28 +02:00
'number' => '@string' ,
2022-02-28 16:32:41 +01:00
'comment' => '@string' ,
2020-07-17 20:30:16 +02:00
];
2022-12-31 21:19:55 +01:00
// if a list of customers is loaded
2020-07-17 20:30:16 +02:00
case 'CustomerCollection' :
return [
'id' => 'int' ,
'name' => 'string' ,
'visible' => 'boolean' ,
2022-03-18 22:31:50 +01:00
'billable' => 'bool' ,
2020-07-17 20:30:16 +02:00
'color' => '@string' ,
2021-10-13 18:31:28 +02:00
'number' => '@string' ,
2022-02-28 16:32:41 +01:00
'comment' => '@string' ,
2020-07-17 20:30:16 +02:00
'metaFields' => [ 'result' => 'array' , 'type' => 'CustomerMeta' ],
'teams' => [ 'result' => 'array' , 'type' => 'Team' ],
'currency' => 'string' , // since 1.10
];
2022-12-31 21:19:55 +01:00
// if a customer is loaded explicitly
2020-07-17 20:30:16 +02:00
case 'CustomerEntity' :
return [
'id' => 'int' ,
'name' => 'string' ,
'visible' => 'bool' ,
2022-03-18 22:31:50 +01:00
'billable' => 'bool' ,
2020-07-17 20:30:16 +02:00
'color' => '@string' ,
'metaFields' => [ 'result' => 'array' , 'type' => 'CustomerMeta' ],
'teams' => [ 'result' => 'array' , 'type' => 'Team' ],
'homepage' => '@string' ,
'number' => '@string' ,
'comment' => '@string' ,
'company' => '@string' ,
'contact' => '@string' ,
'address' => '@string' ,
'country' => 'string' ,
'currency' => 'string' ,
'phone' => '@string' ,
'fax' => '@string' ,
'mobile' => '@string' ,
'email' => '@string' ,
'timezone' => 'string' ,
'budget' => 'float' ,
'timeBudget' => 'int' ,
'vatId' => '@string' , // since 1.10
2021-08-06 18:38:41 +02:00
'budgetType' => '@string' , // since 1.15
2020-07-17 20:30:16 +02:00
];
2022-12-31 21:19:55 +01:00
// if a project is embedded
2020-07-17 20:30:16 +02:00
case 'Project' :
return [
'id' => 'int' ,
'name' => 'string' ,
'visible' => 'bool' ,
2022-03-18 22:31:50 +01:00
'billable' => 'bool' ,
2020-07-17 20:30:16 +02:00
'color' => '@string' ,
'customer' => 'int' ,
2024-04-04 17:43:22 +02:00
'number' => '@int' ,
2022-10-01 18:14:22 +02:00
'globalActivities' => 'bool' ,
2022-02-28 16:32:41 +01:00
'comment' => '@string' ,
2020-07-17 20:30:16 +02:00
];
2022-12-31 21:19:55 +01:00
// if a project is embedded in an expanded collection (here timesheet)
2020-07-17 20:30:16 +02:00
case 'ProjectExpanded' :
return [
'id' => 'int' ,
'name' => 'string' ,
'visible' => 'bool' ,
2022-03-18 22:31:50 +01:00
'billable' => 'bool' ,
2020-07-17 20:30:16 +02:00
'color' => '@string' ,
'customer' => [ 'result' => 'object' , 'type' => 'Customer' ],
2024-04-04 17:43:22 +02:00
'number' => '@int' ,
2022-10-01 18:14:22 +02:00
'globalActivities' => 'bool' ,
2022-02-28 16:32:41 +01:00
'comment' => '@string' ,
2020-07-17 20:30:16 +02:00
];
2022-12-31 21:19:55 +01:00
// if a collection of projects is loaded
2020-07-17 20:30:16 +02:00
case 'ProjectCollection' :
return [
'id' => 'int' ,
'name' => 'string' ,
'visible' => 'bool' ,
2022-03-18 22:31:50 +01:00
'billable' => 'bool' ,
2020-07-17 20:30:16 +02:00
'customer' => 'int' ,
2024-04-04 17:43:22 +02:00
'number' => '@int' ,
2020-07-17 20:30:16 +02:00
'color' => '@string' ,
'metaFields' => [ 'result' => 'array' , 'type' => 'ProjectMeta' ],
'parentTitle' => 'string' ,
'start' => '@datetime' ,
'end' => '@datetime' ,
2022-10-01 18:14:22 +02:00
'globalActivities' => 'bool' ,
2020-07-17 20:30:16 +02:00
'teams' => [ 'result' => 'array' , 'type' => 'Team' ],
2022-02-28 16:32:41 +01:00
'comment' => '@string' ,
2020-07-17 20:30:16 +02:00
];
2022-12-31 21:19:55 +01:00
// if a project is explicitly loaded
2020-07-17 20:30:16 +02:00
case 'ProjectEntity' :
return [
'id' => 'int' ,
'name' => 'string' ,
'visible' => 'bool' ,
2022-03-18 22:31:50 +01:00
'billable' => 'bool' ,
2020-07-17 20:30:16 +02:00
'customer' => 'int' ,
2024-04-04 17:43:22 +02:00
'number' => '@int' ,
2020-07-17 20:30:16 +02:00
'color' => '@string' ,
'metaFields' => [ 'result' => 'array' , 'type' => 'ProjectMeta' ],
'parentTitle' => 'string' ,
2022-12-31 21:19:55 +01:00
'start' => '@date' ,
'end' => '@date' ,
2022-10-01 18:14:22 +02:00
'globalActivities' => 'bool' ,
2020-07-17 20:30:16 +02:00
'teams' => [ 'result' => 'array' , 'type' => 'Team' ],
'comment' => '@string' ,
'budget' => 'float' ,
'timeBudget' => 'int' ,
'orderNumber' => '@string' ,
2022-12-31 21:19:55 +01:00
'orderDate' => '@date' ,
2021-08-06 18:38:41 +02:00
'budgetType' => '@string' , // since 1.15
2020-07-17 20:30:16 +02:00
];
2022-12-31 21:19:55 +01:00
// embedded activities
2020-07-17 20:30:16 +02:00
case 'Activity' :
return [
'id' => 'int' ,
'name' => 'string' ,
'visible' => 'bool' ,
2022-03-18 22:31:50 +01:00
'billable' => 'bool' ,
2020-07-17 20:30:16 +02:00
'project' => '@int' ,
2024-04-04 17:43:22 +02:00
'number' => '@int' ,
2020-07-17 20:30:16 +02:00
'color' => '@string' ,
2022-02-28 16:32:41 +01:00
'comment' => '@string' ,
2020-07-17 20:30:16 +02:00
];
2020-09-17 01:13:48 +02:00
case 'ActivityExpanded' :
return [
'id' => 'int' ,
'name' => 'string' ,
'visible' => 'bool' ,
2022-03-18 22:31:50 +01:00
'billable' => 'bool' ,
2020-09-17 01:13:48 +02:00
'project' => [ 'result' => 'object' , 'type' => '@ProjectExpanded' ],
2024-04-04 17:43:22 +02:00
'number' => '@int' ,
2020-09-17 01:13:48 +02:00
'color' => '@string' ,
2022-02-28 16:32:41 +01:00
'comment' => '@string' ,
2020-09-17 01:13:48 +02:00
];
2022-12-31 21:19:55 +01:00
// collection of activities
2020-07-17 20:30:16 +02:00
case 'ActivityCollection' :
return [
'id' => 'int' ,
'name' => 'string' ,
'visible' => 'bool' ,
2022-03-18 22:31:50 +01:00
'billable' => 'bool' ,
2020-07-17 20:30:16 +02:00
'project' => '@int' ,
2024-04-04 17:43:22 +02:00
'number' => '@int' ,
2020-07-17 20:30:16 +02:00
'color' => '@string' ,
'metaFields' => [ 'result' => 'array' , 'type' => 'ProjectMeta' ],
'parentTitle' => '@string' ,
2022-02-28 16:32:41 +01:00
'comment' => '@string' ,
2020-08-08 18:50:04 +02:00
'teams' => [ 'result' => 'array' , 'type' => 'Team' ],
2020-07-17 20:30:16 +02:00
];
2022-12-31 21:19:55 +01:00
// if a activity is explicitly loaded
2020-07-17 20:30:16 +02:00
case 'ActivityEntity' :
return [
'id' => 'int' ,
'name' => 'string' ,
'visible' => 'bool' ,
2022-03-18 22:31:50 +01:00
'billable' => 'bool' ,
2020-07-17 20:30:16 +02:00
'project' => '@int' ,
2024-04-04 17:43:22 +02:00
'number' => '@int' ,
2020-07-17 20:30:16 +02:00
'color' => '@string' ,
'metaFields' => [ 'result' => 'array' , 'type' => 'ProjectMeta' ],
'parentTitle' => '@string' ,
'comment' => '@string' ,
'budget' => 'float' ,
'timeBudget' => 'int' ,
2020-08-08 18:50:04 +02:00
'teams' => [ 'result' => 'array' , 'type' => 'Team' ],
2021-08-06 18:38:41 +02:00
'budgetType' => '@string' , // since 1.15
2020-07-17 20:30:16 +02:00
];
case 'TimesheetEntity' :
return [
'id' => 'int' ,
'begin' => 'DateTime' ,
'end' => '@DateTime' ,
'duration' => '@int' ,
'description' => '@string' ,
'rate' => 'float' ,
'activity' => 'int' ,
'project' => 'int' ,
'tags' => [ 'result' => 'array' , 'type' => 'string' ],
'user' => 'int' ,
'metaFields' => [ 'result' => 'array' , 'type' => 'TimesheetMeta' ],
'internalRate' => 'float' ,
'exported' => 'bool' ,
2021-04-18 21:51:27 +02:00
'billable' => 'bool' ,
2020-07-17 20:30:16 +02:00
'fixedRate' => '@float' ,
'hourlyRate' => '@float' ,
2022-12-31 21:19:55 +01:00
// TODO new fields: category
2020-07-17 20:30:16 +02:00
];
2022-12-31 21:19:55 +01:00
case 'TimesheetExpanded' :
2020-09-17 01:13:48 +02:00
return [
'id' => 'int' ,
'begin' => 'DateTime' ,
'end' => '@DateTime' ,
'duration' => '@int' ,
'description' => '@string' ,
'rate' => 'float' ,
'activity' => [ 'result' => 'object' , 'type' => 'ActivityExpanded' ],
'project' => [ 'result' => 'object' , 'type' => 'ProjectExpanded' ],
'tags' => [ 'result' => 'array' , 'type' => 'string' ],
2022-12-31 21:19:55 +01:00
'user' => [ 'result' => 'object' , 'type' => 'User' ],
2020-09-17 01:13:48 +02:00
'metaFields' => [ 'result' => 'array' , 'type' => 'TimesheetMeta' ],
'internalRate' => 'float' ,
'exported' => 'bool' ,
2021-04-18 21:51:27 +02:00
'billable' => 'bool' ,
2020-09-17 01:13:48 +02:00
'fixedRate' => '@float' ,
'hourlyRate' => '@float' ,
2022-12-31 21:19:55 +01:00
// TODO new fields: category
2020-09-17 01:13:48 +02:00
];
2020-07-17 20:30:16 +02:00
case 'TimesheetCollection' :
return [
'id' => 'int' ,
'begin' => 'DateTime' ,
'end' => '@DateTime' ,
'duration' => '@int' ,
'description' => '@string' ,
'rate' => 'float' ,
'activity' => 'int' ,
'project' => 'int' ,
'tags' => [ 'result' => 'array' , 'type' => 'string' ],
'user' => 'int' ,
'metaFields' => [ 'result' => 'array' , 'type' => 'TimesheetMeta' ],
'internalRate' => 'float' ,
2021-04-18 21:51:27 +02:00
'exported' => 'bool' ,
'billable' => 'bool' ,
2020-07-17 20:30:16 +02:00
];
case 'TimesheetCollectionFull' :
return [
'id' => 'int' ,
'begin' => 'DateTime' ,
'end' => '@DateTime' ,
'duration' => '@int' ,
'description' => '@string' ,
'rate' => 'float' ,
'activity' => [ 'result' => 'object' , 'type' => 'Activity' ],
'project' => [ 'result' => 'object' , 'type' => 'ProjectExpanded' ],
'tags' => [ 'result' => 'array' , 'type' => 'string' ],
2022-12-31 21:19:55 +01:00
'user' => [ 'result' => 'object' , 'type' => 'User' ],
2020-07-17 20:30:16 +02:00
'metaFields' => [ 'result' => 'array' , 'type' => 'TimesheetMeta' ],
'internalRate' => 'float' ,
2021-04-18 21:51:27 +02:00
'exported' => 'bool' ,
'billable' => 'bool' ,
2020-07-17 20:30:16 +02:00
];
default :
2024-08-04 18:22:27 +02:00
throw new \Exception ( \sprintf ( 'Unknown API response type: %s' , $type ));
2020-07-17 20:30:16 +02:00
}
}
/**
* The $type is either one of the types configured in config / packages / nelmio_api_doc . yaml or the class name .
*
* @ param string $type
* @ param array $result
* @ throws \Exception
*/
2022-12-31 21:19:55 +01:00
protected function assertApiResponseTypeStructure ( string $type , array $result ) : void
2020-07-17 20:30:16 +02:00
{
$expected = self :: getExpectedResponseStructure ( $type );
$expectedKeys = array_keys ( $expected );
$actual = array_keys ( $result );
sort ( $actual );
sort ( $expectedKeys );
2024-08-04 18:22:27 +02:00
self :: assertEquals ( $expectedKeys , $actual , \sprintf ( 'Structure for API response type "%s" does not match' , $type ));
2020-07-17 20:30:16 +02:00
self :: assertEquals (
\count ( $actual ),
\count ( $expectedKeys ),
2024-08-04 18:22:27 +02:00
\sprintf ( 'Mismatch between expected and result keys for API response type "%s". Expected %s keys but found %s.' , $type , \count ( $expected ), \count ( $actual ))
2020-07-17 20:30:16 +02:00
);
foreach ( $expected as $key => $value ) {
if ( \is_array ( $value )) {
switch ( $value [ 'result' ]) {
case 'array' :
foreach ( $result [ $key ] as $subResult ) {
if ( $value [ 'type' ] === 'string' ) {
self :: assertIsString ( $subResult );
} else {
self :: assertIsArray ( $subResult );
if ( $value [ 'type' ][ 0 ] === '@' ) {
if ( empty ( $result [ $key ])) {
continue ;
}
$value [ 'type' ] = substr ( $value [ 'type' ], 1 );
}
self :: assertApiResponseTypeStructure ( $value [ 'type' ], $subResult );
}
}
break ;
case 'object' :
if ( $value [ 'type' ][ 0 ] === '@' ) {
if ( empty ( $result [ $key ])) {
break ;
}
$value [ 'type' ] = substr ( $value [ 'type' ], 1 );
}
2024-08-04 18:22:27 +02:00
self :: assertIsArray ( $result [ $key ], \sprintf ( 'Key "%s" in type "%s" is not an array' , $key , $type ));
2020-09-17 01:13:48 +02:00
2020-07-17 20:30:16 +02:00
self :: assertApiResponseTypeStructure ( $value [ 'type' ], $result [ $key ]);
break ;
default :
2024-08-04 18:22:27 +02:00
throw new \Exception ( \sprintf ( 'Invalid result type "%s" for subresource given' , $value [ 'result' ]));
2020-07-17 20:30:16 +02:00
}
continue ;
}
if ( $value [ 0 ] === '@' ) {
if ( \is_null ( $result [ $key ])) {
continue ;
}
$value = substr ( $value , 1 );
}
if ( strtolower ( $value ) === 'datetime' ) {
2022-12-31 21:19:55 +01:00
$date = \DateTime :: createFromFormat ( 'Y-m-d\TH:i:sO' , $result [ $key ]);
2024-08-04 18:22:27 +02:00
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 ]));
2022-12-31 21:19:55 +01:00
$value = 'string' ;
} elseif ( strtolower ( $value ) === 'date' ) {
$date = \DateTime :: createFromFormat ( 'Y-m-d' , $result [ $key ]);
2024-08-04 18:22:27 +02:00
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 ]));
2020-07-17 20:30:16 +02:00
$value = 'string' ;
}
static :: assertThat (
$result [ $key ],
new IsType ( $value ),
2024-08-04 18:22:27 +02:00
\sprintf ( 'Found type mismatch in structure for API response type %s. Expected type "%s" for key "%s".' , $type , $value , $key )
2020-07-17 20:30:16 +02:00
);
2018-11-18 19:43:58 +01:00
}
}
2018-08-08 23:10:45 +02:00
}