Skip to content

Instantly share code, notes, and snippets.

@marcguyer
Last active February 9, 2020 10:56
Show Gist options
  • Save marcguyer/61637f0dd16d50d44f6a137003cb7ad5 to your computer and use it in GitHub Desktop.
Save marcguyer/61637f0dd16d50d44f6a137003cb7ad5 to your computer and use it in GitHub Desktop.
Functional test abstract using phpunit, Expressive, Doctrine ORM, OAuth2, PSR7, PSR15
<?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