277 lines
8.5 KiB
PHP
277 lines
8.5 KiB
PHP
<?php declare(strict_types=1);
|
|
|
|
/**
|
|
* @license Apache 2.0
|
|
*/
|
|
|
|
namespace OpenApi\Tests;
|
|
|
|
use Closure;
|
|
use DirectoryIterator;
|
|
use Exception;
|
|
use OpenApi\Analyser;
|
|
use OpenApi\Analysis;
|
|
use OpenApi\Annotations\AbstractAnnotation;
|
|
use OpenApi\Annotations\Info;
|
|
use OpenApi\Annotations\OpenApi;
|
|
use OpenApi\Annotations\PathItem;
|
|
use OpenApi\Context;
|
|
use OpenApi\Logger;
|
|
use OpenApi\StaticAnalyser;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\AbstractLogger;
|
|
use Psr\Log\LoggerInterface;
|
|
use Psr\Log\NullLogger;
|
|
use Symfony\Component\Yaml\Exception\ParseException;
|
|
use Symfony\Component\Yaml\Yaml;
|
|
|
|
class OpenApiTestCase extends TestCase
|
|
{
|
|
/**
|
|
* @var array
|
|
*/
|
|
public $expectedLogMessages = [];
|
|
|
|
/**
|
|
* @var Closure
|
|
*/
|
|
private $originalLogger;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->expectedLogMessages = [];
|
|
$this->originalLogger = Logger::getInstance()->log;
|
|
Logger::getInstance()->log = function ($entry, $type) {
|
|
if (count($this->expectedLogMessages)) {
|
|
list($assertion, $needle) = array_shift($this->expectedLogMessages);
|
|
$assertion($entry, $type);
|
|
} else {
|
|
$map = [
|
|
E_USER_NOTICE => 'notice',
|
|
E_USER_WARNING => 'warning',
|
|
];
|
|
if (isset($map[$type])) {
|
|
$this->fail('Unexpected \OpenApi\Logger::' . $map[$type] . '("' . $entry . '")');
|
|
} else {
|
|
$this->fail('Unexpected \OpenApi\Logger->getInstance()->log("' . $entry . '",' . $type . ')');
|
|
}
|
|
}
|
|
};
|
|
parent::setUp();
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
$this->assertEmpty(
|
|
$this->expectedLogMessages,
|
|
implode(PHP_EOL . ' => ', array_merge(
|
|
['OpenApi\Logger messages were not triggered:'],
|
|
array_map(function (array $value) {
|
|
return $value[1];
|
|
}, $this->expectedLogMessages)
|
|
))
|
|
);
|
|
Logger::getInstance()->log = $this->originalLogger;
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function getPsrLogger(bool $tracking = false): ?LoggerInterface
|
|
{
|
|
if (!$tracking) {
|
|
// allow to test the default behaviour without injected PSR logger
|
|
switch (strtoupper($_ENV['NON_TRACKING_LOGGER'] ?? 'FALLBACK')) {
|
|
case 'NULL':
|
|
return new NullLogger();
|
|
case 'FALLBACK':
|
|
default:
|
|
// whatever is set up in Logger::$instance->log
|
|
return null;
|
|
}
|
|
}
|
|
|
|
return new class($this) extends AbstractLogger {
|
|
protected $testCase;
|
|
|
|
public function __construct($testCase)
|
|
{
|
|
$this->testCase = $testCase;
|
|
}
|
|
|
|
public function log($level, $message, array $context = [])
|
|
{
|
|
if (count($this->testCase->expectedLogMessages)) {
|
|
list($assertion, $needle) = array_shift($this->testCase->expectedLogMessages);
|
|
$assertion($message, $level);
|
|
} else {
|
|
$this->testCase->fail('Unexpected \OpenApi\Logger::' . $level . '("' . $message . '")');
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
public function assertOpenApiLogEntryContains($needle, $message = '')
|
|
{
|
|
$this->expectedLogMessages[] = [function ($entry, $type) use ($needle, $message) {
|
|
if ($entry instanceof Exception) {
|
|
$entry = $entry->getMessage();
|
|
}
|
|
$this->assertStringContainsString($needle, $entry, $message);
|
|
}, $needle];
|
|
}
|
|
|
|
/**
|
|
* Compare OpenApi specs assuming strings to contain YAML.
|
|
*
|
|
* @param array|OpenApi|\stdClass|string $actual The generated output
|
|
* @param array|OpenApi|\stdClass|string $expected The specification
|
|
* @param string $message
|
|
* @param bool $normalized flag indicating whether the inputs are already normalized or not
|
|
*/
|
|
protected function assertSpecEquals($actual, $expected, $message = '', $normalized = false)
|
|
{
|
|
$normalize = function ($in) {
|
|
if ($in instanceof OpenApi) {
|
|
$in = $in->toYaml();
|
|
}
|
|
if (is_string($in)) {
|
|
// assume YAML
|
|
try {
|
|
$in = Yaml::parse($in);
|
|
} catch (ParseException $e) {
|
|
$this->fail('Invalid YAML: ' . $e->getMessage() . PHP_EOL . $in);
|
|
}
|
|
}
|
|
|
|
return $in;
|
|
};
|
|
|
|
if (!$normalized) {
|
|
$actual = $normalize($actual);
|
|
$expected = $normalize($expected);
|
|
}
|
|
|
|
if (is_iterable($actual) && is_iterable($expected)) {
|
|
foreach ($actual as $key => $value) {
|
|
$this->assertArrayHasKey($key, (array) $expected, $message . ': property: "' . $key . '" should be absent, but has value: ' . $this->formattedValue($value));
|
|
$this->assertSpecEquals($value, ((array) $expected)[$key], $message . ' > ' . $key, true);
|
|
}
|
|
foreach ($expected as $key => $value) {
|
|
$this->assertArrayHasKey($key, (array) $actual, $message . ': property: "' . $key . '" is missing');
|
|
$this->assertSpecEquals(((array) $actual)[$key], $value, $message . ' > ' . $key, true);
|
|
}
|
|
} else {
|
|
$this->assertEquals($actual, $expected, $message);
|
|
}
|
|
}
|
|
|
|
private function formattedValue($value)
|
|
{
|
|
if (is_bool($value)) {
|
|
return $value ? 'true' : 'false';
|
|
}
|
|
if (is_numeric($value)) {
|
|
return (string) $value;
|
|
}
|
|
if (is_string($value)) {
|
|
return '"' . $value . '"';
|
|
}
|
|
if (is_object($value)) {
|
|
return get_class($value);
|
|
}
|
|
|
|
return gettype($value);
|
|
}
|
|
|
|
/**
|
|
* Parse a comment.
|
|
*
|
|
* @param string $comment Contents of a comment block
|
|
*
|
|
* @return AbstractAnnotation[]
|
|
*/
|
|
protected function parseComment($comment)
|
|
{
|
|
$analyser = new Analyser();
|
|
$context = Context::detect(1);
|
|
|
|
return $analyser->fromComment("<?php\n/**\n * " . implode("\n * ", explode("\n", $comment)) . "\n*/", $context);
|
|
}
|
|
|
|
/**
|
|
* Create a valid OpenApi object with Info.
|
|
*/
|
|
protected function createOpenApiWithInfo()
|
|
{
|
|
return new OpenApi([
|
|
'info' => new Info([
|
|
'title' => 'swagger-php Test-API',
|
|
'version' => 'test',
|
|
'_context' => new Context(['unittest' => true]),
|
|
]),
|
|
'paths' => [
|
|
new PathItem(['path' => '/test']),
|
|
],
|
|
'_context' => new Context(['unittest' => true]),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Resolve fixture filenames.
|
|
*
|
|
* @param array|string $files one ore more files
|
|
*
|
|
* @return array resolved filenames for loading scanning etc
|
|
*/
|
|
public function fixtures($files): array
|
|
{
|
|
return array_map(function ($file) {
|
|
return __DIR__ . '/Fixtures/' . $file;
|
|
}, (array) $files);
|
|
}
|
|
|
|
public function analysisFromFixtures($files): Analysis
|
|
{
|
|
$analyser = new StaticAnalyser();
|
|
$analysis = new Analysis([], new Context());
|
|
|
|
foreach ((array) $files as $file) {
|
|
$analysis->addAnalysis($analyser->fromFile($this->fixtures($file)[0]));
|
|
}
|
|
|
|
return $analysis;
|
|
}
|
|
|
|
public function analysisFromCode(string $code, ?Context $context = null)
|
|
{
|
|
return (new StaticAnalyser())->fromCode("<?php\n" . $code, $context ?: new Context());
|
|
}
|
|
|
|
public function analysisFromDockBlock($comment)
|
|
{
|
|
return (new Analyser())->fromComment($comment, null);
|
|
}
|
|
|
|
/**
|
|
* Collect list of all non abstract annotation classes.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function allAnnotationClasses()
|
|
{
|
|
$classes = [];
|
|
$dir = new DirectoryIterator(__DIR__ . '/../src/Annotations');
|
|
foreach ($dir as $entry) {
|
|
if (!$entry->isFile() || $entry->getExtension() != 'php') {
|
|
continue;
|
|
}
|
|
$class = $entry->getBasename('.php');
|
|
if (in_array($class, ['AbstractAnnotation', 'Operation'])) {
|
|
continue;
|
|
}
|
|
$classes[$class] = ['OpenApi\\Annotations\\' . $class];
|
|
}
|
|
|
|
return $classes;
|
|
}
|
|
}
|