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; | ||
|  |     } | ||
|  | } |