Last active
February 9, 2020 10:56
-
-
Save marcguyer/61637f0dd16d50d44f6a137003cb7ad5 to your computer and use it in GitHub Desktop.
Functional test abstract using phpunit, Expressive, Doctrine ORM, OAuth2, PSR7, PSR15
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace FunctionalTest; | |
use Doctrine\ORM\EntityManagerInterface; | |
use Doctrine\ORM\Tools\SchemaTool; | |
use PHPUnit\Framework\TestCase; | |
use Psr\Container\ContainerInterface; | |
use Psr\Http\Message\ResponseInterface; | |
use Psr\Http\Message\ServerRequestInterface; | |
use Zend\Diactoros\Stream; | |
use Zend\Diactoros\Request; | |
use Zend\Diactoros\Response; | |
use Zend\Diactoros\ServerRequest; | |
use Zend\Diactoros\Uri; | |
use Zend\Expressive\Application; | |
use Zend\Expressive\MiddlewareFactory; | |
use Doctrine\Common\DataFixtures\Executor\ORMExecutor; | |
use FunctionalTest\Common\Fixture; | |
use League\OAuth2\Server\CryptKey; | |
/** | |
* @coversNothing | |
*/ | |
abstract class AbstractFunctionalTest extends TestCase | |
{ | |
/** @var ContainerInterface */ | |
protected static $container; | |
/** @var Application */ | |
protected static $app; | |
/** @var string */ | |
public static $bearerToken; | |
/** @var ServerRequestInterface */ | |
protected $request; | |
public static function setUpBeforeClass(): void | |
{ | |
static::initContainer(); | |
static::initApp(); | |
static::initPipeline(); | |
static::initRoutes(); | |
} | |
public static function tearDownAfterClass(): void | |
{ | |
static::$container = null; | |
static::$app = null; | |
} | |
protected function tearDown() { | |
parent::tearDown(); | |
unset($this->request); | |
} | |
/** | |
* Initialize new container instance. | |
*/ | |
protected static function initContainer(): void | |
{ | |
static::$container = require 'config/container.php'; | |
} | |
/** | |
* Initialize app. | |
*/ | |
protected static function initApp(): void | |
{ | |
static::$app = static::$container->get(Application::class); | |
} | |
/** | |
* Initialize pipeline. | |
*/ | |
protected static function initPipeline(): void | |
{ | |
$factory = static::$container->get(MiddlewareFactory::class); | |
(require 'config/pipeline.php')(static::$app, $factory, static::$container); | |
} | |
/** | |
* Initialize routes. | |
*/ | |
protected static function initRoutes(): void | |
{ | |
$factory = static::$container->get(MiddlewareFactory::class); | |
(require 'config/routes.php')(static::$app, $factory, static::$container); | |
} | |
/** | |
* Initialize db schema. | |
*/ | |
protected static function initDb(): void | |
{ | |
$em = static::$container->get(EntityManagerInterface::class); | |
$start = microtime(true); | |
// printf('Getting all metadata' . PHP_EOL); | |
$tool = new SchemaTool($em); | |
$meta = $em->getMetadataFactory()->getAllMetadata(); | |
// printf('Got all metadata in % sec' . PHP_EOL, round(microtime(true) - $start, 2)); | |
$start = microtime(true); | |
// printf('Dropping schema' . PHP_EOL); | |
$tool->dropSchema($meta); | |
// printf('Dropped schema in % sec' . PHP_EOL, round(microtime(true) - $start, 2)); | |
// printf('Creating schema' . PHP_EOL); | |
$start = microtime(true); | |
$sql = implode(';' . PHP_EOL, $tool->getCreateSchemaSql($meta)) . ';'; | |
// die($sql); | |
$em->getConnection()->executeUpdate($sql); | |
// $tool->createSchema($meta); | |
// printf('Created schema in % sec' . PHP_EOL, round(microtime(true) - $start, 2)); | |
unset($sql, $tool, $meta); | |
} | |
/** | |
* Initial seed for the db -- stuff required for the app to run, like | |
* preference defaults, etc. | |
*/ | |
protected static function initDbSeed(): void | |
{ | |
$em = static::$container->get(EntityManagerInterface::class); | |
$start = microtime(true); | |
// printf('Initial seed' . PHP_EOL); | |
$seedDir = __DIR__ . '/../../db'; | |
//execute in a way where we can get error messages | |
$conn = $em->getConnection(); | |
// This env var works around the mysql warning printed to stdout | |
// regarding use of the passwd at the command line. | |
$cmd = sprintf( | |
'MYSQL_PWD=%s mysql -B --disable-pager -h %s -u %s -P %s %s < ', | |
$conn->getPassword(), | |
$conn->getHost(), | |
$conn->getUsername(), | |
$conn->getPort() ?? 3306, | |
$conn->getDatabase() | |
); | |
passthru($cmd . $seedDir . '/seed.sql', $returnVar); | |
if ($returnVar) { | |
die('Initial seed (seed.sql) failed with error.' . PHP_EOL); | |
} | |
passthru($cmd . $seedDir . '/seed-test.sql', $returnVar); | |
if ($returnVar) { | |
die('Test seed (seed-test.sql) failed with error.' . PHP_EOL); | |
} | |
unset($seed, $seedTest); | |
} | |
/** | |
* Initialize common fixtures. | |
* | |
* @param array $directories additional directories to load | |
* @param bool $loadCommon load the common fixtures | |
*/ | |
protected static function initFixtures(array $directories = [], $loadCommon = true) | |
{ | |
$c = static::$container; | |
if ($loadCommon) { | |
array_unshift($directories, __DIR__ . '/Common/Fixture'); | |
} | |
$loader = $c->get(Fixture\Loader::class); | |
foreach ($directories as $directory) { | |
$loader->loadFromDirectory($directory); | |
} | |
$em = $c->get(EntityManagerInterface::class); | |
$executor = new ORMExecutor($em); | |
$executor->execute($loader->getFixtures(), true); | |
$accessTokenFixture = $loader | |
->getFixture(Fixture\OAuthAccessTokenFixture::class); | |
if ($accessTokenFixture) { | |
$accessToken = $accessTokenFixture->getReference('accessToken'); | |
self::$bearerToken = (string) $accessToken->convertToJwt( | |
new CryptKey($c->get('config')['authentication']['private_key']) | |
); | |
} | |
} | |
/** | |
* @return string | |
*/ | |
protected function getBearerToken(): string | |
{ | |
return self::$bearerToken; | |
} | |
/** | |
* Provider for testEndpoint() method. | |
* | |
* @see self::testEndpoint() for provider signature | |
* | |
* @return array | |
*/ | |
abstract public function endpointProvider(): array; | |
/** | |
* @param string $method | |
* @param string $subdomain | |
* @param string $path | |
* @param array $requestHeaders | |
* @param array $body | |
* @param array $queryParams | |
* | |
* @return ServerRequestInterface | |
*/ | |
protected function getRequest( | |
string $method, | |
string $subdomain, | |
string $path, | |
array $requestHeaders = [], | |
array $body = [], | |
array $queryParams = [] | |
): ServerRequestInterface { | |
if (!empty($body)) { | |
$bodyStream = fopen('php://memory', 'r+'); | |
fwrite($bodyStream, json_encode($body)); | |
$body = new Stream($bodyStream); | |
} | |
if ( | |
!empty($requestHeaders) | |
&& array_key_exists('Authorization', $requestHeaders) | |
&& !isset($requestHeaders['Authorization']) | |
) { | |
$requestHeaders['Authorization'] = 'Bearer ' . $this->getBearerToken(); | |
} | |
$uri = new Uri( | |
'http://' . $subdomain . '.' . | |
static::$container | |
->get('config')['general']['domain'] . $path | |
); | |
if (!empty($queryParams)) { | |
$uri = $uri->withQuery(http_build_query($queryParams)); | |
} | |
return $this->request = new ServerRequest( | |
[], | |
[], | |
$uri, | |
$method, | |
$body ?? 'php://input', | |
$requestHeaders ?? [] | |
); | |
} | |
/** | |
* @dataProvider endpointProvider | |
* | |
* @coversNothing | |
* | |
* @param string $method | |
* @param string $subdomain | |
* @param string $path | |
* @param array $requestHeaders | |
* @param array $body | |
* @param array $queryParams | |
* @param int $expectResponseStatus | |
* @param array $expectResponseHeaders | |
* @param array $expectResponseBody | |
*/ | |
public function testEndpoint( | |
string $method, | |
string $subdomain, | |
string $path, | |
array $requestHeaders = [], | |
array $body = [], | |
array $queryParams = [], | |
int $expectResponseStatus = 200, | |
array $expectResponseHeaders = [], | |
array $expectResponseBody = [] | |
): void { | |
$request = $this->getRequest(...\func_get_args()); | |
// $this->fail("Request:\n" . Request\Serializer::toString($request)); | |
$response = static::$app->handle($request); | |
// $this->fail( | |
// "Request:\n" . Request\Serializer::toString($request) . "\n\n" . | |
// "Response:\n" . Response\Serializer::toString($response) | |
// ); | |
$this->assertInstanceOf(ResponseInterface::class, $response); | |
$this->assertEquals( | |
$expectResponseStatus, | |
$response->getStatusCode(), | |
"Request:\n" . Request\Serializer::toString($request) . "\n\n" . | |
"Response:\n" . Response\Serializer::toString($response) | |
); | |
if (!empty($expectResponseHeaders)) { | |
$this->assertResponseHeaders( | |
$expectResponseHeaders, | |
$response | |
); | |
} | |
if (!empty($expectResponseBody)) { | |
$this->assertResponseBody( | |
$expectResponseBody, | |
json_decode((string) $response->getBody()) | |
); | |
} | |
// $this->markTestIncomplete("Response:\n" . Response\Serializer::toString($response)); | |
} | |
/** | |
* @param array $expectResponseHeaders | |
* @param ResponseInterface $response | |
*/ | |
private function assertResponseHeaders( | |
array $expectResponseHeaders, | |
ResponseInterface $response | |
): void { | |
foreach ($expectResponseHeaders as $header => $headerValue) { | |
// if value is null we expect the header to not be there at all | |
if (null === $headerValue) { | |
$this->assertFalse( | |
$response->hasHeader($header), | |
sprintf( | |
'Expected response header absence but found "%s": %s', | |
$header, | |
json_encode($response->getHeaders()) | |
) | |
); | |
continue; | |
} | |
$this->assertTrue( | |
$response->hasHeader($header), | |
sprintf( | |
'Missing "%s" response header: %s', | |
$header, | |
json_encode($response->getHeaders()) | |
) | |
); | |
$this->assertTrue( | |
in_array( | |
strtolower($headerValue), | |
array_map('strtolower', $response->getHeader($header)) | |
), | |
sprintf( | |
'Response header value "%s" not found in "%s" header: %s', | |
$headerValue, | |
$header, | |
json_encode($response->getHeaders()) | |
) | |
); | |
} | |
} | |
/** | |
* @param array $expectResponseBody | |
* @param object $responseBody | |
*/ | |
private function assertResponseBody( | |
array $expectResponseBody, | |
$responseBody | |
): void { | |
if (empty($expectResponseBody)) { | |
// ensure body is empty | |
$this->assertEmpty( | |
$responseBody, | |
sprintf( | |
'Expected response body to be empty but found %s', | |
json_encode($responseBody) | |
) | |
); | |
return; | |
} | |
foreach ($expectResponseBody as $param => $pattern) { | |
// if null, we expect the key to *not* be in the response | |
if (is_null($pattern)) { | |
$this->assertObjectNotHasAttribute( | |
$param, | |
$responseBody, | |
sprintf( | |
'Expected response body property "%s" to be missing ' . | |
'but it exists and contains: %s', | |
$param, | |
var_export($responseBody, true) | |
) | |
); | |
continue; | |
} | |
$this->assertObjectHasAttribute( | |
$param, | |
$responseBody, | |
sprintf( | |
'Property "%s" appears to be missing from the ' . | |
' response body object: ' . | |
"\n%s", | |
$param, | |
print_r($responseBody, true) | |
) | |
); | |
if (is_array($pattern)) { | |
// nested params | |
$this->assertResponseBody($pattern, $responseBody->$param); | |
continue; | |
} | |
$this->assertNotNull( | |
$responseBody->$param, | |
'Response body param "' . $param . '" is null.' | |
); | |
$this->assertRegExp( | |
$pattern, | |
$responseBody->$param, | |
'Response body param "' . $param . '" does not match pattern "' . | |
$pattern . '". Param is ' . $responseBody->$param | |
); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment