diff --git a/app/Console/Commands/ShiftCodeceptionToPhpUnit.php b/app/Console/Commands/ShiftCodeceptionToPhpUnit.php new file mode 100644 index 0000000..8936e2b --- /dev/null +++ b/app/Console/Commands/ShiftCodeceptionToPhpUnit.php @@ -0,0 +1,35 @@ + 'shift:CodeceptionToPhpUnit']); + (new CodeceptionToLaravelTests)->run(config('shift.project_path')); + } +} diff --git a/app/Shift/LaravelShiftFiles/LaravelTests/CreatesApplication.php b/app/Shift/LaravelShiftFiles/LaravelTests/CreatesApplication.php new file mode 100644 index 0000000..6c64fde --- /dev/null +++ b/app/Shift/LaravelShiftFiles/LaravelTests/CreatesApplication.php @@ -0,0 +1,20 @@ +make(Kernel::class)->bootstrap(); + + return $app; + } +} diff --git a/app/Shift/LaravelShiftFiles/LaravelTests/Support/HttpMock/HttpMock.php b/app/Shift/LaravelShiftFiles/LaravelTests/Support/HttpMock/HttpMock.php new file mode 100644 index 0000000..e7e444d --- /dev/null +++ b/app/Shift/LaravelShiftFiles/LaravelTests/Support/HttpMock/HttpMock.php @@ -0,0 +1,43 @@ +url; + } + + public function addResponse(HttpMockResponse $response): self + { + $this->responses[] = $response; + + return $this; + } + + public function buildMock(): void + { + Http::fake([ + $this->url => function (Request $request) { + foreach ($this->responses as $response) { + if ($response->matchesRequest($request)) { + return $response->asHttpResponse(); + } + } + }, + ]); + } +} diff --git a/app/Shift/LaravelShiftFiles/LaravelTests/Support/HttpMock/HttpMockResponse.php b/app/Shift/LaravelShiftFiles/LaravelTests/Support/HttpMock/HttpMockResponse.php new file mode 100644 index 0000000..66561ed --- /dev/null +++ b/app/Shift/LaravelShiftFiles/LaravelTests/Support/HttpMock/HttpMockResponse.php @@ -0,0 +1,58 @@ +responseConditions === null) { + $this->responseConditions = new MockResponseConditions($this); + } + + return $this->responseConditions; + } + + public function respondWithBody(null|array|string $body = ''): self + { + $this->body = $body; + + return $this; + } + + public function respondWithStatusCode(int $statusCode): self + { + $this->statusCode = $statusCode; + + return $this; + } + + public function withHeaders(array $headers): self + { + $this->headers = $headers; + + return $this; + } + + public function matchesRequest(Request $request): bool + { + return $this->responseConditions === null || $this->responseConditions->conditionsMatchRequest($request); + } + + public function asHttpResponse(): PromiseInterface + { + return Http::response($this->body, $this->statusCode, $this->headers); + } +} diff --git a/app/Shift/LaravelShiftFiles/LaravelTests/Support/HttpMock/MockResponseConditions.php b/app/Shift/LaravelShiftFiles/LaravelTests/Support/HttpMock/MockResponseConditions.php new file mode 100644 index 0000000..71e9a86 --- /dev/null +++ b/app/Shift/LaravelShiftFiles/LaravelTests/Support/HttpMock/MockResponseConditions.php @@ -0,0 +1,62 @@ +requestMethod = 'GET'; + + return $this; + } + + public function patch(): self + { + $this->requestMethod = 'PATCH'; + + return $this; + } + + public function post(): self + { + $this->requestMethod = 'POST'; + + return $this; + } + + public function delete(): self + { + $this->requestMethod = 'DELETE'; + + return $this; + } + + public function bodyIsContaining(string $requestBody): self + { + $this->bodyContains = $requestBody; + + return $this; + } + + public function then(): HttpMockResponse + { + return $this->response; + } + + public function conditionsMatchRequest(Request $request): bool + { + return ($this->requestMethod === '' || $request->method() === $this->requestMethod) + && ($this->bodyContains === null || str_contains($request->body(), $this->bodyContains)); + } +} diff --git a/app/Shift/LaravelShiftFiles/LaravelTests/TestCase.php b/app/Shift/LaravelShiftFiles/LaravelTests/TestCase.php new file mode 100644 index 0000000..5d80a81 --- /dev/null +++ b/app/Shift/LaravelShiftFiles/LaravelTests/TestCase.php @@ -0,0 +1,105 @@ +mockFileContent( + $directory, + $mockFilename, + 'Response', + $type + ); + } + + public function mockRequest( + string $directory, + string $mockFilename, + string $type = null + ): array { + return json_decode($this->mockFileContent( + $directory, + $mockFilename, + 'Request', + $type + ), true); + } + + public function outgoingRequest(string $url): Request + { + return $this->outgoingRequests($url)->first()[0]; + } + + public function outgoingRequests(string $url, string $method = null, string $body = null, bool $bodyMatchExact = false): Collection + { + return Http::recorded(function (Request $request, Response $response) use ($url, $method, $body, $bodyMatchExact) { + return str_contains($request->toPsrRequest()->getUri()->getPath(), $url) + && (! isset($method) || $request->toPsrRequest()->getMethod() === $method) + && (! isset($body) || ( + ($bodyMatchExact && $request->toPsrRequest()->getBody()->getContents() === $body) + || (! $bodyMatchExact && str_contains($request->toPsrRequest()->getBody()->getContents(), $body)) + ) + ); + })->values(); + } + + public static function httpMockResponse(): HttpMockResponse + { + return new HttpMockResponse(); + } + + protected function mockActiveTokenResponse(): void + { + Http::fake( + [config('services.oauth.url').'/token_info' => Http::response( + ['active' => true], + 200, + ['Content-Type', 'application/json; charset=utf-8'] + )] + ); + } + + private function mockFileContent( + string $directory, + string $mockFilename, + string $type = null, + string $subType = null + ): string { + return file_get_contents((new Collection( + [$directory, 'Mock', $type, $subType, $mockFilename] + ))->filter()->map(function ($value, $key) { + return ($key === 0 ? 'rtrim' : 'trim')($value, '/'); + })->join('/')); + } +} diff --git a/app/Shift/LaravelShiftFiles/LaravelTests/phpunit.xml b/app/Shift/LaravelShiftFiles/LaravelTests/phpunit.xml new file mode 100644 index 0000000..7248ff7 --- /dev/null +++ b/app/Shift/LaravelShiftFiles/LaravelTests/phpunit.xml @@ -0,0 +1,29 @@ + + + + + ./tests/Feature + + + ./tests/Unit + + + + + + + + + + + + + + ./app + + + diff --git a/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/AddResponseAsParam.php b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/AddResponseAsParam.php new file mode 100644 index 0000000..ff6eb34 --- /dev/null +++ b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/AddResponseAsParam.php @@ -0,0 +1,62 @@ +chainedCall(), but actually will be converted to something->chainedCall(), due to afterTraverse regex modification +class AddResponseAsParam extends AbstractRector +{ + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + /** @param ClassMethod $node */ + public function refactor(Node $node): ?Node + { + if (! $this->hasResponseVariable($node) || $this->hasResponseParameter($node)) { + return null; + } + + $responseParam = new Node\Param(new Variable('response'), type: new Node\Name('Illuminate\Testing\TestResponse')); + $node->params[] = $responseParam; + + return $node; + } + + private function hasResponseVariable(ClassMethod $node): bool + { + return (bool) $this->betterNodeFinder->findFirst($node->stmts, function (Node $node): bool { + return $node instanceof Variable && $this->getName($node) === 'response'; + }); + } + + private function hasResponseParameter(ClassMethod $node): bool + { + foreach ($node->params as $param) { + if ($this->getName($param->var) === 'response') { + return true; + } + } + + return false; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Upgrade Monolog method signatures and array usage to object usage', [ + new CodeSample( + // code before + 'public function handle(array $record) { return $record[\'context\']; }', + // code after + 'public function handle(\Monolog\LogRecord $record) { return $record->context; }' + ), + ]); + } +} diff --git a/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/AddResponseAsParamWhenCaller.php b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/AddResponseAsParamWhenCaller.php new file mode 100644 index 0000000..3c480cc --- /dev/null +++ b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/AddResponseAsParamWhenCaller.php @@ -0,0 +1,62 @@ +chainedCall(), but actually will be converted to something->chainedCall(), due to afterTraverse regex modification +class AddResponseAsParamWhenCaller extends AbstractRector +{ + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + /** @param ClassMethod $node */ + public function refactor(Node $node): ?Node + { + if (! $this->hasResponseVariable($node) || $this->hasResponseParameter($node)) { + return null; + } + + $responseParam = new Node\Param(new Variable('response'), type: new Node\Name('Illuminate\Testing\TestResponse')); + $node->params[] = $responseParam; + + return $node; + } + + private function hasResponseVariable(ClassMethod $node): bool + { + return (bool) $this->betterNodeFinder->findFirst($node->stmts, function (Node $node): bool { + return $node instanceof Variable && $this->getName($node) === 'response'; + }); + } + + private function hasResponseParameter(ClassMethod $node): bool + { + foreach ($node->params as $param) { + if ($this->getName($param->var) === 'response') { + return true; + } + } + + return false; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Upgrade Monolog method signatures and array usage to object usage', [ + new CodeSample( + // code before + 'public function handle(array $record) { return $record[\'context\']; }', + // code after + 'public function handle(\Monolog\LogRecord $record) { return $record->context; }' + ), + ]); + } +} diff --git a/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/AddTestAttributeForTests.php b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/AddTestAttributeForTests.php new file mode 100644 index 0000000..37393e7 --- /dev/null +++ b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/AddTestAttributeForTests.php @@ -0,0 +1,57 @@ +hasTestAttribute($node)) { + return null; + } + if (str_ends_with($this->file->getFilePath(), 'Cest.php') && $node->isPublic()) { + $node->attrGroups = array_merge([new Node\AttributeGroup([ + new Node\Attribute(new Node\Name('PHPUnit\Framework\Attributes\Test')), + ])], $node->attrGroups ?? []); + } + + return $node; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Upgrade Monolog method signatures and array usage to object usage', [ + new CodeSample( + // code before + 'public function handle(array $record) { return $record[\'context\']; }', + // code after + 'public function handle(\Monolog\LogRecord $record) { return $record->context; }' + ), + ]); + } + + private function hasTestAttribute(ClassMethod $node): bool + { + foreach ($node->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attr) { + if (in_array($this->getName($attr->name), ['Test', 'PHPUnit\Framework\Attributes\Test'])) { + return true; + } + } + } + + return false; + } +} diff --git a/app/Shift/Rector/CodeceptionToLaravel/Rules/ChainResponseCodes.php b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/ChainResponseCodes.php similarity index 80% rename from app/Shift/Rector/CodeceptionToLaravel/Rules/ChainResponseCodes.php rename to app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/ChainResponseCodes.php index d1f9d29..2f7b22e 100644 --- a/app/Shift/Rector/CodeceptionToLaravel/Rules/ChainResponseCodes.php +++ b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/ChainResponseCodes.php @@ -1,14 +1,9 @@ chainedCall(), but actually will be converted to something->chainedCall(), due to afterTraverse regex modification class ChainResponseCodes extends AbstractRector { - private array $sendMethods = ['sendGET', 'sendPOST', 'sendPATCH', 'sendDELETE', 'getJson', 'postJson', 'patchJson', 'deleteJson']; + private array $sendMethods = ['sendGET', 'sendPOST', 'sendPATCH', 'sendDELETE', 'getJson', 'postJson', 'patchJson', 'deleteJson', 'sendGet', 'sendPatch', 'sendDelete', 'sendPost']; + private array $codes = [ 'CONTINUE' => 100, 'PROCESSING' => 102, @@ -79,7 +75,7 @@ class ChainResponseCodes extends AbstractRector 'INSUFFICIENT_STORAGE' => 507, 'LOOP_DETECTED' => 508, 'NOT_EXTENDED' => 510, - 'NETWORK_AUTHENTICATION_REQUIRED' => 511 + 'NETWORK_AUTHENTICATION_REQUIRED' => 511, ]; public function getNodeTypes(): array @@ -89,18 +85,18 @@ public function getNodeTypes(): array public function refactor(Node $node): ?Node { - $methodStmts = array_filter($node->stmts, function ($stmt){ - return $stmt->expr instanceof MethodCall; + $methodStmts = array_filter($node->stmts ?? [], function ($stmt) { + return isset($stmt->expr) && $stmt->expr instanceof MethodCall; }); $sendMethod = null; /** @var MethodCall $stmnt */ - foreach ($methodStmts as $key => $stmnt){ - if(in_array($stmnt->expr?->name?->name, $this->sendMethods)){ + foreach ($methodStmts as $key => $stmnt) { + if (in_array($stmnt->expr?->name?->name, $this->sendMethods)) { $sendMethod = $key; } - if($stmnt->expr?->name?->name === 'seeResponseCodeIs') { + if ($stmnt->expr?->name?->name === 'seeResponseCodeIs') { if (isset($sendMethod)) { - if($stmnt->expr->args[0]->value instanceof Node\Scalar\LNumber){ + if ($stmnt->expr->args[0]->value instanceof Node\Scalar\LNumber) { $code = $stmnt->expr->args[0]->value->value; } else { $code = $this->codes[$stmnt->expr->args[0]->value->name->name]; @@ -115,20 +111,21 @@ public function refactor(Node $node): ?Node return $node; } - public function afterTraverse(array $nodes) { - $file = preg_replace('#\((\$response = (\$I|\$this)->(getJson|postJson|patchJson|deleteJson)\([\s\S]*?\))\)->#ms', '$1->', $this->file->getFileContent()); - file_put_contents($this->file->getFilePath(), $file); + if (! in_array('--dry-run', $_SERVER['argv'])) { + $file = preg_replace('#\((\$response = (\$I|\$this)->(getJson|postJson|patchJson|deleteJson)\([\s\S]*?\))\)->#ms', '$1->', $this->file->getFileContent()); + file_put_contents($this->file->getFilePath(), $file); + } - return null; + return parent::afterTraverse($nodes); } public function getRuleDefinition(): RuleDefinition { return new RuleDefinition('Upgrade Monolog method signatures and array usage to object usage', [ new CodeSample( - // code before + // code before 'public function handle(array $record) { return $record[\'context\']; }', // code after 'public function handle(\Monolog\LogRecord $record) { return $record->context; }' diff --git a/app/Shift/Rector/CodeceptionToLaravel/Rules/ExampleToTestWithDocs.php b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/ExampleToTestWithDocs.php similarity index 60% rename from app/Shift/Rector/CodeceptionToLaravel/Rules/ExampleToTestWithDocs.php rename to app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/ExampleToTestWithDocs.php index bc338a7..09a87a8 100644 --- a/app/Shift/Rector/CodeceptionToLaravel/Rules/ExampleToTestWithDocs.php +++ b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/ExampleToTestWithDocs.php @@ -1,23 +1,12 @@ getDocComment(); - if(!isset($docComment)){ + if (! isset($docComment)) { + return null; + } + preg_match_all('#@example\s+?([\[|\{].+?[\]|\}])\s#ms', $docComment->getText(), $matches); + $node->setDocComment(new Doc(preg_replace('#\s+?\*\s+?@example\s+?([\[|\{].+?[\]|\}])\s#ms', '', $docComment->getText()))); + $customKeys = []; + if (empty($matches[1])) { return null; } - preg_match_all('#@example\s+?(\[.+?\])\s#ms', $docComment->getText(), $matches); - $node->setDocComment(new Doc(preg_replace('#\s+?\*\s+?@example\s+?(\[.+?\])\s#ms', '', $docComment->getText()))); foreach ($matches[1] as $tag) { $array = json_decode($tag, true); + $arrayWithCustomKeys = empty(array_filter(array_keys($array), function ($item) { + return preg_match('/[0-9]/ms', $item, $matchesss) === 1; + })); + if ($arrayWithCustomKeys) { + $customKeys = array_merge($customKeys, array_keys($array)); + $array = array_values($array); + } $args = $this->nodeFactory->createArg($array); $node->attrGroups[] = new Node\AttributeGroup([ - new Node\Attribute(new Node\Name('\PHPUnit\Framework\Attributes\TestWith'), [$args]), + new Node\Attribute(new Node\Name('PHPUnit\Framework\Attributes\TestWith'), [$args]), ]); } foreach ($node->params as $key => $param) { if ($this->getType($param)->getObjectClassNames()[0] === 'Codeception\Example') { $this->exampleName = $param->var->name; unset($node->params[$key]); + $node->params = array_values($node->params); } } + $paramCount = count($node->params); foreach ($node->attrGroups as $group) { $providerItems = $group->attrs[0]->args[0]->value->items; - $i = 1; + $i = $paramCount; foreach ($providerItems as $item) { - $node->params[$i] = new Node\Param(new Variable('argumentFromProvider' . $i-1), type: get_debug_type($item->value->value)); + if (! isset($this->params[$i])) { + $paramName = $arrayWithCustomKeys ? $customKeys[$i - $paramCount] : $i - $paramCount; + $node->params[$i] = new Node\Param(new Variable('argumentFromProvider'.$paramName), type: get_debug_type($item->value->value)); + } $i++; } } - $this->traverseNodesWithCallable($node, function (Node $nodeStatement) use ($node) { + $this->traverseNodesWithCallable($node, function (Node $nodeStatement) { if ($nodeStatement instanceof Node\Expr\ArrayDimFetch && $nodeStatement->var->name === $this->exampleName) { - return new Variable('argumentFromProvider' . $nodeStatement->dim->value); + return new Variable('argumentFromProvider'.$nodeStatement->dim->value); } + return null; }); @@ -77,7 +83,7 @@ public function getRuleDefinition(): RuleDefinition { return new RuleDefinition('Upgrade Monolog method signatures and array usage to object usage', [ new CodeSample( - // code before + // code before 'public function handle(array $record) { return $record[\'context\']; }', // code after 'public function handle(\Monolog\LogRecord $record) { return $record->context; }' diff --git a/app/Shift/Rector/CodeceptionToLaravel/Rules/RefactorGetResponse.php b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/RefactorGetResponse.php similarity index 74% rename from app/Shift/Rector/CodeceptionToLaravel/Rules/RefactorGetResponse.php rename to app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/RefactorGetResponse.php index bd8f81c..e6dcb47 100644 --- a/app/Shift/Rector/CodeceptionToLaravel/Rules/RefactorGetResponse.php +++ b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/RefactorGetResponse.php @@ -1,12 +1,8 @@ nodeTypeResolver->isMethodStaticCallOrClassMethodObjectType($node, new ObjectType('ApiTester'))) { + if (! $this->nodeTypeResolver->isMethodStaticCallOrClassMethodObjectType($node, new ObjectType('ApiTester'))) { return null; } - $rename = $this->methodRenames[$this->getName($node->name)] ?? null; if ($this->getName($node->name) !== 'grabResponse') { return null; } @@ -36,12 +30,11 @@ public function refactor(Node $node): ?Node return $node; } - public function getRuleDefinition(): RuleDefinition { return new RuleDefinition('Upgrade Monolog method signatures and array usage to object usage', [ new CodeSample( - // code before + // code before 'public function handle(array $record) { return $record[\'context\']; }', // code after 'public function handle(\Monolog\LogRecord $record) { return $record->context; }' diff --git a/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/RefactorMockAccess.php b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/RefactorMockAccess.php new file mode 100644 index 0000000..ac8e872 --- /dev/null +++ b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/RefactorMockAccess.php @@ -0,0 +1,102 @@ +stmts, function ($stmnt) { + return $stmnt instanceof ClassMethod; + }); + foreach ($classMethods as $method) { + $this->traverseNodesWithCallable($method, function ($methodNode) use ($classMethods) { + if (isset($methodNode->name->name) && $methodNode->name->name === 'grabRequestsMadeToRemoteService') { + $condition = $methodNode->args[0]->value; + if ($condition->var->name === 'this') { + $method = $this->nodeFinder->find($classMethods, function ($node) use ($condition) { + return $node instanceof ClassMethod && $node->name->name === $condition->name->name; + }); + $condition = $method[0]->stmts; + } + $requestMethodNode = $this->nodeFinder->findFirst($condition, function ($stmt) { + return ($stmt instanceof MethodCall || $stmt instanceof Node\Expr\StaticCall) && in_array($stmt->name, + $this->phiremockRequestConditions); + }); + $requestUrls = $this->nodeFinder->findFirst($condition, function ($stmt) { + return $stmt instanceof MethodCall && $stmt->name->name === 'andUrl'; + }); + $bodyCondition = $this->nodeFinder->findFirst($condition, function ($stmt) { + return $stmt instanceof MethodCall && $stmt->name->name === 'andBody'; + }); + $strictBody = $bodyCondition->args[0]->value->name->name ?? '' === 'equalTo'; + $toUrl = $requestUrls->args[0]->value->args[0]->value; + $toUrl = $toUrl instanceof Node\Scalar\String_ ? $toUrl->value : $toUrl; + $withMethod = strtoupper(str_replace('Request', '', $requestMethodNode->name->name)); + $body = $bodyCondition->args[0]->value->args[0]->value ?? null; + $body = $body instanceof Node\Scalar\String_ ? $body->value ?? null : $body; + $methodNode = new MethodCall( + new Node\Expr\Variable('this'), + 'outgoingRequests', + [ + $this->nodeFactory->createArg($toUrl ?? ''), + ...(! empty($withMethod) ? [$this->nodeFactory->createArg($withMethod)] : []), + ...(isset($body) ? [$this->nodeFactory->createArg($body), $this->nodeFactory->createArg($strictBody)] : []), + ] + ); + + return $methodNode; + } + + return $methodNode; + }); + } + + return $node; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Upgrade Monolog method signatures and array usage to object usage', [ + new CodeSample( + // code before + 'public function handle(array $record) { return $record[\'context\']; }', + // code after + 'public function handle(\Monolog\LogRecord $record) { return $record->context; }' + ), + ]); + } +} diff --git a/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/RefactorMockCreation.php b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/RefactorMockCreation.php new file mode 100644 index 0000000..f667497 --- /dev/null +++ b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/RefactorMockCreation.php @@ -0,0 +1,44 @@ +getName($node) !== 'expectARequestToRemoteServiceWithAResponse') { + return null; + } + + return $node; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Upgrade Monolog method signatures and array usage to object usage', [ + new CodeSample( + // code before + 'public function handle(array $record) { return $record[\'context\']; }', + // code after + 'public function handle(\Monolog\LogRecord $record) { return $record->context; }' + ), + ]); + } +} diff --git a/app/Shift/Rector/CodeceptionToLaravel/Rules/RenameApiTesterMethod.php b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/RenameApiTesterMethod.php similarity index 76% rename from app/Shift/Rector/CodeceptionToLaravel/Rules/RenameApiTesterMethod.php rename to app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/RenameApiTesterMethod.php index b91fefe..e912a40 100644 --- a/app/Shift/Rector/CodeceptionToLaravel/Rules/RenameApiTesterMethod.php +++ b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/RenameApiTesterMethod.php @@ -1,9 +1,7 @@ 'withHeader', 'sendGET' => 'getJson', + 'sendGet' => 'getJson', 'sendPOST' => 'postJson', + 'sendPost' => 'postJson', 'sendPATCH' => 'patchJson', + 'sendPatch' => 'patchJson', 'sendDELETE' => 'deleteJson', + 'sendDelete' => 'deleteJson', ]; public function getNodeTypes(): array @@ -30,15 +32,15 @@ public function getNodeTypes(): array public function refactor(Node $node): ?Node { - if (!$this->nodeTypeResolver->isMethodStaticCallOrClassMethodObjectType($node, new ObjectType('ApiTester'))) { + if (! $this->nodeTypeResolver->isMethodStaticCallOrClassMethodObjectType($node, new ObjectType('ApiTester'))) { return null; } $rename = $this->methodRenames[$this->getName($node->name)] ?? null; - if (!isset($rename)) { + if (! isset($rename)) { return null; } $node->name = new Node\Identifier($rename); - if(in_array($node->name, ['getJson', 'postJson', 'patchJson', 'deleteJson'])){ + if (in_array($node->name, ['getJson', 'postJson', 'patchJson', 'deleteJson'])) { /** @var MethodCall $node */ $variable = new Variable('response'); $node = new Assign($variable, $node); @@ -47,13 +49,11 @@ public function refactor(Node $node): ?Node return $node; } - - public function getRuleDefinition(): RuleDefinition { return new RuleDefinition('Upgrade Monolog method signatures and array usage to object usage', [ new CodeSample( - // code before + // code before 'public function handle(array $record) { return $record[\'context\']; }', // code after 'public function handle(\Monolog\LogRecord $record) { return $record->context; }' diff --git a/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/ResponseCodesToAsserts.php b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/ResponseCodesToAsserts.php new file mode 100644 index 0000000..f9cdba1 --- /dev/null +++ b/app/Shift/Rector/CodeceptionToLaravel/RulesFirstRun/ResponseCodesToAsserts.php @@ -0,0 +1,86 @@ +chainedCall(), but actually will be converted to something->chainedCall(), due to afterTraverse regex modification +class ResponseCodesToAsserts extends AbstractRector +{ + public function getNodeTypes(): array + { + return [MethodCall::class]; + } + + public function refactor(Node $node): ?Node + { + if (! $this->isName($node->name, 'assertStatus')) { + return null; + } + + if (count($node->getArgs()) !== 1) { + return null; + } + + $arg = $node->getArgs()[0]; + $argValue = $arg->value; + + if (! $argValue instanceof LNumber) { + return null; + } + $replacementMethod = match ($argValue->value) { + 200 => 'assertOk', + 201 => 'assertCreated', + 202 => 'assertAccepted', + 204 => 'assertNoContent', + 301 => 'assertMovedPermanently', + 302 => 'assertFound', + 304 => 'assertNotModified', + 307 => 'assertTemporaryRedirect', + 308 => 'assertPermanentRedirect', + 400 => 'assertBadRequest', + 401 => 'assertUnauthorized', + 402 => 'assertPaymentRequired', + 403 => 'assertForbidden', + 404 => 'assertNotFound', + 405 => 'assertMethodNotAllowed', + 406 => 'assertNotAcceptable', + 408 => 'assertRequestTimeout', + 409 => 'assertConflict', + 410 => 'assertGone', + 415 => 'assertUnsupportedMediaType', + 422 => 'assertUnprocessable', + 429 => 'assertTooManyRequests', + 500 => 'assertInternalServerError', + 503 => 'assertServiceUnavailable', + default => null + }; + + if ($replacementMethod === null) { + return null; + } + + $node->name = new Identifier($replacementMethod); + $node->args = []; + + return $node; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Upgrade Monolog method signatures and array usage to object usage', [ + new CodeSample( + // code before + 'public function handle(array $record) { return $record[\'context\']; }', + // code after + 'public function handle(\Monolog\LogRecord $record) { return $record->context; }' + ), + ]); + } +} diff --git a/app/Shift/Rector/CodeceptionToLaravel/RulesSecondRun/RefactorClassToPhpUnitTestCase.php b/app/Shift/Rector/CodeceptionToLaravel/RulesSecondRun/RefactorClassToPhpUnitTestCase.php new file mode 100644 index 0000000..333731f --- /dev/null +++ b/app/Shift/Rector/CodeceptionToLaravel/RulesSecondRun/RefactorClassToPhpUnitTestCase.php @@ -0,0 +1,39 @@ +name->name, 'Cest')) { + $node->name->name = str_replace('Cest', 'Test', $node->name->name); + $node->extends = new Name('Tests\TestCase'); + } + + return $node; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Upgrade Monolog method signatures and array usage to object usage', [ + new CodeSample( + // code before + 'public function handle(array $record) { return $record[\'context\']; }', + // code after + 'public function handle(\Monolog\LogRecord $record) { return $record->context; }' + ), + ]); + } +} diff --git a/app/Shift/Rector/CodeceptionToLaravel/RulesSecondRun/ReplaceApiTesterObject.php b/app/Shift/Rector/CodeceptionToLaravel/RulesSecondRun/ReplaceApiTesterObject.php new file mode 100644 index 0000000..501622c --- /dev/null +++ b/app/Shift/Rector/CodeceptionToLaravel/RulesSecondRun/ReplaceApiTesterObject.php @@ -0,0 +1,48 @@ +chainedCall(), but actually will be converted to something->chainedCall(), due to afterTraverse regex modification +class ReplaceApiTesterObject extends AbstractRector +{ + public function getNodeTypes(): array + { + return [Node\Stmt\Class_::class]; + } + + /** + * @param Node\Stmt\Class_ $node + */ + public function refactor(Node $node): ?Node + { + if (str_ends_with($node->name->name, 'Cest') || str_ends_with($node->name->name, 'Test')) { + return null; + } + $this->traverseNodesWithCallable($node->stmts, function (Node $node) { + if ($node instanceof Variable) { + return $node; + } + }); + + return $node; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Upgrade Monolog method signatures and array usage to object usage', [ + new CodeSample( + // code before + 'public function handle(array $record) { return $record[\'context\']; }', + // code after + 'public function handle(\Monolog\LogRecord $record) { return $record->context; }' + ), + ]); + } +} diff --git a/app/Shift/Rector/CodeceptionToLaravel/rector.php b/app/Shift/Rector/CodeceptionToLaravel/rector.php deleted file mode 100644 index 26f3e95..0000000 --- a/app/Shift/Rector/CodeceptionToLaravel/rector.php +++ /dev/null @@ -1,23 +0,0 @@ -rule(RenameApiTesterMethod::class); - $rectorConfig->rule(RefactorGetResponse::class); - $rectorConfig->rule(ChainResponseCodes::class); - $rectorConfig->rule(ExampleToTestWithDocs::class); - $rectorConfig->importNames(); -// $rectorConfig->rules([ -// ExtendTestCase::class, -// -// ]) -}; diff --git a/app/Shift/Rector/CodeceptionToLaravel/rectorFirstRun.php b/app/Shift/Rector/CodeceptionToLaravel/rectorFirstRun.php new file mode 100644 index 0000000..22cc071 --- /dev/null +++ b/app/Shift/Rector/CodeceptionToLaravel/rectorFirstRun.php @@ -0,0 +1,32 @@ +rule(RenameApiTesterMethod::class); + $rectorConfig->rule(RefactorMockAccess::class); + $rectorConfig->rule(RefactorGetResponse::class); + $rectorConfig->rule(ChainResponseCodes::class); + $rectorConfig->rule(ExampleToTestWithDocs::class); + $rectorConfig->rule(ResponseCodesToAsserts::class); + $rectorConfig->rule(RefactorMockCreation::class); + $rectorConfig->rule(AddTestAttributeForTests::class); + // $rectorConfig->rule(AddResponseAsParam::class); Need to improve + // $rectorConfig->rule(AddResponseAsParamWhenCaller::class) Add to second run and check if method has it as a caller + + $rectorConfig->importNames(); + $rectorConfig->removeUnusedImports(); +}; diff --git a/app/Shift/Rector/CodeceptionToLaravel/rectorSecondRun.php b/app/Shift/Rector/CodeceptionToLaravel/rectorSecondRun.php new file mode 100644 index 0000000..996b34d --- /dev/null +++ b/app/Shift/Rector/CodeceptionToLaravel/rectorSecondRun.php @@ -0,0 +1,15 @@ +rule(ReplaceApiTesterObject::class); + $rectorConfig->rule(RefactorClassToPhpUnitTestCase::class); + + $rectorConfig->importNames(); +}; diff --git a/app/Shift/Shifts/CodeceptionToLaravelTests.php b/app/Shift/Shifts/CodeceptionToLaravelTests.php new file mode 100644 index 0000000..2d09d71 --- /dev/null +++ b/app/Shift/Shifts/CodeceptionToLaravelTests.php @@ -0,0 +1,96 @@ +addTestFiles(app_path('/Shift/LaravelShiftFiles/LaravelTests/'), $directory); + $this->runRector($directory); + // $this->fixTestFileFormatting($directory); + } + + // private function addLaravelFiles(string $sourceDirectory, string $destinationDirectory): void + // { + // $filesAndDirectories = scandir($sourceDirectory) ?: throw new Exception('Failed to scan directory'); + // unset($filesAndDirectories[array_search('.', $filesAndDirectories, true)]); + // unset($filesAndDirectories[array_search('..', $filesAndDirectories, true)]); + // + // if (count($filesAndDirectories) < 1) { + // return; + // } + // + // foreach ($filesAndDirectories as $fileOrDirectory) { + // $sourcePath = $sourceDirectory.'/'.$fileOrDirectory; + // $destinationPath = $destinationDirectory.'/'.$fileOrDirectory; + // + // if (is_dir($sourcePath)) { + // if (! is_dir($destinationPath)) { + // mkdir($destinationPath, 0755, true); + // } + // + // $this->addLaravelFiles($sourcePath, $destinationPath); + // } elseif (! file_exists($destinationPath) || str_contains($destinationPath, 'config/app.php') || str_contains($destinationPath, 'index.php')) { + // copy($sourcePath, $destinationPath); + // } elseif (file_exists($destinationPath)) { + // $this->overLappingFiles[] = $destinationPath; + // } + // } + // } + // + + private function addTestFiles(string $app_path, string $directory) + { + } + + private function runRector($directory) + { + $process = new Process(['vendor/bin/rector', 'process', 'C:\Users\martins.rucevskis\projects\product-server\web\tests\api\Travel\Estonia\TravelRequestCest.php', '--config', app_path('\Shift\Rector\CodeceptionToLaravel\rectorFirstRun.php'), '--xdebug', '--debug', '--dry-run'], null, null, null, 300); + $process->run(); + echo 'Rector Changes from First run : '.PHP_EOL; + echo $process->getOutput(); + + $process = new Process(['vendor/bin/rector', 'process', 'C:\Users\martins.rucevskis\projects\product-server\web\tests\api\Travel\Estonia\TravelRequestCest.php', '--config', app_path('\Shift\Rector\CodeceptionToLaravel\rectorSecondRun.php'), '--xdebug', '--debug', '--dry-run'], null, null, null, 300); + $process->run(); + echo 'Rector Changes from Second run : '.PHP_EOL; + echo $process->getOutput(); + } + + private function fixTestFileFormatting(string $sourceDirectory) + { + $directory = opendir($sourceDirectory); + if ($directory === false) { + throw new Exception("Unable to open directory: $sourceDirectory"); + } + + while (false !== ($file = readdir($directory))) { + if ($file === '.' || $file === '..') { + continue; + } + + if (is_dir("$sourceDirectory/$file") === true) { + $this->fixTestFileFormatting("$sourceDirectory/$file"); + } else { + if (str_ends_with($file, 'Cest.php')) { + $this->fixTestAnnotations($sourceDirectory.$file); + rename($sourceDirectory.'/'.$file, str_replace('Cest.php', 'Test.php', $file)); + } + } + } + } + + private function fixTestAnnotations(string $string) + { + } +} diff --git a/tests/Feature/FixProject/FixProjectTest.php b/tests/Feature/FixProject/FixProjectTest.php index 7244929..47f0dce 100644 --- a/tests/Feature/FixProject/FixProjectTest.php +++ b/tests/Feature/FixProject/FixProjectTest.php @@ -14,9 +14,14 @@ protected function setUp(): void parent::setUp(); } + #[\PHPUnit\Framework\Attributes\TestWith()] public function test_migrate_project(): void { $this->markTestIncomplete('Need to traverse whole arrays, provide files for github actions'); + $response = $this->getJson(); + $response->assertTemporaryRedirect(); + + $response->assertStatus(); Artisan::call('shift:Lumen8ToLaravel8'); $this->assertEquals( file_get_contents(__DIR__.'/Resources/TestProject/app/TestController.php'),