diff --git a/src/Commands/GenerateServer.php b/src/Commands/GenerateServer.php index a96ff24..96118ed 100644 --- a/src/Commands/GenerateServer.php +++ b/src/Commands/GenerateServer.php @@ -10,6 +10,16 @@ class GenerateServer extends Command { + public const SUPPORTED_ENTITIES = [ + 'controllers', + 'enums', + 'requests', + 'routes', + 'pest_tests', + 'resources', + 'policies', + ]; + /** var @string */ protected $signature = 'openapi:generate-server {--e|entities=}'; @@ -55,7 +65,12 @@ public function handleMapping(string $sourcePath, array $optionsPerEntity) { $specObject = $this->parseSpec($sourcePath); - foreach ($this->config['supported_entities'] as $entity => $generatorClass) { + foreach (static::SUPPORTED_ENTITIES as $entity) { + $generatorClass = $this->config['supported_entities'][$entity] ?? null; + if (!isset($generatorClass)) { + continue; + } + if (!$this->shouldEntityBeGenerated($entity)) { continue; } diff --git a/src/Data/Controllers/ControllersStorage.php b/src/Data/Controllers/ControllersStorage.php new file mode 100644 index 0000000..75833e4 --- /dev/null +++ b/src/Data/Controllers/ControllersStorage.php @@ -0,0 +1,29 @@ +controllers[$serversUrl][$path][$method] = $responseCodes; + } + + public function isExistControllerMethod( + string $serversUrl, + string $path, + string $method, + int $responseCode + ): bool { + $codes = $this->controllers[$serversUrl][$path][$method] ?? []; + + return !in_array($responseCode, $codes); + } +} diff --git a/src/Generators/BaseGenerator.php b/src/Generators/BaseGenerator.php index 277c140..e214718 100644 --- a/src/Generators/BaseGenerator.php +++ b/src/Generators/BaseGenerator.php @@ -2,6 +2,8 @@ namespace Ensi\LaravelOpenApiServerGenerator\Generators; +use Ensi\LaravelOpenApiServerGenerator\Data\Controllers\ControllersStorage; +use Ensi\LaravelOpenApiServerGenerator\Utils\ClassParser; use Ensi\LaravelOpenApiServerGenerator\Utils\PhpDocGenerator; use Ensi\LaravelOpenApiServerGenerator\Utils\PSR4PathConverter; use Ensi\LaravelOpenApiServerGenerator\Utils\RouteHandlerParser; @@ -22,6 +24,8 @@ public function __construct( protected RouteHandlerParser $routeHandlerParser, protected TypesMapper $typesMapper, protected PhpDocGenerator $phpDocGenerator, + protected ClassParser $classParser, + protected ControllersStorage $controllersStorage, ) { } diff --git a/src/Generators/ControllersGenerator.php b/src/Generators/ControllersGenerator.php index 8290a02..48a1b69 100644 --- a/src/Generators/ControllersGenerator.php +++ b/src/Generators/ControllersGenerator.php @@ -3,16 +3,26 @@ namespace Ensi\LaravelOpenApiServerGenerator\Generators; use cebe\openapi\SpecObjectInterface; +use Ensi\LaravelOpenApiServerGenerator\Utils\ClassParser; use stdClass; class ControllersGenerator extends BaseGenerator implements GeneratorInterface { + public const REQUEST_NAMESPACE = 'Illuminate\Http\Request'; + public const RESPONSABLE_NAMESPACE = 'Illuminate\Contracts\Support\Responsable'; + public const DELIMITER = "\n "; + private array $methodsWithRequests = ['PATCH', 'POST', 'PUT', 'DELETE']; + private string $serversUrl; + public function generate(SpecObjectInterface $specObject): void { + $openApiData = $specObject->getSerializableData(); + $this->serversUrl = $openApiData?->servers[0]?->url ?? ''; + $controllers = $this->extractControllers($specObject); - $this->createControllersFiles($controllers, $this->templatesManager->getTemplate('Controller.template')); + $this->createControllersFiles($controllers); } private function extractControllers(SpecObjectInterface $specObject): array @@ -21,7 +31,7 @@ private function extractControllers(SpecObjectInterface $specObject): array $controllers = []; $paths = $openApiData->paths ?: []; - foreach ($paths as $routes) { + foreach ($paths as $path => $routes) { foreach ($routes as $method => $route) { $requestClassName = null; $methodWithRequest = in_array(strtoupper($method), $this->methodsWithRequests); @@ -56,14 +66,20 @@ private function extractControllers(SpecObjectInterface $specObject): array list($requestClassName, $requestNamespace) = $this->getActualClassNameAndNamespace($requestClassName, $requestNamespace); $requestNamespace .= '\\' . ucfirst($requestClassName); - $controllers[$fqcn]['requestsNamespaces'][] = $requestNamespace; - } elseif ($methodWithRequest) { - $controllers[$fqcn]['requestsNamespaces'][] = 'Illuminate\Http\Request'; + $controllers[$fqcn]['requestsNamespaces'][$requestNamespace] = $requestNamespace; } + $responses = $route->responses ?? null; $controllers[$fqcn]['actions'][] = [ 'name' => $handler->method ?: '__invoke', + 'with_request_namespace' => $methodWithRequest && !empty($route->{'x-lg-skip-request-generation'}), 'parameters' => array_merge($this->extractPathParameters($route), $this->getActionExtraParameters($methodWithRequest, $requestClassName)), + + 'route' => [ + 'method' => $method, + 'path' => $path, + 'responseCodes' => $responses ? array_keys(get_object_vars($responses)) : [], + ], ]; } } @@ -73,7 +89,7 @@ private function extractControllers(SpecObjectInterface $specObject): array private function extractPathParameters(stdClass $route): array { - $oasRoutePath = array_filter($route->parameters ?? [], fn (stdClass $param) => $param?->in === "path"); + $oasRoutePath = array_filter($route->parameters ?? [], fn (stdClass $param) => $param?->in === "path"); return array_map(fn (stdClass $param) => [ 'name' => $param->name, @@ -93,39 +109,81 @@ private function getActionExtraParameters(bool $methodWithRequest, $requestClass return []; } - private function createControllersFiles(array $controllers, string $template): void + private function createControllersFiles(array $controllers): void { foreach ($controllers as $controller) { $namespace = $controller['namespace']; $className = $controller['className']; $filePath = $this->getNamespacedFilePath($className, $namespace); - if ($this->filesystem->exists($filePath)) { + $controllerExists = $this->filesystem->exists($filePath); + if (!$controllerExists) { + $this->createEmptyControllerFile($filePath, $controller); + } + + $class = $this->classParser->parse("$namespace\\$className"); + + $newMethods = $this->convertMethodsToString($class, $controller['actions'], $controller['requestsNamespaces']); + if (!empty($newMethods)) { + $controller['requestsNamespaces'][static::RESPONSABLE_NAMESPACE] = static::RESPONSABLE_NAMESPACE; + } elseif ($controllerExists) { continue; } - $this->putWithDirectoryCheck( - $filePath, - $this->replacePlaceholders($template, [ - '{{ namespace }}' => $namespace, - '{{ className }}' => $className, - '{{ requestsNamespaces }}' => $this->formatRequestNamespaces($controller['requestsNamespaces']), - '{{ methods }}' => $this->convertMethodsToString($controller['actions']), - ]) - ); + $content = $class->getContentWithAdditionalMethods($newMethods, $controller['requestsNamespaces']); + + $this->writeControllerFile($filePath, $controller, $content); } } + protected function writeControllerFile(string $filePath, array $controller, string $classContent): void + { + $this->putWithDirectoryCheck( + $filePath, + $this->replacePlaceholders( + $this->templatesManager->getTemplate('ControllerExists.template'), + [ + '{{ namespace }}' => $controller['namespace'], + '{{ requestsNamespaces }}' => $this->formatRequestNamespaces($controller['requestsNamespaces']), + '{{ classContent }}' => $classContent, + ] + ) + ); + } + + protected function createEmptyControllerFile(string $filePath, array $controller): void + { + $this->putWithDirectoryCheck( + $filePath, + $this->replacePlaceholders( + $this->templatesManager->getTemplate('ControllerEmpty.template'), + [ + '{{ namespace }}' => $controller['namespace'], + '{{ requestsNamespaces }}' => $this->formatRequestNamespaces($controller['requestsNamespaces']), + '{{ className }}' => $controller['className'], + ] + ) + ); + } + private function formatActionParamsAsString(array $params): string { return implode(', ', array_map(fn (array $param) => $param['type'] . " $" . $param['name'], $params)); } - private function convertMethodsToString(array $methods): string + private function convertMethodsToString(ClassParser $class, array $methods, array &$namespaces): string { $methodsStrings = []; foreach ($methods as $method) { + if ($class->hasMethod($method['name'])) { + continue; + } + + if ($method['with_request_namespace']) { + $namespaces[static::REQUEST_NAMESPACE] = static::REQUEST_NAMESPACE; + } + $methodsStrings[] = $this->replacePlaceholders( $this->templatesManager->getTemplate('ControllerMethod.template'), [ @@ -133,15 +191,24 @@ private function convertMethodsToString(array $methods): string '{{ params }}' => $this->formatActionParamsAsString($method['parameters']), ] ); + + $this->controllersStorage->markNewControllerMethod( + serversUrl: $this->serversUrl, + path: $method['route']['path'], + method: $method['route']['method'], + responseCodes: $method['route']['responseCodes'], + ); } - return implode("\n\n ", $methodsStrings); + $prefix = !empty($methodsStrings) ? static::DELIMITER : ''; + + return $prefix . implode(static::DELIMITER, $methodsStrings); } protected function formatRequestNamespaces(array $namespaces): string { - $namespaces = array_unique($namespaces); - sort($namespaces); + $namespaces = array_values($namespaces); + sort($namespaces, SORT_STRING | SORT_FLAG_CASE); return implode("\n", array_map(fn (string $namespaces) => "use {$namespaces};", $namespaces)); } diff --git a/src/Generators/PestTestsGenerator.php b/src/Generators/PestTestsGenerator.php index 6e14761..7d69a52 100644 --- a/src/Generators/PestTestsGenerator.php +++ b/src/Generators/PestTestsGenerator.php @@ -34,11 +34,9 @@ private function getPhpHttpTestMethodCommon(string $httpMethod): string return $httpMethod; } - protected function convertRoutesToTestsString(array $routes, string $serversUrl): string + protected function convertRoutesToTestsString(array $routes, string $serversUrl, bool $onlyNewMethods = false): string { - $testsFunctions = [ - "uses()->group('component');", - ]; + $testsFunctions = $onlyNewMethods ? [] : ["uses()->group('component');"]; foreach ($routes as $route) { foreach ($route['responseCodes'] as $responseCode) { @@ -46,9 +44,20 @@ protected function convertRoutesToTestsString(array $routes, string $serversUrl) continue; } + $methodExists = $this->controllersStorage->isExistControllerMethod( + serversUrl: $serversUrl, + path: $route['path'], + method: $route['method'], + responseCode: $responseCode, + ); + + if ($onlyNewMethods && $methodExists) { + continue; + } + $url = $serversUrl . $route['path']; $testName = strtoupper($route['method']) . ' ' . $url. ' ' . $responseCode; - $phpHttpMethod = $this->getPhpHttpTestMethod($route['method'], $route['responseContentType']); + $phpHttpMethod = $this->getPhpHttpTestMethod($route['method'], $route['responseContentType']); $testsFunctions[] = << $className, 'namespace' => $namespace, 'methods' => $methods]) { $filePath = $this->getNamespacedFilePath($className, $namespace); if ($this->filesystem->exists($filePath)) { + $class = $this->classParser->parse("$namespace\\$className"); + + $newPolicies = $this->convertMethodsToString($methods, $class); + if (!empty($newPolicies)) { + $class->addMethods($newPolicies); + } + continue; } @@ -104,17 +112,31 @@ private function handlerValidation(ParsedRouteHandler $handler): bool }; } - private function convertMethodsToString(array $methods): string + private function convertMethodsToString(array $methods, ?ClassParser $class = null): string { $methodsStrings = []; foreach ($methods as $method) { + if ($class?->hasMethod($method)) { + continue; + } + $methodsStrings[] = $this->replacePlaceholders( $this->templatesManager->getTemplate('PolicyGate.template'), ['{{ method }}' => $method] ); } + if ($class) { + $existMethods = $class->getMethods(); + foreach ($existMethods as $methodName => $method) { + if (!in_array($methodName, $methods) && !$class->isTraitMethod($methodName)) { + $className = $class->getClassName(); + console_warning("Warning: метод {$className}::{$methodName} отсутствует в спецификации или не может возвращать 403 ошибку"); + } + } + } + return implode("\n\n ", $methodsStrings); } } diff --git a/src/Generators/RoutesGenerator.php b/src/Generators/RoutesGenerator.php index 445d70a..39ceb06 100644 --- a/src/Generators/RoutesGenerator.php +++ b/src/Generators/RoutesGenerator.php @@ -112,12 +112,7 @@ private function formatControllerNamespaces(array $controllerNamespaces): string } } - uasort($namespaces, function (string $first, string $second) { - $firstNamespace = str_replace('\\', ' ', trim(preg_replace('%/\*(.*)\*/%s', '', $first))); - $secondNamespace = str_replace('\\', ' ', trim(preg_replace('%/\*(.*)\*/%s', '', $second))); - - return strcasecmp($firstNamespace, $secondNamespace); - }); + sort($namespaces, SORT_STRING | SORT_FLAG_CASE); return implode("\n", array_map(fn (string $namespace) => "use {$namespace};", $namespaces)); } diff --git a/src/Generators/TestsGenerator.php b/src/Generators/TestsGenerator.php index c516bae..c479c4c 100644 --- a/src/Generators/TestsGenerator.php +++ b/src/Generators/TestsGenerator.php @@ -9,7 +9,7 @@ abstract class TestsGenerator extends BaseGenerator implements GeneratorInterface { - abstract protected function convertRoutesToTestsString(array $routes, string $serversUrl): string; + abstract protected function convertRoutesToTestsString(array $routes, string $serversUrl, bool $onlyNewMethods = false): string; abstract protected function convertRoutesToImportsString(array $routes): string; @@ -95,6 +95,16 @@ protected function createTestsFiles(array $testsData, string $template, $servers foreach ($testsData as ['className' => $className, 'namespace' => $namespace, 'routes' => $routes]) { $filePath = $this->getNamespacedFilePath($className, $namespace); if ($this->filesystem->exists($filePath)) { + $newTests = $this->convertRoutesToTestsString($routes, $serversUrl, true); + if (!empty($newTests)) { + $data = <<filesystem->append($filePath, $data); + } + continue; } diff --git a/src/LaravelOpenApiServerGeneratorServiceProvider.php b/src/LaravelOpenApiServerGeneratorServiceProvider.php index 40d66ac..e395225 100644 --- a/src/LaravelOpenApiServerGeneratorServiceProvider.php +++ b/src/LaravelOpenApiServerGeneratorServiceProvider.php @@ -3,6 +3,7 @@ namespace Ensi\LaravelOpenApiServerGenerator; use Ensi\LaravelOpenApiServerGenerator\Commands\GenerateServer; +use Ensi\LaravelOpenApiServerGenerator\Data\Controllers\ControllersStorage; use Ensi\LaravelOpenApiServerGenerator\Utils\PSR4PathConverter; use Ensi\LaravelOpenApiServerGenerator\Utils\TemplatesManager; use Illuminate\Support\ServiceProvider; @@ -28,6 +29,10 @@ public function register() $this->app->when(PSR4PathConverter::class) ->needs('$mappings') ->give(config('openapi-server-generator.namespaces_to_directories_mapping', [])); + + $this->app->singleton(ControllersStorage::class, function () { + return new ControllersStorage(); + }); } /** diff --git a/src/Utils/ClassParser.php b/src/Utils/ClassParser.php new file mode 100644 index 0000000..2fd0794 --- /dev/null +++ b/src/Utils/ClassParser.php @@ -0,0 +1,154 @@ +ref = new ReflectionClass($className); + $this->methods = null; + + return $this; + } + + public function getClassName(): string + { + return $this->ref->getName(); + } + + public function isEmpty(): bool + { + return $this->getMethods()->isEmpty(); + } + + public function getMethods(): Collection + { + if (!$this->methods) { + $this->methods = collect($this->ref->getMethods())->keyBy('name'); + } + + return $this->methods; + } + + public function getTraits(): Collection + { + if (!$this->traits) { + $this->traits = collect($this->ref->getTraits())->map(function (ReflectionClass $trait) { + return collect($trait->getMethods())->pluck('name'); + }); + } + + return $this->traits; + } + + public function isTraitMethod(string $methodName): bool + { + $traits = $this->getTraits(); + + return $traits->contains(fn (Collection $methods) => $methods->contains($methodName)); + } + + public function addMethods(string $methods): void + { + if (empty($methods)) { + return; + } + + $lines = []; + $currentLine = 0; + $endLine = $this->getEndLine(); + $filePath = $this->getFileName(); + + foreach ($this->filesystem->lines($filePath) as $line) { + $currentLine++; + if ($currentLine === $endLine) { + $lines[] = ""; + $lines[] = " $methods"; + $lines[] = "}"; + + break; + } + + $lines[] = $line; + } + + $contents = implode(PHP_EOL, $lines); + + $this->filesystem->put($filePath, $contents); + } + + public function hasMethod(string $methodName): bool + { + return $this->getMethods()->has($methodName); + } + + public function getStartLine(bool $withoutComments = false): int + { + $comments = $this->ref->getDocComment(); + if ($withoutComments || !$comments) { + return $this->ref->getStartLine(); + } + + return $this->ref->getStartLine() - count(explode("\n", $comments)); + } + + public function getEndLine(): int + { + return $this->ref->getEndLine(); + } + + public function getFileName(): string + { + return $this->ref->getFileName(); + } + + public function getContentWithAdditionalMethods(string $additionalMethods, array &$namespaces = []): string + { + $currentLine = 0; + $classContent = ''; + $classEndLine = $this->getEndLine(); + $classStartLine = $this->getStartLine(); + + foreach ($this->filesystem->lines($this->getFileName()) as $line) { + $currentLine++; + + if ($currentLine < $classStartLine) { + preg_match(static::NAMESPACE_LINE_PATTERN, $line, $matches); + $namespace = $matches[1] ?? null; + if ($namespace && !in_array($namespace, $namespaces)) { + $namespaces[$namespace] = $namespace; + } + + continue; + } + + if ($currentLine === $classEndLine) { + $additionalMethods = $this->isEmpty() ? ltrim($additionalMethods, "\n") : $additionalMethods; + $classContent .= $additionalMethods . $line; + + break; + } + + $classContent .= "$line\n"; + } + + return $classContent; + } +} diff --git a/src/Utils/PSR4PathConverter.php b/src/Utils/PSR4PathConverter.php index 98f7446..11aba1f 100644 --- a/src/Utils/PSR4PathConverter.php +++ b/src/Utils/PSR4PathConverter.php @@ -24,9 +24,10 @@ public function namespaceToPath(?string $namespace): string if (is_null($namespace)) { return ''; } - foreach ($this->mappings as $mappingNamescape => $mappingPath) { - if (str_starts_with($namespace, $mappingNamescape)) { - $namespaceWithoutBase = substr($namespace, strlen($mappingNamescape)); + + foreach ($this->mappings as $mappingNamespace => $mappingPath) { + if (str_starts_with($namespace, $mappingNamespace)) { + $namespaceWithoutBase = substr($namespace, strlen($mappingNamespace)); return $mappingPath . '/' . trim(str_replace("\\", '/', $namespaceWithoutBase), '/'); } diff --git a/templates/Controller.template b/templates/ControllerEmpty.template similarity index 57% rename from templates/Controller.template rename to templates/ControllerEmpty.template index fd465db..6c0d172 100644 --- a/templates/Controller.template +++ b/templates/ControllerEmpty.template @@ -2,10 +2,8 @@ namespace {{ namespace }}; -use Illuminate\Contracts\Support\Responsable; {{ requestsNamespaces }} class {{ className }} { - {{ methods }} } diff --git a/templates/ControllerExists.template b/templates/ControllerExists.template new file mode 100644 index 0000000..4f91257 --- /dev/null +++ b/templates/ControllerExists.template @@ -0,0 +1,7 @@ +mock(Filesystem::class); + + $parser = new ClassParser($filesystem); + $parser->parse($namespace); + + expect($parser->isEmpty())->toBe($result); +})->with([ + [LaravelExistsController::class, false], + [LaravelEmptyController::class, true], +]); + +test('ClassParser check getMethods success', function (string $namespace, array $result) { + $filesystem = $this->mock(Filesystem::class); + + $parser = new ClassParser($filesystem); + $parser->parse($namespace); + + $methods = $parser->getMethods()->keys()->toArray(); + + expect($methods)->toBe($result); +})->with([ + [LaravelExistsController::class, ['delete']], + [LaravelEmptyController::class, []], +]); + +test('ClassParser check hasMethod success', function (string $namespace, string $method, bool $result) { + $filesystem = $this->mock(Filesystem::class); + + $parser = new ClassParser($filesystem); + $parser->parse($namespace); + + expect($parser->hasMethod($method))->toBe($result); +})->with([ + [LaravelExistsController::class, 'delete', true], + [LaravelExistsController::class, 'search', false], + [LaravelEmptyController::class, 'delete', false], + [LaravelEmptyController::class, 'search', false], +]); + +test('ClassParser check getLines success', function (string $namespace, int $start, int $end) { + $filesystem = $this->mock(Filesystem::class); + + $parser = new ClassParser($filesystem); + $parser->parse($namespace); + + expect($parser->getStartLine())->toBe($start); + expect($parser->getEndLine())->toBe($end); +})->with([ + [LaravelExistsController::class, 8, 14], + [LaravelEmptyController::class, 5, 10], +]); + +test('ClassParser check isTraitMethod success', function (string $namespace, string $method, bool $result) { + $filesystem = $this->mock(Filesystem::class); + + $parser = new ClassParser($filesystem); + $parser->parse($namespace); + + expect($parser->isTraitMethod($method))->toBe($result); +})->with([ + [LaravelPolicy::class, 'allow', true], + [LaravelPolicy::class, 'search', false], + [LaravelPolicy::class, 'get', false], + + [LaravelWithoutTraitPolicy::class, 'allow', false], + [LaravelWithoutTraitPolicy::class, 'search', false], + [LaravelWithoutTraitPolicy::class, 'get', false], +]); + +test('ClassParser check getClassName success', function (string $namespace) { + $filesystem = $this->mock(Filesystem::class); + + $parser = new ClassParser($filesystem); + $parser->parse($namespace); + + expect($parser->getClassName())->toBe($namespace); +})->with([ + [LaravelPolicy::class], + [LaravelExistsController::class], + [LaravelEmptyController::class], +]); + +test('ClassParser check getFileName success', function (string $namespace) { + $filesystem = $this->mock(Filesystem::class); + + $parser = new ClassParser($filesystem); + $parser->parse($namespace); + + $class = last(explode("\\", $namespace)); + + expect($parser->getFileName())->toBe(realpath(__DIR__ . "/expects/Controllers/{$class}.php")); +})->with([ + [LaravelExistsController::class], + [LaravelEmptyController::class], +]); + +test('ClassParser check getContentWithAdditionalMethods success', function ( + string $namespace, + string $expect, + string $additional = "", + array $namespaces = [], + array $expectNamespaces = [], +) { + $class = last(explode("\\", $namespace)); + + /** @var \Mockery\Mock|Filesystem $filesystem */ + $filesystem = $this->mock(Filesystem::class); + $filesystem + ->shouldReceive('lines') + ->andReturn(file(__DIR__ . "/expects/Controllers/{$class}.php", FILE_IGNORE_NEW_LINES)); + + $parser = new ClassParser($filesystem); + $parser->parse($namespace); + + $content = $parser->getContentWithAdditionalMethods($additional, $namespaces); + + $expectPathFile = __DIR__ . "/expects/Controllers/{$expect}.expect"; + $expectResult = implode("\n", file($expectPathFile, FILE_IGNORE_NEW_LINES)); + + expect($content)->toBe($expectResult); + + expect($namespaces)->toBe($expectNamespaces); +})->with([ + [ + LaravelExistsController::class, + 'LaravelExists_1_Controller', + "\n public function test() {}\n", + [ + "App\Http\ApiV1\Support\Resources\EmptyResource" => "App\Http\ApiV1\Support\Resources\EmptyResource", + ], + [ + "App\Http\ApiV1\Support\Resources\EmptyResource" => "App\Http\ApiV1\Support\Resources\EmptyResource", + "Illuminate\Contracts\Support\Responsable" => "Illuminate\Contracts\Support\Responsable", + "Illuminate\Http\Request" => "Illuminate\Http\Request", + ], + ], + [ + LaravelExistsController::class, + 'LaravelExists_2_Controller', + "", + [], + [ + "Illuminate\Contracts\Support\Responsable" => "Illuminate\Contracts\Support\Responsable", + "Illuminate\Http\Request" => "Illuminate\Http\Request", + ], + ], + [LaravelEmptyController::class, 'LaravelEmpty_1_Controller'], + [LaravelEmptyController::class, 'LaravelEmpty_2_Controller', "\n public function test() {}\n"], +]); diff --git a/tests/GenerateServerTest.php b/tests/GenerateServerTest.php index df3e159..15cd4a6 100644 --- a/tests/GenerateServerTest.php +++ b/tests/GenerateServerTest.php @@ -5,6 +5,7 @@ use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Facades\Config; use function Pest\Laravel\artisan; +use function PHPUnit\Framework\assertEquals; use function PHPUnit\Framework\assertEqualsCanonicalizing; use function PHPUnit\Framework\assertNotTrue; use function PHPUnit\Framework\assertStringContainsString; @@ -27,7 +28,8 @@ $appRoot = realpath($this->makeFilePath(__DIR__ . '/../vendor/orchestra/testbench-core/laravel/')); $putFiles = []; $filesystem->shouldReceive('put')->withArgs(function ($path, $content) use (&$putFiles, $appRoot) { - $putFiles[] = $this->makeFilePath(str_replace($appRoot, '', $path)); + $filePath = $this->makeFilePath(str_replace($appRoot, '', $path)); + $putFiles[$filePath] = $filePath; return true; }); @@ -75,7 +77,7 @@ $this->makeFilePath('/app/Http/Controllers/PoliciesController.php'), $this->makeFilePath('/app/Http/Tests/PoliciesComponentTest.php'), $this->makeFilePath('/app/Http/Policies/PoliciesControllerPolicy.php'), - ], $putFiles); + ], array_values($putFiles)); }); test("Correct requests in controller methods", function () { @@ -170,3 +172,72 @@ $routes ); }); + +test("Update tests success", function (array $parameters, bool $withControllerEntity) { + /** @var TestCase $this */ + $mapping = Config::get('openapi-server-generator.api_docs_mappings'); + $mappingValue = current($mapping); + $mapping = [$this->makeFilePath(__DIR__ . '/resources/index.yaml') => $mappingValue]; + Config::set('openapi-server-generator.api_docs_mappings', $mapping); + + $appRoot = realpath($this->makeFilePath(__DIR__ . '/../vendor/orchestra/testbench-core/laravel/')); + + $existTest = $this->makeFilePath('/app/Http/Tests/ResourcesComponentTest.php'); + + $filesystem = $this->mock(Filesystem::class); + $filesystem->shouldReceive('exists')->andReturnUsing(function ($path) use ($appRoot, $existTest) { + $filePath = $this->makeFilePath(str_replace($appRoot, '', $path)); + + return $filePath === $existTest; + }); + + $filesystem->shouldReceive('get')->withArgs(function ($path) { + return (bool)strstr($path, '.template'); + })->andReturnUsing(function ($path) { + return file_get_contents($path); + }); + $filesystem->shouldReceive('cleanDirectory', 'ensureDirectoryExists'); + + $putFiles = []; + $filesystem->shouldReceive('put')->withArgs(function ($path, $content) use (&$putFiles, $appRoot) { + $filePath = $this->makeFilePath(str_replace($appRoot, '', $path)); + $putFiles[$filePath] = $filePath; + + return true; + }); + + $appendFiles = []; + $filesystem->shouldReceive('append')->withArgs(function ($filePath, $data) use (&$appendFiles, $appRoot, $existTest) { + $filePath = $this->makeFilePath(str_replace($appRoot, '', $filePath)); + $appendFiles[$filePath] = $data; + + return true; + }); + + artisan(GenerateServer::class, $parameters); + + $appendData = [ + 'POST /resources:test-generate-without-properties 200', + 'POST /resources:test-empty-rename-request 200', + 'POST /resources:test-rename-request 200', + 'POST /resources:test-laravel-validations-application-json-request 200', + 'POST /resources:test-laravel-validations-multipart-form-data-request 200', + 'POST /resources:test-laravel-validations-non-available-content-type 200', + 'POST /resources:test-generate-resource-bad-response-key 200', + 'POST /resources:test-generate-without-properties 200', + ]; + + assertEquals(isset($appendFiles[$existTest]), $withControllerEntity); + + if ($withControllerEntity) { + $appendTestData = $appendFiles[$existTest]; + foreach ($appendData as $data) { + assertStringContainsString($data, $appendTestData); + } + } +})->with([ + [['-e' => 'pest_tests'], false], + [['-e' => 'controllers,pest_tests'], true], + [['-e' => 'pest_tests,controllers'], true], + [[], true], +]); diff --git a/tests/TestCase.php b/tests/TestCase.php index e5247b0..46b2161 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,10 +3,17 @@ namespace Ensi\LaravelOpenApiServerGenerator\Tests; use Ensi\LaravelOpenApiServerGenerator\LaravelOpenApiServerGeneratorServiceProvider; +use Ensi\LaravelOpenApiServerGenerator\Utils\ClassParser; use Orchestra\Testbench\TestCase as Orchestra; class TestCase extends Orchestra { + protected function setUp(): void + { + parent::setUp(); + $this->mockClassParserGenerator(); + } + protected function getPackageProviders($app) { return [ @@ -19,6 +26,24 @@ public function makeFilePath(string $path): string return str_replace('/', DIRECTORY_SEPARATOR, $path); } + public function mockClassParserGenerator(): void + { + $parser = $this->mock(ClassParser::class); + + $parser->shouldReceive('parse')->andReturnSelf(); + $parser->shouldReceive('hasMethod')->andReturn(false); + $parser->shouldReceive('getContentWithAdditionalMethods')->andReturnArg(0); + } + + /** + * Откатывает действие метода mockClassParserGenerator + * @return void + */ + protected function forgetMockClassParserGenerator(): void + { + $this->forgetMock(ClassParser::class); + } + public function getEnvironmentSetUp($app) { config()->set('database.default', 'testing'); diff --git a/tests/expects/Controllers/LaravelEmptyController.php b/tests/expects/Controllers/LaravelEmptyController.php new file mode 100644 index 0000000..40835ac --- /dev/null +++ b/tests/expects/Controllers/LaravelEmptyController.php @@ -0,0 +1,10 @@ +