new: add /api openapi spec view with redoc, add faker to fixtures, validate api responses with openapi spec, add /api/v1/ prefix to api routes
parent
f45727704f
commit
a69608530c
|
@ -8,4 +8,4 @@ webroot/theme/node_modules
|
||||||
.vscode
|
.vscode
|
||||||
docker/run/
|
docker/run/
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
config.json
|
config.json
|
||||||
|
|
|
@ -19,7 +19,9 @@
|
||||||
"cakephp/bake": "^2.0.3",
|
"cakephp/bake": "^2.0.3",
|
||||||
"cakephp/cakephp-codesniffer": "~4.0.0",
|
"cakephp/cakephp-codesniffer": "~4.0.0",
|
||||||
"cakephp/debug_kit": "^4.0",
|
"cakephp/debug_kit": "^4.0",
|
||||||
|
"fzaninotto/faker": "^1.9",
|
||||||
"josegonzalez/dotenv": "^3.2",
|
"josegonzalez/dotenv": "^3.2",
|
||||||
|
"league/openapi-psr7-validator": "^0.16.4",
|
||||||
"phpunit/phpunit": "^8.5",
|
"phpunit/phpunit": "^8.5",
|
||||||
"psy/psysh": "@stable"
|
"psy/psysh": "@stable"
|
||||||
},
|
},
|
||||||
|
@ -44,7 +46,6 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"post-install-cmd": "App\\Console\\Installer::postInstall",
|
"post-install-cmd": "App\\Console\\Installer::postInstall",
|
||||||
"post-create-project-cmd": "App\\Console\\Installer::postInstall",
|
"post-create-project-cmd": "App\\Console\\Installer::postInstall",
|
||||||
"post-autoload-dump": "Cake\\Composer\\Installer\\PluginInstaller::postAutoloadDump",
|
|
||||||
"check": [
|
"check": [
|
||||||
"@test",
|
"@test",
|
||||||
"@cs-check"
|
"@cs-check"
|
||||||
|
|
|
@ -92,14 +92,14 @@ $routes->prefix('Open', function (RouteBuilder $routes) {
|
||||||
$routes->fallbacks(DashedRoute::class);
|
$routes->fallbacks(DashedRoute::class);
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
// API routes
|
||||||
* If you need a different set of middleware or none at all,
|
$routes->scope('/api', function (RouteBuilder $routes) {
|
||||||
* open new scope and define routes there.
|
// $routes->applyMiddleware('ratelimit', 'auth.api');
|
||||||
*
|
$routes->scope('/v1', function (RouteBuilder $routes) {
|
||||||
* ```
|
// $routes->applyMiddleware('v1compat');
|
||||||
* $routes->scope('/api', function (RouteBuilder $builder) {
|
$routes->setExtensions(['json']);
|
||||||
* // No $builder->applyMiddleware() here.
|
|
||||||
* // Connect API actions here.
|
// Generic API route
|
||||||
* });
|
$routes->connect('/{controller}/{action}/*');
|
||||||
* ```
|
});
|
||||||
*/
|
});
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Controller\AppController;
|
||||||
|
|
||||||
|
class ApiController extends AppController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Controller action for displaying built-in Redoc UI
|
||||||
|
*
|
||||||
|
* @return \Cake\Http\Response|null|void Renders view
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$url = '/docs/openapi.yaml';
|
||||||
|
$this->set('url', $url);
|
||||||
|
}
|
||||||
|
}
|
|
@ -193,6 +193,9 @@ class ACLComponent extends Component
|
||||||
'getBookmarks' => ['*'],
|
'getBookmarks' => ['*'],
|
||||||
'saveBookmark' => ['*'],
|
'saveBookmark' => ['*'],
|
||||||
'deleteBookmark' => ['*']
|
'deleteBookmark' => ['*']
|
||||||
|
],
|
||||||
|
'Api' => [
|
||||||
|
'index' => ['*']
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
<redoc spec-url='<?php echo $url ?>'></redoc>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
|
|
@ -11,24 +11,60 @@ class AuthKeysFixture extends TestFixture
|
||||||
{
|
{
|
||||||
public $connection = 'test';
|
public $connection = 'test';
|
||||||
|
|
||||||
public const ADMIN_API_KEY = '4cd687b314a3b9c4d83264e6195b9a3706ef4c2f';
|
public const ADMIN_API_KEY = 'd033e22ae348aeb5660fc2140aec35850c4da997';
|
||||||
|
public const SYNC_API_KEY = '6b387ced110858dcbcda36edb044dc18f91a0894';
|
||||||
|
public const ORG_ADMIN_API_KEY = '1c4685d281d478dbcebd494158024bc3539004d0';
|
||||||
|
public const USER_API_KEY = '12dea96fec20593566ab75692c9949596833adc9';
|
||||||
|
|
||||||
public function init(): void
|
public function init(): void
|
||||||
{
|
{
|
||||||
$hasher = new DefaultPasswordHasher();
|
$hasher = new DefaultPasswordHasher();
|
||||||
|
$faker = \Faker\Factory::create();
|
||||||
|
|
||||||
$this->records = [
|
$this->records = [
|
||||||
[
|
[
|
||||||
'id' => 1,
|
'uuid' => $faker->uuid(),
|
||||||
'uuid' => '3ebfbe50-e7d2-406e-a092-f031e604b6e5',
|
|
||||||
'authkey' => $hasher->hash(self::ADMIN_API_KEY),
|
'authkey' => $hasher->hash(self::ADMIN_API_KEY),
|
||||||
'authkey_start' => '4cd6',
|
'authkey_start' => substr(self::ADMIN_API_KEY, 0, 4),
|
||||||
'authkey_end' => '4c2f',
|
'authkey_end' => substr(self::ADMIN_API_KEY, -4),
|
||||||
'expiration' => 0,
|
'expiration' => 0,
|
||||||
'user_id' => 1,
|
'user_id' => UsersFixture::USER_ADMIN_ID,
|
||||||
'comment' => '',
|
'comment' => '',
|
||||||
'created' => time(),
|
'created' => $faker->dateTime()->getTimestamp(),
|
||||||
'modified' => time()
|
'modified' => $faker->dateTime()->getTimestamp()
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'uuid' => $faker->uuid(),
|
||||||
|
'authkey' => $hasher->hash(self::SYNC_API_KEY),
|
||||||
|
'authkey_start' => substr(self::SYNC_API_KEY, 0, 4),
|
||||||
|
'authkey_end' => substr(self::SYNC_API_KEY, -4),
|
||||||
|
'expiration' => 0,
|
||||||
|
'user_id' => UsersFixture::USER_SYNC_ID,
|
||||||
|
'comment' => '',
|
||||||
|
'created' => $faker->dateTime()->getTimestamp(),
|
||||||
|
'modified' => $faker->dateTime()->getTimestamp()
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'uuid' => $faker->uuid(),
|
||||||
|
'authkey' => $hasher->hash(self::ORG_ADMIN_API_KEY),
|
||||||
|
'authkey_start' => substr(self::ORG_ADMIN_API_KEY, 0, 4),
|
||||||
|
'authkey_end' => substr(self::ORG_ADMIN_API_KEY, -4),
|
||||||
|
'expiration' => 0,
|
||||||
|
'user_id' => UsersFixture::USER_ORG_ADMIN_ID,
|
||||||
|
'comment' => '',
|
||||||
|
'created' => $faker->dateTime()->getTimestamp(),
|
||||||
|
'modified' => $faker->dateTime()->getTimestamp()
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'uuid' => $faker->uuid(),
|
||||||
|
'authkey' => $hasher->hash(self::USER_API_KEY),
|
||||||
|
'authkey_start' => substr(self::USER_API_KEY, 0, 4),
|
||||||
|
'authkey_end' => substr(self::USER_API_KEY, -4),
|
||||||
|
'expiration' => 0,
|
||||||
|
'user_id' => UsersFixture::USER_REGULAR_USER_ID,
|
||||||
|
'comment' => '',
|
||||||
|
'created' => $faker->dateTime()->getTimestamp(),
|
||||||
|
'modified' => $faker->dateTime()->getTimestamp()
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
parent::init();
|
parent::init();
|
||||||
|
|
|
@ -10,16 +10,64 @@ class IndividualsFixture extends TestFixture
|
||||||
{
|
{
|
||||||
public $connection = 'test';
|
public $connection = 'test';
|
||||||
|
|
||||||
public $records = [
|
// Admin individual
|
||||||
[
|
public const INDIVIDUAL_ADMIN_ID = 1;
|
||||||
'id' => 1,
|
|
||||||
'uuid' => '3ebfbe50-e7d2-406e-a092-f031e604b6e1',
|
// Sync individual
|
||||||
'email' => 'admin@admin.test',
|
public const INDIVIDUAL_SYNC_ID = 2;
|
||||||
'first_name' => 'admin',
|
|
||||||
'last_name' => 'admin',
|
// Org Admin individual
|
||||||
'position' => 'admin',
|
public const INDIVIDUAL_ORG_ADMIN_ID = 3;
|
||||||
'created' => '2022-01-04 10:00:00',
|
|
||||||
'modified' => '2022-01-04 10:00:00'
|
// Regular User individual
|
||||||
]
|
public const INDIVIDUAL_REGULAR_USER_ID = 4;
|
||||||
];
|
|
||||||
|
public function init(): void
|
||||||
|
{
|
||||||
|
$faker = \Faker\Factory::create();
|
||||||
|
|
||||||
|
$this->records = [
|
||||||
|
[
|
||||||
|
'id' => self::INDIVIDUAL_ADMIN_ID,
|
||||||
|
'uuid' => $faker->uuid(),
|
||||||
|
'email' => $faker->email(),
|
||||||
|
'first_name' => $faker->firstName,
|
||||||
|
'last_name' => $faker->lastName,
|
||||||
|
'position' => 'admin',
|
||||||
|
'created' => $faker->dateTime()->getTimestamp(),
|
||||||
|
'modified' => $faker->dateTime()->getTimestamp()
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => self::INDIVIDUAL_SYNC_ID,
|
||||||
|
'uuid' => $faker->uuid(),
|
||||||
|
'email' => $faker->email(),
|
||||||
|
'first_name' => $faker->firstName,
|
||||||
|
'last_name' => $faker->lastName,
|
||||||
|
'position' => 'sync',
|
||||||
|
'created' => $faker->dateTime()->getTimestamp(),
|
||||||
|
'modified' => $faker->dateTime()->getTimestamp()
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => self::INDIVIDUAL_ORG_ADMIN_ID,
|
||||||
|
'uuid' => $faker->uuid(),
|
||||||
|
'email' => $faker->email(),
|
||||||
|
'first_name' => $faker->firstName,
|
||||||
|
'last_name' => $faker->lastName,
|
||||||
|
'position' => 'org_admin',
|
||||||
|
'created' => $faker->dateTime()->getTimestamp(),
|
||||||
|
'modified' => $faker->dateTime()->getTimestamp()
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => self::INDIVIDUAL_REGULAR_USER_ID,
|
||||||
|
'uuid' => $faker->uuid(),
|
||||||
|
'email' => $faker->email(),
|
||||||
|
'first_name' => $faker->firstName,
|
||||||
|
'last_name' => $faker->lastName,
|
||||||
|
'position' => 'user',
|
||||||
|
'created' => $faker->dateTime()->getTimestamp(),
|
||||||
|
'modified' => $faker->dateTime()->getTimestamp()
|
||||||
|
]
|
||||||
|
];
|
||||||
|
parent::init();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Test\Fixture;
|
||||||
|
|
||||||
|
use Cake\TestSuite\Fixture\TestFixture;
|
||||||
|
use Authentication\PasswordHasher\DefaultPasswordHasher;
|
||||||
|
|
||||||
|
class OrganisationsFixture extends TestFixture
|
||||||
|
{
|
||||||
|
public $connection = 'test';
|
||||||
|
|
||||||
|
// Organisation A
|
||||||
|
public const ORGANISATION_A_ID = 1;
|
||||||
|
|
||||||
|
// Organisation B
|
||||||
|
public const ORGANISATION_B_ID = 2;
|
||||||
|
|
||||||
|
public function init(): void
|
||||||
|
{
|
||||||
|
$faker = \Faker\Factory::create();
|
||||||
|
|
||||||
|
$this->records = [
|
||||||
|
[
|
||||||
|
'id' => self::ORGANISATION_A_ID,
|
||||||
|
'uuid' => $faker->uuid(),
|
||||||
|
'name' => 'Organisation A',
|
||||||
|
'url' => $faker->url,
|
||||||
|
'nationality' => $faker->countryCode,
|
||||||
|
'sector' => 'IT',
|
||||||
|
'type' => '',
|
||||||
|
'contacts' => '',
|
||||||
|
'created' => $faker->dateTime()->getTimestamp(),
|
||||||
|
'modified' => $faker->dateTime()->getTimestamp()
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => self::ORGANISATION_B_ID,
|
||||||
|
'uuid' => $faker->uuid(),
|
||||||
|
'name' => 'Organisation B',
|
||||||
|
'url' => $faker->url,
|
||||||
|
'nationality' => $faker->countryCode,
|
||||||
|
'sector' => 'IT',
|
||||||
|
'type' => '',
|
||||||
|
'contacts' => '',
|
||||||
|
'created' => $faker->dateTime()->getTimestamp(),
|
||||||
|
'modified' => $faker->dateTime()->getTimestamp()
|
||||||
|
]
|
||||||
|
];
|
||||||
|
parent::init();
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,15 +10,53 @@ class RolesFixture extends TestFixture
|
||||||
{
|
{
|
||||||
public $connection = 'test';
|
public $connection = 'test';
|
||||||
|
|
||||||
public $records = [
|
public const ROLE_ADMIN_ID = 1;
|
||||||
[
|
public const ROLE_SYNC_ID = 2;
|
||||||
'id' => 1,
|
public const ROLE_ORG_ADMIN_ID = 3;
|
||||||
'uuid' => '3ebfbe50-e7d2-406e-a092-f031e604b6e4',
|
public const ROLE_REGULAR_USER_ID = 4;
|
||||||
'name' => 'admin',
|
|
||||||
'is_default' => true,
|
public function init(): void
|
||||||
'perm_admin' => true,
|
{
|
||||||
'perm_sync' => true,
|
$faker = \Faker\Factory::create();
|
||||||
'perm_org_admin' => true
|
|
||||||
]
|
$this->records = [
|
||||||
];
|
[
|
||||||
|
'id' => self::ROLE_ADMIN_ID,
|
||||||
|
'uuid' => $faker->uuid(),
|
||||||
|
'name' => 'admin',
|
||||||
|
'is_default' => false,
|
||||||
|
'perm_admin' => true,
|
||||||
|
'perm_sync' => false,
|
||||||
|
'perm_org_admin' => false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => self::ROLE_SYNC_ID,
|
||||||
|
'uuid' => $faker->uuid(),
|
||||||
|
'name' => 'sync',
|
||||||
|
'is_default' => false,
|
||||||
|
'perm_admin' => false,
|
||||||
|
'perm_sync' => true,
|
||||||
|
'perm_org_admin' => false
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => self::ROLE_ORG_ADMIN_ID,
|
||||||
|
'uuid' => $faker->uuid(),
|
||||||
|
'name' => 'org_admin',
|
||||||
|
'is_default' => false,
|
||||||
|
'perm_admin' => false,
|
||||||
|
'perm_sync' => false,
|
||||||
|
'perm_org_admin' => true
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => self::ROLE_REGULAR_USER_ID,
|
||||||
|
'uuid' => $faker->uuid(),
|
||||||
|
'name' => 'user',
|
||||||
|
'is_default' => true,
|
||||||
|
'perm_admin' => false,
|
||||||
|
'perm_sync' => false,
|
||||||
|
'perm_org_admin' => false
|
||||||
|
]
|
||||||
|
];
|
||||||
|
parent::init();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,26 +11,80 @@ class UsersFixture extends TestFixture
|
||||||
{
|
{
|
||||||
public $connection = 'test';
|
public $connection = 'test';
|
||||||
|
|
||||||
public const ADMIN_USER = 'admin';
|
// Admin user
|
||||||
public const ADMIN_PASSWORD = 'Password1234';
|
public const USER_ADMIN_ID = 1;
|
||||||
|
public const USER_ADMIN_USERNAME = 'admin';
|
||||||
|
public const USER_ADMIN_PASSWORD = 'AdminPassword';
|
||||||
|
|
||||||
|
// Sync user
|
||||||
|
public const USER_SYNC_ID = 2;
|
||||||
|
public const USER_SYNC_USERNAME = 'sync';
|
||||||
|
public const USER_SYNC_PASSWORD = 'SyncPassword';
|
||||||
|
|
||||||
|
// Org Admin user
|
||||||
|
public const USER_ORG_ADMIN_ID = 3;
|
||||||
|
public const USER_ORG_ADMIN_USERNAME = 'org_admin';
|
||||||
|
public const USER_ORG_ADMIN_PASSWORD = 'OrgAdminPassword';
|
||||||
|
|
||||||
|
// Regular User user
|
||||||
|
public const USER_REGULAR_USER_ID = 4;
|
||||||
|
public const USER_REGULAR_USER_USERNAME = 'user';
|
||||||
|
public const USER_REGULAR_USER_PASSWORD = 'UserPassword';
|
||||||
|
|
||||||
|
|
||||||
public function init(): void
|
public function init(): void
|
||||||
{
|
{
|
||||||
$hasher = new DefaultPasswordHasher();
|
$hasher = new DefaultPasswordHasher();
|
||||||
|
$faker = \Faker\Factory::create();
|
||||||
|
|
||||||
$this->records = [
|
$this->records = [
|
||||||
[
|
[
|
||||||
'id' => 1,
|
'id' => self::USER_ADMIN_ID,
|
||||||
'uuid' => '3ebfbe50-e7d2-406e-a092-f031e604b6e5',
|
'uuid' => $faker->uuid(),
|
||||||
'username' => self::ADMIN_USER,
|
'username' => self::USER_ADMIN_USERNAME,
|
||||||
'password' => $hasher->hash(self::ADMIN_PASSWORD),
|
'password' => $hasher->hash(self::USER_ADMIN_PASSWORD),
|
||||||
'role_id' => 1,
|
'role_id' => RolesFixture::ROLE_ADMIN_ID,
|
||||||
'individual_id' => 1,
|
'individual_id' => IndividualsFixture::INDIVIDUAL_ADMIN_ID,
|
||||||
'disabled' => 0,
|
'disabled' => 0,
|
||||||
'organisation_id' => 1,
|
'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID,
|
||||||
'created' => '2022-01-04 10:00:00',
|
'created' => $faker->dateTime()->getTimestamp(),
|
||||||
'modified' => '2022-01-04 10:00:00'
|
'modified' => $faker->dateTime()->getTimestamp()
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => self::USER_SYNC_ID,
|
||||||
|
'uuid' => $faker->uuid(),
|
||||||
|
'username' => self::USER_SYNC_USERNAME,
|
||||||
|
'password' => $hasher->hash(self::USER_SYNC_PASSWORD),
|
||||||
|
'role_id' => RolesFixture::ROLE_SYNC_ID,
|
||||||
|
'individual_id' => IndividualsFixture::INDIVIDUAL_SYNC_ID,
|
||||||
|
'disabled' => 0,
|
||||||
|
'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID,
|
||||||
|
'created' => $faker->dateTime()->getTimestamp(),
|
||||||
|
'modified' => $faker->dateTime()->getTimestamp()
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => self::USER_ORG_ADMIN_ID,
|
||||||
|
'uuid' => $faker->uuid(),
|
||||||
|
'username' => self::USER_ORG_ADMIN_USERNAME,
|
||||||
|
'password' => $hasher->hash(self::USER_ORG_ADMIN_PASSWORD),
|
||||||
|
'role_id' => RolesFixture::ROLE_ORG_ADMIN_ID,
|
||||||
|
'individual_id' => IndividualsFixture::INDIVIDUAL_ORG_ADMIN_ID,
|
||||||
|
'disabled' => 0,
|
||||||
|
'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID,
|
||||||
|
'created' => $faker->dateTime()->getTimestamp(),
|
||||||
|
'modified' => $faker->dateTime()->getTimestamp()
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => self::USER_REGULAR_USER_ID,
|
||||||
|
'uuid' => $faker->uuid(),
|
||||||
|
'username' => self::USER_REGULAR_USER_USERNAME,
|
||||||
|
'password' => $hasher->hash(self::USER_REGULAR_USER_PASSWORD),
|
||||||
|
'role_id' => RolesFixture::ROLE_REGULAR_USER_ID,
|
||||||
|
'individual_id' => IndividualsFixture::INDIVIDUAL_REGULAR_USER_ID,
|
||||||
|
'disabled' => 0,
|
||||||
|
'organisation_id' => OrganisationsFixture::ORGANISATION_A_ID,
|
||||||
|
'created' => $faker->dateTime()->getTimestamp(),
|
||||||
|
'modified' => $faker->dateTime()->getTimestamp()
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
parent::init();
|
parent::init();
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Test\Helper;
|
||||||
|
|
||||||
|
use \League\OpenAPIValidation\PSR7\ValidatorBuilder;
|
||||||
|
use \League\OpenAPIValidation\PSR7\RequestValidator;
|
||||||
|
use \League\OpenAPIValidation\PSR7\ResponseValidator;
|
||||||
|
use \League\OpenAPIValidation\PSR7\OperationAddress;
|
||||||
|
|
||||||
|
trait ApiTestTrait
|
||||||
|
{
|
||||||
|
/** @var string */
|
||||||
|
protected $_authToken = '';
|
||||||
|
|
||||||
|
/** @var ValidatorBuilder */
|
||||||
|
private $validator;
|
||||||
|
|
||||||
|
/** @var RequestValidator */
|
||||||
|
private $requestValidator;
|
||||||
|
|
||||||
|
/** @var ResponseValidator */
|
||||||
|
private $responseValidator;
|
||||||
|
|
||||||
|
public function setAuthToken(string $authToken): void
|
||||||
|
{
|
||||||
|
$this->_authToken = $authToken;
|
||||||
|
|
||||||
|
// somehow this is not set automatically in test environment
|
||||||
|
$_SERVER['HTTP_AUTHORIZATION'] = $authToken;
|
||||||
|
|
||||||
|
$this->configRequest([
|
||||||
|
'headers' => [
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
'Authorization' => $this->_authToken
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the OpenAPI specification and create a validator
|
||||||
|
*
|
||||||
|
* @param string $specFile
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function initializeValidator(string $specFile): void
|
||||||
|
{
|
||||||
|
$this->validator = (new ValidatorBuilder)->fromYamlFile($specFile);
|
||||||
|
$this->requestValidator = $this->validator->getRequestValidator();
|
||||||
|
$this->responseValidator = $this->validator->getResponseValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the API request against the OpenAPI spec
|
||||||
|
*
|
||||||
|
* @param string $path The path to the API endpoint
|
||||||
|
* @param string $method The HTTP method used to call the endpoint
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function validateRequest(string $endpoint, string $method = 'get'): void
|
||||||
|
{
|
||||||
|
// TODO: find a workaround to create a PSR-7 request object for validation
|
||||||
|
throw NotImplementedException("Unfortunately cakephp does not save the PSR-7 request object in the test context");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the API response against the OpenAPI spec
|
||||||
|
*
|
||||||
|
* @param string $path The path to the API endpoint
|
||||||
|
* @param string $method The HTTP method used to call the endpoint
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function validateResponse(string $endpoint, string $method = 'get'): void
|
||||||
|
{
|
||||||
|
$address = new OperationAddress($endpoint, $method);
|
||||||
|
$this->responseValidator->validate($address, $this->_response);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,14 @@
|
||||||
# Testing
|
# Testing
|
||||||
Add a test database to your `config/app_local.php` config file and set `debug` mode to `true`.
|
|
||||||
|
1. Add a `cerebrate_test` database to the db:
|
||||||
|
```mysql
|
||||||
|
CREATE DATABASE cerebrate_test;
|
||||||
|
GRANT ALL PRIVILEGES ON cerebrate_test.* to cerebrate@localhost;
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
QUIT;
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add a the test database to your `config/app_local.php` config file and set `debug` mode to `true`.
|
||||||
```php
|
```php
|
||||||
'debug' => true,
|
'debug' => true,
|
||||||
'Datasources' => [
|
'Datasources' => [
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Test\TestCase\Api\Users;
|
||||||
|
|
||||||
|
use Cake\TestSuite\IntegrationTestTrait;
|
||||||
|
use Cake\TestSuite\TestCase;
|
||||||
|
use App\Test\Fixture\AuthKeysFixture;
|
||||||
|
use App\Test\Fixture\UsersFixture;
|
||||||
|
use App\Test\Helper\ApiTestTrait;
|
||||||
|
|
||||||
|
class UsersApiTest extends TestCase
|
||||||
|
{
|
||||||
|
use IntegrationTestTrait;
|
||||||
|
use ApiTestTrait;
|
||||||
|
|
||||||
|
protected const ENDPOINT = '/api/v1/users/view';
|
||||||
|
|
||||||
|
protected $fixtures = [
|
||||||
|
'app.Individuals',
|
||||||
|
'app.Roles',
|
||||||
|
'app.Users',
|
||||||
|
'app.AuthKeys'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->initializeValidator(APP . '../webroot/docs/openapi.yaml');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testViewMe(): void
|
||||||
|
{
|
||||||
|
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||||
|
$this->get(self::ENDPOINT);
|
||||||
|
|
||||||
|
$this->assertResponseOk();
|
||||||
|
$this->assertResponseContains(sprintf('"username": "%s"', UsersFixture::USER_ADMIN_USERNAME));
|
||||||
|
// TODO: $this->validateRequest()
|
||||||
|
$this->validateResponse(self::ENDPOINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testViewById(): void
|
||||||
|
{
|
||||||
|
$this->setAuthToken(AuthKeysFixture::ADMIN_API_KEY);
|
||||||
|
$url = sprintf('%s/%d', self::ENDPOINT, UsersFixture::USER_ADMIN_ID);
|
||||||
|
$this->get($url);
|
||||||
|
|
||||||
|
$this->assertResponseOk();
|
||||||
|
$this->assertResponseContains(sprintf('"username": "%s"', UsersFixture::USER_ADMIN_USERNAME));
|
||||||
|
// TODO: $this->validateRequest()
|
||||||
|
$this->validateResponse($url);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,40 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Test\TestCase\Api;
|
|
||||||
|
|
||||||
use Cake\TestSuite\IntegrationTestTrait;
|
|
||||||
use Cake\TestSuite\TestCase;
|
|
||||||
use App\Test\Fixture\AuthKeysFixture;
|
|
||||||
use App\Test\Fixture\UsersFixture;
|
|
||||||
|
|
||||||
class UsersApiTest extends TestCase
|
|
||||||
{
|
|
||||||
use IntegrationTestTrait;
|
|
||||||
|
|
||||||
protected $fixtures = [
|
|
||||||
'app.Individuals',
|
|
||||||
'app.Roles',
|
|
||||||
'app.Users',
|
|
||||||
'app.AuthKeys'
|
|
||||||
];
|
|
||||||
|
|
||||||
public function testViewMe(): void
|
|
||||||
{
|
|
||||||
// ugly hack, $_SERVER['HTTP_AUTHORIZATION'] is not set automatically in test environment
|
|
||||||
$_SERVER['HTTP_AUTHORIZATION'] = AuthKeysFixture::ADMIN_API_KEY;
|
|
||||||
$this->configRequest([
|
|
||||||
'headers' => [
|
|
||||||
// this does not work: https://book.cakephp.org/4/en/development/testing.html#testing-stateless-authentication-and-apis
|
|
||||||
// 'Authorization' => AuthKeysFixture::ADMIN_API_KEY,
|
|
||||||
'Accept' => 'application/json'
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->get('/users/view');
|
|
||||||
|
|
||||||
$this->assertResponseOk();
|
|
||||||
$this->assertResponseContains(sprintf('"username": "%s"', UsersFixture::ADMIN_USER));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Test\TestCase\Controller;
|
namespace App\Test\TestCase\Controller\Users;
|
||||||
|
|
||||||
use Cake\TestSuite\IntegrationTestTrait;
|
use Cake\TestSuite\IntegrationTestTrait;
|
||||||
use Cake\TestSuite\TestCase;
|
use Cake\TestSuite\TestCase;
|
|
@ -66,4 +66,6 @@ if (!in_array('skip-migrations', $_SERVER['argv'])) {
|
||||||
['plugin' => 'Tags', 'connection' => 'test', 'skip' => ['*']],
|
['plugin' => 'Tags', 'connection' => 'test', 'skip' => ['*']],
|
||||||
['plugin' => 'ADmad/SocialAuth', 'connection' => 'test', 'skip' => ['*']]
|
['plugin' => 'ADmad/SocialAuth', 'connection' => 'test', 'skip' => ['*']]
|
||||||
]);
|
]);
|
||||||
|
}else{
|
||||||
|
echo "[ * ] Skipping migrations ...\n";
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,215 @@
|
||||||
|
openapi: 3.0.0
|
||||||
|
info:
|
||||||
|
version: 1.3.0
|
||||||
|
title: Cerebrate Project API
|
||||||
|
description: |
|
||||||
|
|
||||||
|
TODO: markdown description
|
||||||
|
|
||||||
|
servers:
|
||||||
|
- url: https://cerebrate.local
|
||||||
|
|
||||||
|
tags:
|
||||||
|
- name: Users
|
||||||
|
description: "TODO: users resource descriptions"
|
||||||
|
|
||||||
|
paths:
|
||||||
|
/api/v1/users/view:
|
||||||
|
get:
|
||||||
|
summary: "Get information about the current user"
|
||||||
|
operationId: viewUserMe
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/ViewUserResponse"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/UnauthorizedApiErrorResponse"
|
||||||
|
default:
|
||||||
|
$ref: "#/components/responses/ApiErrorResponse"
|
||||||
|
|
||||||
|
/api/v1/users/view/{userId}:
|
||||||
|
get:
|
||||||
|
summary: "Get information of a user by id"
|
||||||
|
operationId: viewUserById
|
||||||
|
tags:
|
||||||
|
- Users
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/userId"
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
$ref: "#/components/responses/ViewUserResponse"
|
||||||
|
"403":
|
||||||
|
$ref: "#/components/responses/UnauthorizedApiErrorResponse"
|
||||||
|
default:
|
||||||
|
$ref: "#/components/responses/ApiErrorResponse"
|
||||||
|
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
# General
|
||||||
|
UUID:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
maxLength: 36
|
||||||
|
example: "c99506a6-1255-4b71-afa5-7b8ba48c3b1b"
|
||||||
|
|
||||||
|
ID:
|
||||||
|
type: integer
|
||||||
|
format: int32
|
||||||
|
example: 1
|
||||||
|
|
||||||
|
DateTime:
|
||||||
|
type: string
|
||||||
|
format: datetime
|
||||||
|
example: "2022-01-05T11:19:26+00:00"
|
||||||
|
|
||||||
|
# Users
|
||||||
|
Username:
|
||||||
|
type: string
|
||||||
|
example: "admin"
|
||||||
|
|
||||||
|
User:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
$ref: "#/components/schemas/ID"
|
||||||
|
uuid:
|
||||||
|
$ref: "#/components/schemas/UUID"
|
||||||
|
username:
|
||||||
|
$ref: "#/components/schemas/Username"
|
||||||
|
role_id:
|
||||||
|
$ref: "#/components/schemas/ID"
|
||||||
|
individual_id:
|
||||||
|
$ref: "#/components/schemas/ID"
|
||||||
|
disabled:
|
||||||
|
type: boolean
|
||||||
|
created:
|
||||||
|
$ref: "#/components/schemas/DateTime"
|
||||||
|
modified:
|
||||||
|
$ref: "#/components/schemas/DateTime"
|
||||||
|
organisation_id:
|
||||||
|
$ref: "#/components/schemas/ID"
|
||||||
|
|
||||||
|
# Individuals
|
||||||
|
|
||||||
|
# Organisations
|
||||||
|
|
||||||
|
# Roles
|
||||||
|
RoleName:
|
||||||
|
type: string
|
||||||
|
maxLength: 255
|
||||||
|
example: "admin"
|
||||||
|
|
||||||
|
Role:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
$ref: "#/components/schemas/ID"
|
||||||
|
name:
|
||||||
|
$ref: "#/components/schemas/RoleName"
|
||||||
|
is_default:
|
||||||
|
type: boolean
|
||||||
|
perm_admin:
|
||||||
|
type: boolean
|
||||||
|
perm_sync:
|
||||||
|
type: boolean
|
||||||
|
perm_org_admin:
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
# Errors
|
||||||
|
ApiError:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- message
|
||||||
|
- url
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
example: "/users"
|
||||||
|
|
||||||
|
UnauthorizedApiError:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- message
|
||||||
|
- url
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
example: "Authentication failed. Please make sure you pass the API key of an API enabled user along in the Authorization header."
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: "Authentication failed. Please make sure you pass the API key of an API enabled user along in the Authorization header."
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
example: "/users"
|
||||||
|
|
||||||
|
NotFoundApiError:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- message
|
||||||
|
- url
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
example: "Invalid user"
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
example: "Invalid user"
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
example: "/users/1234"
|
||||||
|
|
||||||
|
parameters:
|
||||||
|
userId:
|
||||||
|
name: userId
|
||||||
|
in: path
|
||||||
|
description: "Numeric ID of the User"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ID"
|
||||||
|
|
||||||
|
securitySchemes:
|
||||||
|
ApiKeyAuth:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: Authorization
|
||||||
|
description: |
|
||||||
|
The authorization is performed by using the following header in the HTTP requests:
|
||||||
|
|
||||||
|
Authorization: YOUR_API_KEY
|
||||||
|
|
||||||
|
# requestBodies:
|
||||||
|
|
||||||
|
responses:
|
||||||
|
# User
|
||||||
|
ViewUserResponse:
|
||||||
|
description: "User response"
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/User"
|
||||||
|
|
||||||
|
# Errors
|
||||||
|
ApiErrorResponse:
|
||||||
|
description: "Unexpected API error"
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ApiError"
|
||||||
|
|
||||||
|
UnauthorizedApiErrorResponse:
|
||||||
|
description: "Authentication failed. Please make sure you pass the API key of an API enabled user along in the Authorization header."
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/UnauthorizedApiError"
|
||||||
|
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
Loading…
Reference in New Issue