From 3a6c562a18aad822e08080e487bd0fba0e598e16 Mon Sep 17 00:00:00 2001 From: Jesse Rushlow Date: Fri, 29 Mar 2024 11:58:19 -0400 Subject: [PATCH 1/3] query for multiple objects in single request --- src/Factory/Model/GitHubIssueFactory.php | 85 +++++++ .../Model/GitHubPullRequestFactory.php | 85 +++++++ src/Factory/Model/GitHubUrlDataFactory.php | 96 +++++++ src/Factory/TaskFactory.php | 11 + src/GraphQLRequest/_issue.fragment.graphql | 3 + src/GraphQLRequest/_issue.template.php | 3 + src/GraphQLRequest/_pr.fragment.graphql | 3 + src/GraphQLRequest/_pr.template.php | 3 + src/GraphQLRequest/repository.template.php | 7 + src/Service/TaskService.php | 160 ++++++++---- .../repository.issue.200.json | 9 + .../repository.issues.200.json | 12 + .../repository.mixed.200.json | 12 + .../repository.pull.200.json | 9 + .../repository.pulls.200.json | 12 + .../response.bug.200.json | 15 ++ tests/Functional/Service/TaskServiceTest.php | 237 +++++++++++------- 17 files changed, 627 insertions(+), 135 deletions(-) create mode 100644 src/Factory/Model/GitHubIssueFactory.php create mode 100644 src/Factory/Model/GitHubPullRequestFactory.php create mode 100644 src/Factory/Model/GitHubUrlDataFactory.php create mode 100644 src/GraphQLRequest/_issue.fragment.graphql create mode 100644 src/GraphQLRequest/_issue.template.php create mode 100644 src/GraphQLRequest/_pr.fragment.graphql create mode 100644 src/GraphQLRequest/_pr.template.php create mode 100644 src/GraphQLRequest/repository.template.php create mode 100644 tests/Functional/Service/GraphQLResponseFixture/repository.issue.200.json create mode 100644 tests/Functional/Service/GraphQLResponseFixture/repository.issues.200.json create mode 100644 tests/Functional/Service/GraphQLResponseFixture/repository.mixed.200.json create mode 100644 tests/Functional/Service/GraphQLResponseFixture/repository.pull.200.json create mode 100644 tests/Functional/Service/GraphQLResponseFixture/repository.pulls.200.json create mode 100644 tests/Functional/Service/GraphQLResponseFixture/response.bug.200.json diff --git a/src/Factory/Model/GitHubIssueFactory.php b/src/Factory/Model/GitHubIssueFactory.php new file mode 100644 index 0000000..b5d6a0f --- /dev/null +++ b/src/Factory/Model/GitHubIssueFactory.php @@ -0,0 +1,85 @@ + + * + * @method GitHubIssue|Proxy create(array|callable $attributes = []) + * @method static GitHubIssue|Proxy createOne(array $attributes = []) + * @method static GitHubIssue|Proxy find(object|array|mixed $criteria) + * @method static GitHubIssue|Proxy findOrCreate(array $attributes) + * @method static GitHubIssue|Proxy first(string $sortedField = 'id') + * @method static GitHubIssue|Proxy last(string $sortedField = 'id') + * @method static GitHubIssue|Proxy random(array $attributes = []) + * @method static GitHubIssue|Proxy randomOrCreate(array $attributes = []) + * @method static GitHubIssue[]|Proxy[] all() + * @method static GitHubIssue[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static GitHubIssue[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static GitHubIssue[]|Proxy[] findBy(array $attributes) + * @method static GitHubIssue[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static GitHubIssue[]|Proxy[] randomSet(int $number, array $attributes = []) + * + * @psalm-method GitHubIssue&Proxy create(array|callable $attributes = []) + * @psalm-method static GitHubIssue&Proxy createOne(array $attributes = []) + * @psalm-method static GitHubIssue&Proxy find(object|array|mixed $criteria) + * @psalm-method static GitHubIssue&Proxy findOrCreate(array $attributes) + * @psalm-method static GitHubIssue&Proxy first(string $sortedField = 'id') + * @psalm-method static GitHubIssue&Proxy last(string $sortedField = 'id') + * @psalm-method static GitHubIssue&Proxy random(array $attributes = []) + * @psalm-method static GitHubIssue&Proxy randomOrCreate(array $attributes = []) + * @psalm-method static GitHubIssue[]&Proxy[] all() + * @psalm-method static GitHubIssue[]&Proxy[] createMany(int $number, array|callable $attributes = []) + * @psalm-method static GitHubIssue[]&Proxy[] createSequence(iterable|callable $sequence) + * @psalm-method static GitHubIssue[]&Proxy[] findBy(array $attributes) + * @psalm-method static GitHubIssue[]&Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @psalm-method static GitHubIssue[]&Proxy[] randomSet(int $number, array $attributes = []) + */ +class GitHubIssueFactory extends ModelFactory +{ + #[\Override] + protected function getDefaults(): array + { + return [ + 'owner' => $owner = self::faker()->slug(), + 'repo' => $repository = self::faker()->slug(), + 'number' => $id = self::faker()->randomNumber(), + 'uri' => sprintf('https://github.com/%s/%s/issues/%s', + $owner, + $repository, + $id + ), + 'title' => self::faker()->sentence(), + ]; + } + + public function fromUrlData(GitHubUrlData $urlData): self + { + return $this->addState([ + 'owner' => $urlData->owner, + 'repo' => $urlData->repository, + 'number' => $urlData->identifier, + 'uri' => $urlData->uri, + 'title' => 'some sort of issue', + ]); + } + + #[\Override] + protected function initialize(): self + { + return $this + ->withoutPersisting() + ; + } + + #[\Override] + protected static function getClass(): string + { + return GitHubIssue::class; + } +} diff --git a/src/Factory/Model/GitHubPullRequestFactory.php b/src/Factory/Model/GitHubPullRequestFactory.php new file mode 100644 index 0000000..6e8d71f --- /dev/null +++ b/src/Factory/Model/GitHubPullRequestFactory.php @@ -0,0 +1,85 @@ + + * + * @method GitHubPullRequest|Proxy create(array|callable $attributes = []) + * @method static GitHubPullRequest|Proxy createOne(array $attributes = []) + * @method static GitHubPullRequest|Proxy find(object|array|mixed $criteria) + * @method static GitHubPullRequest|Proxy findOrCreate(array $attributes) + * @method static GitHubPullRequest|Proxy first(string $sortedField = 'id') + * @method static GitHubPullRequest|Proxy last(string $sortedField = 'id') + * @method static GitHubPullRequest|Proxy random(array $attributes = []) + * @method static GitHubPullRequest|Proxy randomOrCreate(array $attributes = []) + * @method static GitHubPullRequest[]|Proxy[] all() + * @method static GitHubPullRequest[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static GitHubPullRequest[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static GitHubPullRequest[]|Proxy[] findBy(array $attributes) + * @method static GitHubPullRequest[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static GitHubPullRequest[]|Proxy[] randomSet(int $number, array $attributes = []) + * + * @psalm-method GitHubPullRequest&Proxy create(array|callable $attributes = []) + * @psalm-method static GitHubPullRequest&Proxy createOne(array $attributes = []) + * @psalm-method static GitHubPullRequest&Proxy find(object|array|mixed $criteria) + * @psalm-method static GitHubPullRequest&Proxy findOrCreate(array $attributes) + * @psalm-method static GitHubPullRequest&Proxy first(string $sortedField = 'id') + * @psalm-method static GitHubPullRequest&Proxy last(string $sortedField = 'id') + * @psalm-method static GitHubPullRequest&Proxy random(array $attributes = []) + * @psalm-method static GitHubPullRequest&Proxy randomOrCreate(array $attributes = []) + * @psalm-method static GitHubPullRequest[]&Proxy[] all() + * @psalm-method static GitHubPullRequest[]&Proxy[] createMany(int $number, array|callable $attributes = []) + * @psalm-method static GitHubPullRequest[]&Proxy[] createSequence(iterable|callable $sequence) + * @psalm-method static GitHubPullRequest[]&Proxy[] findBy(array $attributes) + * @psalm-method static GitHubPullRequest[]&Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @psalm-method static GitHubPullRequest[]&Proxy[] randomSet(int $number, array $attributes = []) + */ +class GitHubPullRequestFactory extends ModelFactory +{ + #[\Override] + protected function getDefaults(): array + { + return [ + 'owner' => $owner = self::faker()->slug(), + 'repo' => $repository = self::faker()->slug(), + 'number' => $id = self::faker()->randomNumber(), + 'uri' => sprintf('https://github.com/%s/%s/pulls/%s', + $owner, + $repository, + $id + ), + 'title' => self::faker()->sentence(), + ]; + } + + public function fromUrlData(GitHubUrlData|Proxy $urlData, string $title = '[ci] run sa tools'): self + { + return $this->addState([ + 'owner' => $urlData->owner, + 'repo' => $urlData->repository, + 'number' => $urlData->identifier, + 'uri' => $urlData->uri, + 'title' => $title, + ]); + } + + #[\Override] + protected function initialize(): self + { + return $this + ->withoutPersisting() + ; + } + + #[\Override] + protected static function getClass(): string + { + return GitHubPullRequest::class; + } +} diff --git a/src/Factory/Model/GitHubUrlDataFactory.php b/src/Factory/Model/GitHubUrlDataFactory.php new file mode 100644 index 0000000..cea7763 --- /dev/null +++ b/src/Factory/Model/GitHubUrlDataFactory.php @@ -0,0 +1,96 @@ + + * + * @method GitHubUrlData|Proxy create(array|callable $attributes = []) + * @method static GitHubUrlData|Proxy createOne(array $attributes = []) + * @method static GitHubUrlData|Proxy find(object|array|mixed $criteria) + * @method static GitHubUrlData|Proxy findOrCreate(array $attributes) + * @method static GitHubUrlData|Proxy first(string $sortedField = 'id') + * @method static GitHubUrlData|Proxy last(string $sortedField = 'id') + * @method static GitHubUrlData|Proxy random(array $attributes = []) + * @method static GitHubUrlData|Proxy randomOrCreate(array $attributes = []) + * @method static GitHubUrlData[]|Proxy[] all() + * @method static GitHubUrlData[]|Proxy[] createMany(int $number, array|callable $attributes = []) + * @method static GitHubUrlData[]|Proxy[] createSequence(iterable|callable $sequence) + * @method static GitHubUrlData[]|Proxy[] findBy(array $attributes) + * @method static GitHubUrlData[]|Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @method static GitHubUrlData[]|Proxy[] randomSet(int $number, array $attributes = []) + * + * @psalm-method GitHubUrlData&Proxy create(array|callable $attributes = []) + * @psalm-method static GitHubUrlData&Proxy createOne(array $attributes = []) + * @psalm-method static GitHubUrlData&Proxy find(object|array|mixed $criteria) + * @psalm-method static GitHubUrlData&Proxy findOrCreate(array $attributes) + * @psalm-method static GitHubUrlData&Proxy first(string $sortedField = 'id') + * @psalm-method static GitHubUrlData&Proxy last(string $sortedField = 'id') + * @psalm-method static GitHubUrlData&Proxy random(array $attributes = []) + * @psalm-method static GitHubUrlData&Proxy randomOrCreate(array $attributes = []) + * @psalm-method static GitHubUrlData[]&Proxy[] all() + * @psalm-method static GitHubUrlData[]&Proxy[] createMany(int $number, array|callable $attributes = []) + * @psalm-method static GitHubUrlData[]&Proxy[] createSequence(iterable|callable $sequence) + * @psalm-method static GitHubUrlData[]&Proxy[] findBy(array $attributes) + * @psalm-method static GitHubUrlData[]&Proxy[] randomRange(int $min, int $max, array $attributes = []) + * @psalm-method static GitHubUrlData[]&Proxy[] randomSet(int $number, array $attributes = []) + */ +class GitHubUrlDataFactory extends ModelFactory +{ + #[\Override] + protected function getDefaults(): array + { + return [ + 'owner' => $owner = self::faker()->slug(), + 'repository' => $repository = self::faker()->slug(), + 'type' => $type = TypeEnum::PULL_REQUEST, + 'identifier' => $id = self::faker()->randomNumber(), + 'uri' => sprintf('https://github.com/%s/%s/%s/%s', + $owner, + $repository, + TypeEnum::PULL_REQUEST === $type ? 'pulls' : 'issues', + $id + ), + ]; + } + + public function asIssue(): self + { + $uri = $this->getDefaults()['uri']; + + return $this->addState([ + 'type' => TypeEnum::ISSUE, + 'uri' => str_replace(TypeEnum::PULL_REQUEST->value, TypeEnum::ISSUE->value, (string) $uri), + ]); + } + + public function forResponse(): self + { + return $this->addState([ + 'owner' => 'rushlow-development', + 'repository' => 'big-desk', + 'type' => TypeEnum::PULL_REQUEST, + 'identifier' => 100, + 'uri' => 'https://github.com/rushlow-development/big-desk/pulls/100', + ]); + } + + #[\Override] + protected function initialize(): self + { + return $this + ->withoutPersisting() + ; + } + + #[\Override] + protected static function getClass(): string + { + return GitHubUrlData::class; + } +} diff --git a/src/Factory/TaskFactory.php b/src/Factory/TaskFactory.php index 2d41913..81cfb82 100644 --- a/src/Factory/TaskFactory.php +++ b/src/Factory/TaskFactory.php @@ -3,6 +3,7 @@ namespace App\Factory; use App\Entity\Task; +use App\Entity\TodoList; use App\Repository\TaskRepository; use Doctrine\Common\Collections\ArrayCollection; use Zenstruck\Foundry\ModelFactory; @@ -56,6 +57,16 @@ protected function getDefaults(): array ]; } + public function withName(string $name): self + { + return $this->addState(['name' => $name]); + } + + public function forTodoList(TodoList|Proxy $todoList): self + { + return $this->addState(['todoList' => $todoList]); + } + #[\Override] protected static function getClass(): string { diff --git a/src/GraphQLRequest/_issue.fragment.graphql b/src/GraphQLRequest/_issue.fragment.graphql new file mode 100644 index 0000000..c05a39c --- /dev/null +++ b/src/GraphQLRequest/_issue.fragment.graphql @@ -0,0 +1,3 @@ +fragment issue on Issue { + title +} diff --git a/src/GraphQLRequest/_issue.template.php b/src/GraphQLRequest/_issue.template.php new file mode 100644 index 0000000..85fe73c --- /dev/null +++ b/src/GraphQLRequest/_issue.template.php @@ -0,0 +1,3 @@ +issueZ: issue(number: ) { + ...issue +} diff --git a/src/GraphQLRequest/_pr.fragment.graphql b/src/GraphQLRequest/_pr.fragment.graphql new file mode 100644 index 0000000..0fc3553 --- /dev/null +++ b/src/GraphQLRequest/_pr.fragment.graphql @@ -0,0 +1,3 @@ +fragment pr on PullRequest { + title +} diff --git a/src/GraphQLRequest/_pr.template.php b/src/GraphQLRequest/_pr.template.php new file mode 100644 index 0000000..7121c54 --- /dev/null +++ b/src/GraphQLRequest/_pr.template.php @@ -0,0 +1,3 @@ +pullZ: pullRequest(number: ) { + ...pr +} diff --git a/src/GraphQLRequest/repository.template.php b/src/GraphQLRequest/repository.template.php new file mode 100644 index 0000000..0d41ce1 --- /dev/null +++ b/src/GraphQLRequest/repository.template.php @@ -0,0 +1,7 @@ +query($repository_owner: String!, $repository_name: String!) { + repository (owner: $repository_owner, name: $repository_name) { + + } +} + + diff --git a/src/Service/TaskService.php b/src/Service/TaskService.php index a0f203a..ca97a8e 100644 --- a/src/Service/TaskService.php +++ b/src/Service/TaskService.php @@ -31,75 +31,143 @@ public function __construct( ) { } - /** @throws HttpClientException */ - public function getGitHubDataFromUrl(GitHubUrlData $urlData): GitHubPullRequest|GitHubIssue|null + /** + * @return array + * + * @throws HttpClientException + */ + public function getGitHubDataFromUrl(GitHubUrlData|array $urlData, string $owner, string $repository): array { - if (TypeEnum::PULL_REQUEST === $urlData->type) { - return $this->queryPullRequest($urlData); + if ($urlData instanceof GitHubUrlData) { + $urlData = [$urlData]; } - if (TypeEnum::ISSUE === $urlData->type) { - return $this->queryIssue($urlData); + foreach ($urlData as $key => $urlObject) { + if (!$urlObject instanceof GitHubUrlData) { + unset($urlData[$key]); + } } - return null; - } - - private function queryIssue(GitHubUrlData $urlData): ?GitHubIssue - { - if (!$payload = file_get_contents($filename = __DIR__.'/../GraphQLRequest/issue.graphql')) { - throw new HttpClientException(sprintf('Unable to read payload contents for: %s', $filename)); - } + $payload = $this->createQueryContent($urlData); $response = $this->makeRequest( $payload, [ - 'repository_owner' => $urlData->owner, - 'repository_name' => $urlData->repository, - 'number' => (int) $urlData->identifier, + 'repository_owner' => $owner, + 'repository_name' => $repository, ] ); if (!$response) { - return null; + return []; } - return new GitHubIssue( - uri: $urlData->uri, - owner: $urlData->owner, - repo: $urlData->repository, - number: (int) $urlData->identifier, - title: (string) $response['data']['repository']['issue']['title'], - ); + $objects = []; + + foreach ($response['data']['repository'] as $tag => $object) { + $tagParts = explode('Z', $tag); + $type = TypeEnum::tryFrom($tagParts[0]); + + if (TypeEnum::PULL_REQUEST === $type) { + /** @var GitHubUrlData $result */ + $result = array_reduce($urlData, function ($match, GitHubUrlData $data) use ($tagParts) { + if ((int) $data->identifier === (int) $tagParts[1]) { + $match = $data; + } + + return $match; + }); + + if (null === $result) { + continue; + } + + $objects[] = new GitHubPullRequest( + uri: $result->uri, + owner: $result->owner, + repo: $result->repository, + number: (int) $result->identifier, + title: (string) $object['title'], + ); + } else { + /** @var GitHubUrlData $result */ + $result = array_reduce($urlData, function ($match, GitHubUrlData $data) use ($tagParts) { + if ((int) $data->identifier === (int) $tagParts[1]) { + $match = $data; + } + + return $match; + }); + + if (null === $result) { + continue; + } + + $objects[] = new GitHubIssue( + uri: $result->uri, + owner: $result->owner, + repo: $result->repository, + number: (int) $result->identifier, + title: (string) $object['title'], + ); + } + } + + return $objects; } - /** @throws HttpClientException */ - private function queryPullRequest(GitHubUrlData $urlData): ?GitHubPullRequest + /** + * @param GitHubUrlData[] $urls + */ + private function createQueryContent(array $urls): string { - if (!$payload = file_get_contents($filename = __DIR__.'/../GraphQLRequest/pull-request.graphql')) { - throw new HttpClientException(sprintf('Unable to read payload contents for: %s', $filename)); + $templateRootDir = \dirname(__DIR__).'/GraphQLRequest/'; + $objectQueries = ''; + + $needsPrFragment = false; + $needsIssueFragment = false; + + foreach ($urls as $url) { + if (TypeEnum::PULL_REQUEST === $url->type) { + $needsPrFragment = true; + $fileName = '_pr.template.php'; + } else { + $needsIssueFragment = true; + $fileName = '_issue.template.php'; + } + + $templateSource = trim($this->getTemplate( + templatePath: $templateRootDir.$fileName, + parameters: ['number' => $url->identifier] + )); + + $objectQueries = trim(sprintf("%s\n%s,", $objectQueries, $templateSource)); } - $response = $this->makeRequest( - $payload, - [ - 'repository_owner' => $urlData->owner, - 'repository_name' => $urlData->repository, - 'number' => (int) $urlData->identifier, - ] - ); + $objectQueries = rtrim($objectQueries, ','); - if (!$response) { - return null; + $fragment = ''; + if ($needsIssueFragment) { + $fragment = file_get_contents($templateRootDir.'/_issue.fragment.graphql'); } - return new GitHubPullRequest( - uri: $urlData->uri, - owner: $urlData->owner, - repo: $urlData->repository, - number: (int) $urlData->identifier, - title: (string) $response['data']['repository']['pullRequest']['title'], - ); + if ($needsPrFragment) { + $fragment = sprintf("%s\n%s", $fragment, file_get_contents($templateRootDir.'/_pr.fragment.graphql')); + } + + return $this->getTemplate($templateRootDir.'/repository.template.php', [ + 'objects' => $objectQueries, + 'fragments' => $fragment, + ]); + } + + private function getTemplate(string $templatePath, array $parameters): string + { + ob_start(); + extract($parameters, \EXTR_SKIP); + include $templatePath; + + return ob_get_clean(); } /** diff --git a/tests/Functional/Service/GraphQLResponseFixture/repository.issue.200.json b/tests/Functional/Service/GraphQLResponseFixture/repository.issue.200.json new file mode 100644 index 0000000..981488e --- /dev/null +++ b/tests/Functional/Service/GraphQLResponseFixture/repository.issue.200.json @@ -0,0 +1,9 @@ +{ + "data": { + "repository": { + "issueZ101": { + "title": "some sort of issue" + } + } + } +} diff --git a/tests/Functional/Service/GraphQLResponseFixture/repository.issues.200.json b/tests/Functional/Service/GraphQLResponseFixture/repository.issues.200.json new file mode 100644 index 0000000..3fbec75 --- /dev/null +++ b/tests/Functional/Service/GraphQLResponseFixture/repository.issues.200.json @@ -0,0 +1,12 @@ +{ + "data": { + "repository": { + "issueZ101": { + "title": "some sort of issue" + }, + "issueZ201": { + "title": "another type of issue" + } + } + } +} diff --git a/tests/Functional/Service/GraphQLResponseFixture/repository.mixed.200.json b/tests/Functional/Service/GraphQLResponseFixture/repository.mixed.200.json new file mode 100644 index 0000000..6f20434 --- /dev/null +++ b/tests/Functional/Service/GraphQLResponseFixture/repository.mixed.200.json @@ -0,0 +1,12 @@ +{ + "data": { + "repository": { + "pullZ100": { + "title": "[ci] run sa tools" + }, + "issueZ101": { + "title": "some sort of issue" + } + } + } +} diff --git a/tests/Functional/Service/GraphQLResponseFixture/repository.pull.200.json b/tests/Functional/Service/GraphQLResponseFixture/repository.pull.200.json new file mode 100644 index 0000000..8d2f0d0 --- /dev/null +++ b/tests/Functional/Service/GraphQLResponseFixture/repository.pull.200.json @@ -0,0 +1,9 @@ +{ + "data": { + "repository": { + "pullZ100": { + "title": "[ci] run sa tools" + } + } + } +} diff --git a/tests/Functional/Service/GraphQLResponseFixture/repository.pulls.200.json b/tests/Functional/Service/GraphQLResponseFixture/repository.pulls.200.json new file mode 100644 index 0000000..4037c4b --- /dev/null +++ b/tests/Functional/Service/GraphQLResponseFixture/repository.pulls.200.json @@ -0,0 +1,12 @@ +{ + "data": { + "repository": { + "pullZ100": { + "title": "[ci] run sa tools" + }, + "pullZ200": { + "title": "i cant remember that..." + } + } + } +} diff --git a/tests/Functional/Service/GraphQLResponseFixture/response.bug.200.json b/tests/Functional/Service/GraphQLResponseFixture/response.bug.200.json new file mode 100644 index 0000000..f518f53 --- /dev/null +++ b/tests/Functional/Service/GraphQLResponseFixture/response.bug.200.json @@ -0,0 +1,15 @@ +{ + "data": { + "repository": { + "pullZ1482": { + "title": "WIP - failing generator test with global ns collision" + }, + "issueZ1479": { + "title": "[make:entity] Cannot create an entity called \"Locale\"" + }, + "pullZ1416": { + "title": "Adding `ORM\\JoinTable` attribute to the `make:entity` command's `ManyToMany` field type" + } + } + } +} diff --git a/tests/Functional/Service/TaskServiceTest.php b/tests/Functional/Service/TaskServiceTest.php index 038f718..1b5c54a 100644 --- a/tests/Functional/Service/TaskServiceTest.php +++ b/tests/Functional/Service/TaskServiceTest.php @@ -2,15 +2,15 @@ namespace App\Tests\Functional\Service; +use App\Factory\Model\GitHubIssueFactory; +use App\Factory\Model\GitHubPullRequestFactory; +use App\Factory\Model\GitHubUrlDataFactory; use App\Factory\UserFactory; use App\Model\EncryptedData; -use App\Model\GitHubIssue; -use App\Model\GitHubPullRequest; -use App\Model\GitHubUrlData; use App\Service\EncryptorService; use App\Service\TaskService; use App\Tests\FunctionalTestCase; -use App\Util\TypeEnum; +use PHPUnit\Framework\MockObject\MockObject; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpClient\MockHttpClient; use Symfony\Component\HttpClient\Response\MockResponse; @@ -18,13 +18,27 @@ final class TaskServiceTest extends FunctionalTestCase { + private const string ISSUE_FRAGMENT = 'fragment issue on Issue'; + private const string PULL_FRAGMENT = 'fragment pr on PullRequest'; + + private Security|MockObject $mockSecurity; + private EncryptorService|MockObject $mockEncryptor; + private MockResponse $mockResponse; + + #[\Override] + protected function setUp(): void + { + $this->mockSecurity = $this->createMock(Security::class); + $this->mockEncryptor = $this->createMock(EncryptorService::class); + } + public function testSmokeGitHubHttpClient(): void { static::createKernel()->boot(); - static::getContainer()->set(HttpClientInterface::class, new MockHttpClient()); + ($container = static::getContainer())->set(HttpClientInterface::class, new MockHttpClient()); - $client = static::getContainer()->get('git.hub.http.client.uri_template.inner'); + $client = $container->get('git.hub.http.client.uri_template.inner'); self::assertInstanceOf(HttpClientInterface::class, $client); @@ -45,120 +59,165 @@ public function testSmokeGitHubHttpClient(): void self::assertArrayIsEqualToArrayOnlyConsideringListOfKeys( $expected, $reflectedDefaultOptions[$rUri]['headers'], - array_keys($expected)); + array_keys($expected) + ); } - public function testGetGitHubDataFromUrlWithPullRequest(): void + public function testGetGitHubDateWithSinglePullRequest(): void { - $user = UserFactory::createOne(); - $user->disableAutoRefresh(); - $user->setGitHubToken($data = new EncryptedData('', '')); + $pr1Fixture = GitHubUrlDataFactory::new()->forResponse()->create(['identifier' => 100]); + $expectedPr1 = GitHubPullRequestFactory::new()->fromUrlData($pr1Fixture->object())->create(); - $urlDataFixture = new GitHubUrlData( - owner: 'rushlow', - repository: 'big-desk', - type: TypeEnum::PULL_REQUEST, - identifier: '100', // Set as a string to ensure it's converted to an int - uri: 'https://github.com/rushlow/big-desk/pulls/100' - ); + $service = $this->getService('repository.pull.200.json'); + $result = $service->getGitHubDataFromUrl([$pr1Fixture->object()], 'rushlow-development', 'big-desk'); - $expected = new GitHubPullRequest( - uri: 'https://github.com/rushlow/big-desk/pulls/100', - owner: 'rushlow', - repo: 'big-desk', - number: 100, - title: 'The title of my PR.', - ); + self::assertEquals([$expectedPr1->object()], $result); - $mockResponse = new MockResponse((string) file_get_contents(__DIR__.'/GraphQLResponseFixture/pull-request.200.json')); - $mockHttpClient = new MockHttpClient($mockResponse); - $mockSecurity = $this->createMock(Security::class); - $mockSecurity - ->expects(self::once()) - ->method('getUser') - ->willReturn($user->object()) - ; - $mockEncryptor = $this->createMock(EncryptorService::class); - $mockEncryptor - ->expects(self::once()) - ->method('decryptData') - ->with($data) - ->willReturn('$superSecretToken') - ; + self::assertSame('https://example.com/graphql', $this->mockResponse->getRequestUrl()); + self::assertSame('POST', $this->mockResponse->getRequestMethod()); + + $requestBody = $this->mockResponse->getRequestOptions()['body']; + + self::assertStringContainsString('pullRequest(', $requestBody); + self::assertStringContainsString('\u0022repository_owner\u0022:\u0022rushlow-development\u0022', $requestBody); + self::assertStringContainsString('\u0022repository_name\u0022:\u0022big-desk\u0022', $requestBody); + self::assertStringContainsString(self::PULL_FRAGMENT, $requestBody); + self::assertStringNotContainsString(self::ISSUE_FRAGMENT, $requestBody); + + $calledWithOptions = $this->mockResponse->getRequestOptions(); + self::assertSame('Authorization: bearer $superSecretToken', $calledWithOptions['normalized_headers']['authorization'][0]); + } + + public function testGetGitHubDateWithSingleIssue(): void + { + $issue1Fixture = GitHubUrlDataFactory::new()->forResponse()->asIssue()->create(['identifier' => 101]); + $expectedIssue1 = GitHubIssueFactory::new()->fromUrlData($issue1Fixture->object())->create(); + + $service = $this->getService('repository.issues.200.json'); + $result = $service->getGitHubDataFromUrl([$issue1Fixture->object()], 'rushlow-development', 'big-desk'); + + self::assertEquals([$expectedIssue1->object()], $result); + + self::assertSame('https://example.com/graphql', $this->mockResponse->getRequestUrl()); + self::assertSame('POST', $this->mockResponse->getRequestMethod()); + + $requestBody = $this->mockResponse->getRequestOptions()['body']; + + self::assertStringContainsString('issue(', $requestBody); + self::assertStringContainsString('\u0022repository_owner\u0022:\u0022rushlow-development\u0022', $requestBody); + self::assertStringContainsString('\u0022repository_name\u0022:\u0022big-desk\u0022', $requestBody); + self::assertStringContainsString(self::ISSUE_FRAGMENT, $requestBody); + self::assertStringNotContainsString(self::PULL_FRAGMENT, $requestBody); + + $calledWithOptions = $this->mockResponse->getRequestOptions(); + self::assertSame('Authorization: bearer $superSecretToken', $calledWithOptions['normalized_headers']['authorization'][0]); + } + + public function testGetGitHubDataFromUrlWithMultiplePulls(): void + { + $pr1Fixture = GitHubUrlDataFactory::new()->forResponse()->create(['identifier' => 100]); + $pr2Fixture = GitHubUrlDataFactory::new()->forResponse()->create(['identifier' => 200]); + + $expectedPr1 = GitHubPullRequestFactory::new()->fromUrlData($pr1Fixture->object())->create(); + $expectedPr2 = GitHubPullRequestFactory::new()->fromUrlData($pr2Fixture->object())->create(['title' => 'i cant remember that...']); - $service = new TaskService($mockHttpClient, $mockSecurity, $mockEncryptor); - $result = $service->getGitHubDataFromUrl($urlDataFixture); + $service = $this->getService('repository.pulls.200.json'); + $result = $service->getGitHubDataFromUrl([$pr1Fixture->object(), $pr2Fixture->object()], 'rushlow-development', 'big-desk'); - self::assertEquals($expected, $result); + self::assertEquals([$expectedPr1->object(), $expectedPr2->object()], $result); - self::assertSame('https://example.com/graphql', $mockResponse->getRequestUrl()); - self::assertSame('POST', $mockResponse->getRequestMethod()); + self::assertSame('https://example.com/graphql', $this->mockResponse->getRequestUrl()); + self::assertSame('POST', $this->mockResponse->getRequestMethod()); - $requestBody = $mockResponse->getRequestOptions()['body']; + $requestBody = $this->mockResponse->getRequestOptions()['body']; self::assertStringContainsString('pullRequest(', $requestBody); - self::assertStringContainsString('\u0022repository_owner\u0022:\u0022rushlow\u0022', $requestBody); + self::assertStringContainsString('\u0022repository_owner\u0022:\u0022rushlow-development\u0022', $requestBody); self::assertStringContainsString('\u0022repository_name\u0022:\u0022big-desk\u0022', $requestBody); - self::assertStringContainsString('\u0022number\u0022:100', $requestBody); + self::assertStringContainsString(self::PULL_FRAGMENT, $requestBody); + self::assertStringNotContainsString(self::ISSUE_FRAGMENT, $requestBody); - $calledWithOptions = $mockResponse->getRequestOptions(); + $calledWithOptions = $this->mockResponse->getRequestOptions(); self::assertSame('Authorization: bearer $superSecretToken', $calledWithOptions['normalized_headers']['authorization'][0]); } - public function testGetGitHubDataFromUrlWithIssue(): void + public function testGetGitHubDataFromUrlWithMultipleIssues(): void { - $user = UserFactory::createOne(); - $user->disableAutoRefresh(); - $user->setGitHubToken($data = new EncryptedData('', '')); + $issue1Fixture = GitHubUrlDataFactory::new()->forResponse()->asIssue()->create(['identifier' => 101]); + $issue2Fixture = GitHubUrlDataFactory::new()->forResponse()->asIssue()->create(['identifier' => 201]); - $urlDataFixture = new GitHubUrlData( - owner: 'rushlow', - repository: 'big-desk', - type: TypeEnum::ISSUE, - identifier: '100', // Set as a string to ensure it's converted to an int - uri: 'https://github.com/rushlow/big-desk/issues/100' - ); + $expectedIssue1 = GitHubIssueFactory::new()->fromUrlData($issue1Fixture->object())->create(); + $expectedIssue2 = GitHubIssueFactory::new()->fromUrlData($issue2Fixture->object())->create(['number' => 201, 'title' => 'another type of issue']); - $expected = new GitHubIssue( - uri: 'https://github.com/rushlow/big-desk/issues/100', - owner: 'rushlow', - repo: 'big-desk', - number: 100, - title: 'Add a maker for Doctrine migrations', - ); + $service = $this->getService('repository.issues.200.json'); + $result = $service->getGitHubDataFromUrl([$issue1Fixture->object(), $issue2Fixture->object()], 'rushlow-development', 'big-desk'); + + self::assertEquals([$expectedIssue1->object(), $expectedIssue2->object()], $result); + + self::assertSame('https://example.com/graphql', $this->mockResponse->getRequestUrl()); + self::assertSame('POST', $this->mockResponse->getRequestMethod()); + + $requestBody = $this->mockResponse->getRequestOptions()['body']; + + self::assertStringContainsString('issue(', $requestBody); + self::assertStringContainsString('\u0022repository_owner\u0022:\u0022rushlow-development\u0022', $requestBody); + self::assertStringContainsString('\u0022repository_name\u0022:\u0022big-desk\u0022', $requestBody); + self::assertStringContainsString(self::ISSUE_FRAGMENT, $requestBody); + self::assertStringNotContainsString(self::PULL_FRAGMENT, $requestBody); + + $calledWithOptions = $this->mockResponse->getRequestOptions(); + self::assertSame('Authorization: bearer $superSecretToken', $calledWithOptions['normalized_headers']['authorization'][0]); + } + + public function testGetGitHubDataFromUrlWithMixedObjects(): void + { + $issue1Fixture = GitHubUrlDataFactory::new()->forResponse()->asIssue()->create(['identifier' => 101]); + $pr1Fixture = GitHubUrlDataFactory::new()->forResponse()->create(['identifier' => 100]); + + $expectedIssue1 = GitHubIssueFactory::new()->fromUrlData($issue1Fixture->object())->create(); + $expectedPr1 = GitHubPullRequestFactory::new()->fromUrlData($pr1Fixture->object())->create(); + + $service = $this->getService('repository.mixed.200.json'); + $result = $service->getGitHubDataFromUrl([$issue1Fixture->object(), $pr1Fixture->object()], 'rushlow-development', 'big-desk'); + + self::assertEquals([$expectedPr1->object(), $expectedIssue1->object()], $result); - $mockResponse = new MockResponse((string) file_get_contents(__DIR__.'/GraphQLResponseFixture/issue.200.json')); - $mockHttpClient = new MockHttpClient($mockResponse); - $mockSecurity = $this->createMock(Security::class); - $mockSecurity + self::assertSame('https://example.com/graphql', $this->mockResponse->getRequestUrl()); + self::assertSame('POST', $this->mockResponse->getRequestMethod()); + + $requestBody = $this->mockResponse->getRequestOptions()['body']; + + self::assertStringContainsString('issue(', $requestBody); + self::assertStringContainsString('\u0022repository_owner\u0022:\u0022rushlow-development\u0022', $requestBody); + self::assertStringContainsString('\u0022repository_name\u0022:\u0022big-desk\u0022', $requestBody); + self::assertStringContainsString(self::PULL_FRAGMENT, $requestBody); + self::assertStringContainsString(self::ISSUE_FRAGMENT, $requestBody); + + $calledWithOptions = $this->mockResponse->getRequestOptions(); + self::assertSame('Authorization: bearer $superSecretToken', $calledWithOptions['normalized_headers']['authorization'][0]); + } + + private function getService(string $responseFileName): TaskService + { + $user = UserFactory::new()->withoutPersisting()->create(); + $user->setGitHubToken($data = new EncryptedData('', '')); + + $this->mockSecurity ->expects(self::once()) ->method('getUser') ->willReturn($user->object()) ; - $mockEncryptor = $this->createMock(EncryptorService::class); - $mockEncryptor + + $this->mockEncryptor ->expects(self::once()) ->method('decryptData') ->with($data) ->willReturn('$superSecretToken') ; - $service = new TaskService($mockHttpClient, $mockSecurity, $mockEncryptor); - $result = $service->getGitHubDataFromUrl($urlDataFixture); - - self::assertEquals($expected, $result); - - self::assertSame('https://example.com/graphql', $mockResponse->getRequestUrl()); - self::assertSame('POST', $mockResponse->getRequestMethod()); - - $requestBody = $mockResponse->getRequestOptions()['body']; + $this->mockResponse = new MockResponse((string) file_get_contents(__DIR__.'/GraphQLResponseFixture/'.$responseFileName)); + $mockHttpClient = new MockHttpClient($this->mockResponse); - self::assertStringContainsString('issue(', $requestBody); - self::assertStringContainsString('\u0022repository_owner\u0022:\u0022rushlow\u0022', $requestBody); - self::assertStringContainsString('\u0022repository_name\u0022:\u0022big-desk\u0022', $requestBody); - self::assertStringContainsString('\u0022number\u0022:100', $requestBody); - - $calledWithOptions = $mockResponse->getRequestOptions(); - self::assertSame('Authorization: bearer $superSecretToken', $calledWithOptions['normalized_headers']['authorization'][0]); + return new TaskService($mockHttpClient, $this->mockSecurity, $this->mockEncryptor); } } From 39b06f2cd4f6da5a7705012ad9f72020856c1e1a Mon Sep 17 00:00:00 2001 From: Jesse Rushlow Date: Fri, 29 Mar 2024 13:09:52 -0400 Subject: [PATCH 2/3] fix sa errors --- phpstan.dist.neon | 7 ++++- .../Model/GitHubPullRequestFactory.php | 1 + src/Factory/Model/GitHubUrlDataFactory.php | 6 +++-- src/Factory/TaskFactory.php | 1 + src/Service/TaskService.php | 26 ++++++++++++++----- tests/Functional/Service/TaskServiceTest.php | 5 ++-- 6 files changed, 33 insertions(+), 13 deletions(-) diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 905ae44..9eb7e1a 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -7,4 +7,9 @@ parameters: containerXmlPath: var/cache/test/App_KernelTestDebugContainer.xml doctrine: ormRepositoryClass: App\Repository\AbstractRepository - objectManagerLoader: tests/phpstan-bootstrap.php \ No newline at end of file + objectManagerLoader: tests/phpstan-bootstrap.php + ignoreErrors: + - + message: '#Variable \$[a-zA-Z0-9]+ might not be defined\.#' +# identifier: variable.undefined // Available in phpstan 1.11.0 + path: src/GraphQLRequest/* diff --git a/src/Factory/Model/GitHubPullRequestFactory.php b/src/Factory/Model/GitHubPullRequestFactory.php index 6e8d71f..59117d6 100644 --- a/src/Factory/Model/GitHubPullRequestFactory.php +++ b/src/Factory/Model/GitHubPullRequestFactory.php @@ -58,6 +58,7 @@ protected function getDefaults(): array ]; } + /** @param GitHubUrlData|Proxy $urlData */ public function fromUrlData(GitHubUrlData|Proxy $urlData, string $title = '[ci] run sa tools'): self { return $this->addState([ diff --git a/src/Factory/Model/GitHubUrlDataFactory.php b/src/Factory/Model/GitHubUrlDataFactory.php index cea7763..a7cb3e9 100644 --- a/src/Factory/Model/GitHubUrlDataFactory.php +++ b/src/Factory/Model/GitHubUrlDataFactory.php @@ -42,6 +42,7 @@ */ class GitHubUrlDataFactory extends ModelFactory { + /** @return array */ #[\Override] protected function getDefaults(): array { @@ -53,7 +54,7 @@ protected function getDefaults(): array 'uri' => sprintf('https://github.com/%s/%s/%s/%s', $owner, $repository, - TypeEnum::PULL_REQUEST === $type ? 'pulls' : 'issues', + TypeEnum::PULL_REQUEST === $type ? 'pulls' : 'issues', // @phpstan-ignore-line $id ), ]; @@ -61,11 +62,12 @@ protected function getDefaults(): array public function asIssue(): self { + /** @var string $uri */ $uri = $this->getDefaults()['uri']; return $this->addState([ 'type' => TypeEnum::ISSUE, - 'uri' => str_replace(TypeEnum::PULL_REQUEST->value, TypeEnum::ISSUE->value, (string) $uri), + 'uri' => str_replace(TypeEnum::PULL_REQUEST->value, TypeEnum::ISSUE->value, $uri), ]); } diff --git a/src/Factory/TaskFactory.php b/src/Factory/TaskFactory.php index 81cfb82..59c97bc 100644 --- a/src/Factory/TaskFactory.php +++ b/src/Factory/TaskFactory.php @@ -62,6 +62,7 @@ public function withName(string $name): self return $this->addState(['name' => $name]); } + /** @param TodoList|Proxy $todoList */ public function forTodoList(TodoList|Proxy $todoList): self { return $this->addState(['todoList' => $todoList]); diff --git a/src/Service/TaskService.php b/src/Service/TaskService.php index ca97a8e..b2fc1bc 100644 --- a/src/Service/TaskService.php +++ b/src/Service/TaskService.php @@ -32,6 +32,8 @@ public function __construct( } /** + * @param GitHubUrlData|GitHubUrlData[] $urlData + * * @return array * * @throws HttpClientException @@ -78,9 +80,9 @@ public function getGitHubDataFromUrl(GitHubUrlData|array $urlData, string $owner return $match; }); - if (null === $result) { - continue; - } + // if (null === $result) { + // continue; + // } $objects[] = new GitHubPullRequest( uri: $result->uri, @@ -99,9 +101,9 @@ public function getGitHubDataFromUrl(GitHubUrlData|array $urlData, string $owner return $match; }); - if (null === $result) { - continue; - } + // if (null === $result) { + // continue; + // } $objects[] = new GitHubIssue( uri: $result->uri, @@ -161,13 +163,23 @@ private function createQueryContent(array $urls): string ]); } + /** + * @param string $templatePath The absolute path to the template with filename and extension + * @param array $parameters array of variables used in the template + * where the "key" is the variable name and the "value" is the value of the variable + * in the template + */ private function getTemplate(string $templatePath, array $parameters): string { ob_start(); extract($parameters, \EXTR_SKIP); include $templatePath; - return ob_get_clean(); + if (false === $template = ob_get_clean()) { + throw new \RuntimeException('Ooops! somethign went wrong trying to generate the template.'); + } + + return $template; } /** diff --git a/tests/Functional/Service/TaskServiceTest.php b/tests/Functional/Service/TaskServiceTest.php index 1b5c54a..a299be6 100644 --- a/tests/Functional/Service/TaskServiceTest.php +++ b/tests/Functional/Service/TaskServiceTest.php @@ -20,9 +20,8 @@ final class TaskServiceTest extends FunctionalTestCase { private const string ISSUE_FRAGMENT = 'fragment issue on Issue'; private const string PULL_FRAGMENT = 'fragment pr on PullRequest'; - - private Security|MockObject $mockSecurity; - private EncryptorService|MockObject $mockEncryptor; + private MockObject&Security $mockSecurity; + private EncryptorService&MockObject $mockEncryptor; private MockResponse $mockResponse; #[\Override] From 5a5b5009531ac024e40562bbcc9776ea211f2605 Mon Sep 17 00:00:00 2001 From: Jesse Rushlow Date: Tue, 2 Apr 2024 07:31:48 -0400 Subject: [PATCH 3/3] WIP - combine multiple requests into one --- composer.json | 1 + composer.lock | 95 +++++++++++---- rector.php | 5 + src/Controller/TodoListController.php | 55 +++++---- src/Entity/TodoList.php | 8 ++ src/Factory/TodoListFactory.php | 7 ++ .../Controller/TodoListControllerTest.php | 108 ++++++++++++++++++ tests/Story/TodoStory.php | 82 +++++++++++++ tests/bootstrap.php | 3 + tools/rector/composer.json | 3 +- 10 files changed, 325 insertions(+), 42 deletions(-) create mode 100644 tests/Story/TodoStory.php diff --git a/composer.json b/composer.json index d571af2..9e24466 100644 --- a/composer.json +++ b/composer.json @@ -126,6 +126,7 @@ }, "require-dev": { "dbrekelmans/bdi": "^1.1", + "dg/bypass-finals": "^1.6", "doctrine/doctrine-fixtures-bundle": "^3.5", "phpunit/phpunit": "^11.0", "symfony/browser-kit": "7.0.*", diff --git a/composer.lock b/composer.lock index 0825a43..92d8c79 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9bf56090c13f846d5c8b6d15be3e2650", + "content-hash": "be3b40c410cd1410170d0cbec7782870", "packages": [ { "name": "carbonphp/carbon-doctrine-types", @@ -1803,16 +1803,16 @@ }, { "name": "nesbot/carbon", - "version": "3.1.1", + "version": "3.2.3", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "34ccf6f6b49c915421c7886c88c0cb77f3ebbfd2" + "reference": "4d599a6e2351d6b6bf21737accdfe1a4ce3fdbb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/34ccf6f6b49c915421c7886c88c0cb77f3ebbfd2", - "reference": "34ccf6f6b49c915421c7886c88c0cb77f3ebbfd2", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4d599a6e2351d6b6bf21737accdfe1a4ce3fdbb1", + "reference": "4d599a6e2351d6b6bf21737accdfe1a4ce3fdbb1", "shasum": "" }, "require": { @@ -1830,14 +1830,14 @@ "require-dev": { "doctrine/dbal": "^3.6.3 || ^4.0", "doctrine/orm": "^2.15.2 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.18.0", - "kylekatarnls/multi-tester": "^2.2.0", - "ondrejmirtes/better-reflection": "^6.11.0.0", - "phpmd/phpmd": "^2.13.0", - "phpstan/extension-installer": "^1.3.0", - "phpstan/phpstan": "^1.10.20", - "phpunit/phpunit": "^10.2.2", - "squizlabs/php_codesniffer": "^3.7.2" + "friendsofphp/php-cs-fixer": "^3.52.1", + "kylekatarnls/multi-tester": "^2.5.3", + "ondrejmirtes/better-reflection": "^6.25.0.4", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.10.65", + "phpunit/phpunit": "^10.5.15", + "squizlabs/php_codesniffer": "^3.9.0" }, "bin": [ "bin/carbon" @@ -1905,7 +1905,7 @@ "type": "tidelift" } ], - "time": "2024-03-13T12:42:37+00:00" + "time": "2024-03-30T18:22:00+00:00" }, { "name": "nette/schema", @@ -8580,6 +8580,59 @@ }, "time": "2024-02-22T15:29:35+00:00" }, + { + "name": "dg/bypass-finals", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/dg/bypass-finals.git", + "reference": "efe2fe04bae9f0de271dd462afc049067889e6d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dg/bypass-finals/zipball/efe2fe04bae9f0de271dd462afc049067889e6d1", + "reference": "efe2fe04bae9f0de271dd462afc049067889e6d1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "nette/tester": "^2.3", + "phpstan/phpstan": "^0.12" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0", + "GPL-3.0" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + } + ], + "description": "Removes final keyword from source code on-the-fly and allows mocking of final methods and classes", + "keywords": [ + "finals", + "mocking", + "phpunit", + "testing", + "unit" + ], + "support": { + "issues": "https://github.com/dg/bypass-finals/issues", + "source": "https://github.com/dg/bypass-finals/tree/v1.6.0" + }, + "time": "2023-11-19T22:19:30+00:00" + }, { "name": "doctrine/data-fixtures", "version": "1.7.0", @@ -9507,16 +9560,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.0.8", + "version": "11.0.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "48ea58408879a9aad630022186398364051482fc" + "reference": "591bbfe416400385527d5086b346b92c06de404b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/48ea58408879a9aad630022186398364051482fc", - "reference": "48ea58408879a9aad630022186398364051482fc", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/591bbfe416400385527d5086b346b92c06de404b", + "reference": "591bbfe416400385527d5086b346b92c06de404b", "shasum": "" }, "require": { @@ -9587,7 +9640,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.0.8" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.0.9" }, "funding": [ { @@ -9603,7 +9656,7 @@ "type": "tidelift" } ], - "time": "2024-03-22T04:21:01+00:00" + "time": "2024-03-28T10:09:42+00:00" }, { "name": "sebastian/cli-parser", @@ -11417,5 +11470,5 @@ "ext-sodium": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/rector.php b/rector.php index b0d4908..41f7ec2 100644 --- a/rector.php +++ b/rector.php @@ -20,8 +20,13 @@ $config->symfonyContainerXml(__DIR__.'/var/cache/test/App_KernelTestDebugContainer.xml'); $config->symfonyContainerPhp(__DIR__.'/tests/rector-bootstrap.php'); +// $config->singleton(\Zenstruck\Foundry\Utils\Rector\PersistenceResolver::class, +// static fn() => new \Zenstruck\Foundry\Utils\Rector\PersistenceResolver( +// (require __DIR__.'/tests/rector-bootstrap.php')->get('doctrine')->getManager()) +// ); $config->sets([ +// \Zenstruck\Foundry\Utils\Rector\FoundrySetList::UP_TO_FOUNDRY_2, LevelSetList::UP_TO_PHP_83, SetList::DEAD_CODE, SymfonySetList::SYMFONY_64, diff --git a/src/Controller/TodoListController.php b/src/Controller/TodoListController.php index 7ca37af..5ae0e99 100644 --- a/src/Controller/TodoListController.php +++ b/src/Controller/TodoListController.php @@ -6,12 +6,13 @@ use App\Entity\TodoList; use App\Exception\HttpClientException; use App\Form\TodoListType; -use App\Model\GitHubIssue; -use App\Model\GitHubPullRequest; +use App\Model\GitHubUrlData; +use App\Repository\TaskRepository; use App\Repository\TodoListRepository; use App\Security\Voter\TodoListVoter; use App\Service\TaskService; use App\Util\UrlParser; +use Doctrine\Common\Collections\Collection; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -40,10 +41,8 @@ public function new(Request $request, TaskService $taskService): Response if ($form->isSubmitted() && $form->isValid()) { /** @var TodoList $list */ $list = $form->getData(); - - foreach ($list->getTasks() as $task) { - $this->checkForGitHub($task, $taskService); - } + // @TODO Fix this for new tasks... + $this->checkForGitHub($list->getTasks(), $taskService); $this->listRepository->persist($list, true); @@ -57,15 +56,13 @@ public function new(Request $request, TaskService $taskService): Response #[IsGranted(TodoListVoter::EDIT, 'todoList')] #[Route('/{id}/edit', name: 'list_edit', requirements: ['id' => Requirement::UUID], methods: ['GET', 'POST'])] - public function edit(Request $request, TodoList $todoList, TaskService $taskService): Response + public function edit(Request $request, TodoList $todoList, TaskService $taskService, TaskRepository $taskRepository): Response { $form = $this->createForm(TodoListType::class, $todoList); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { - foreach ($todoList->getTasks() as $task) { - $this->checkForGitHub($task, $taskService); - } + $todoList->setTasks($this->checkForGitHub($todoList->getTasks(), $taskService)); $this->listRepository->flush(); @@ -105,24 +102,42 @@ public function removeTask(TodoList $todoList, Task $task): JsonResponse return $this->json(['Ok']); } - protected function checkForGitHub(Task $task, TaskService $taskService): void + /** + * @param Collection $tasks + * + * @return Collection + */ + protected function checkForGitHub(Collection $tasks, TaskService $taskService): Collection { - $gitHubLink = UrlParser::getGitHubUrlFromText($task->getName()); + $links = []; + + foreach ($tasks as $task) { + $link = UrlParser::getGitHubUrlFromText($task->getName()); - if (false === $gitHubLink) { - return; + if (!$link instanceof GitHubUrlData) { + continue; + } + + $links[] = $link; } try { // We want to query GitHub to grab the information about the link - $data = $taskService->getGitHubDataFromUrl($gitHubLink); - } catch (HttpClientException) { - // @TODO - Do something with this in the future... - return; + $objects = $taskService->getGitHubDataFromUrl($links, 'rushlow-development', 'big-desk'); + } catch (HttpClientException $e) { + throw new \RuntimeException('getGitHubDataFromUrl() Failed.', previous: $e); } - if ($data instanceof GitHubIssue || $data instanceof GitHubPullRequest) { - $task->addGitHub($data); + foreach ($objects as $gitHubObject) { + $tasks = $tasks->map(function (Task $task) use ($gitHubObject) { + if (str_contains($task->getName(), $gitHubObject->uri)) { + $task->addGitHub($gitHubObject); + } + + return $task; + }); } + + return $tasks; } } diff --git a/src/Entity/TodoList.php b/src/Entity/TodoList.php index 1a5c3bc..669d08b 100644 --- a/src/Entity/TodoList.php +++ b/src/Entity/TodoList.php @@ -55,6 +55,14 @@ public function addTask(Task $task): self return $this; } + /** @param Collection $tasks */ + public function setTasks(Collection $tasks): self + { + $this->tasks = $tasks; + + return $this; + } + public function removeTask(Task $task): self { $this->tasks->removeElement($task); diff --git a/src/Factory/TodoListFactory.php b/src/Factory/TodoListFactory.php index 95c8427..948b721 100644 --- a/src/Factory/TodoListFactory.php +++ b/src/Factory/TodoListFactory.php @@ -3,6 +3,7 @@ namespace App\Factory; use App\Entity\TodoList; +use App\Entity\User; use App\Repository\TodoListRepository; use Zenstruck\Foundry\ModelFactory; use Zenstruck\Foundry\Proxy; @@ -54,6 +55,12 @@ protected function getDefaults(): array ]; } + /** @param Proxy $owner */ + public function withOwner(Proxy $owner): self + { + return $this->addState(['owner' => $owner]); + } + #[\Override] protected static function getClass(): string { diff --git a/tests/Functional/Controller/TodoListControllerTest.php b/tests/Functional/Controller/TodoListControllerTest.php index aded136..246fa5d 100644 --- a/tests/Functional/Controller/TodoListControllerTest.php +++ b/tests/Functional/Controller/TodoListControllerTest.php @@ -2,12 +2,17 @@ namespace App\Tests\Functional\Controller; +use App\Entity\Task; use App\Entity\TodoList; use App\Factory\TaskFactory; use App\Factory\TodoListFactory; use App\Factory\UserFactory; +use App\Repository\TaskRepository; use App\Repository\TodoListRepository; +use App\Service\TaskService; use App\Tests\FunctionalTestCase; +use App\Tests\Story\TodoStory; +use Doctrine\Common\Collections\ArrayCollection; use Symfony\Bundle\FrameworkBundle\KernelBrowser; class TodoListControllerTest extends FunctionalTestCase @@ -79,6 +84,109 @@ public function testEdit(): void self::assertSame('Weekly Tasks', $fixture->getName()); } + public function testEditByAddingTaskWithGitHubLinks(): void + { + $expectedPullRequestObjects = []; + + foreach (['1482', '1479', '1416'] as $pullNumber) { + $expectedPullRequestObjects[$pullNumber] = TodoStory::getExpectedPullRequest((int) $pullNumber); + } + + $expectedTasks = [ + '1482' => 'WIP - failing generator test with global ns collision', + '1479' => '[make:entity] Cannot create an entity called "Locale"', + '1416' => 'Adding `ORM\JoinTable` attribute to the `make:entity` command\'s `ManyToMany` field type', + ]; + + $mockService = $this->createMock(TaskService::class); + $mockService + ->expects(self::once()) + ->method('getGitHubDataFromUrl') + ->willReturn($expectedPullRequestObjects) + ; + + static::getContainer()->set(TaskService::class, $mockService); + $this->client->disableReboot(); + + TodoStory::load(); + $path = sprintf('%s/%s/edit', $this->path, TodoStory::getTodoList()->getId()); + $this->client->loginUser(TodoStory::owner()->object()); + $this->client->request('GET', $path); + + self::assertResponseIsSuccessful(); + + $urls = [ + '1482' => 'https://github.com/rushlow-development/big-desk/pull/1482', + '1479' => 'https://github.com/rushlow-development/big-desk/pull/1479', + '1416' => 'https://github.com/rushlow-development/big-desk/pull/1416', + ]; + + $this->client->submitForm('Update', [ + 'todo_list[tasks][0][name]' => 'https://github.com/rushlow-development/big-desk/pull/1482', + 'todo_list[tasks][1][name]' => 'https://github.com/rushlow-development/big-desk/pull/1479', + 'todo_list[tasks][2][name]' => 'https://github.com/rushlow-development/big-desk/pull/1416', + ]); + + foreach ($expectedTasks as $taskId => $expectedFormattedName) { + self::assertInstanceOf(Task::class, $task = static::getContainer()->get(TaskRepository::class)->findOneBy(['name' => $urls[$taskId]])); + self::assertEquals(new ArrayCollection([$expectedPullRequestObjects[$taskId]]), $task->getGitHubObjects()); + } + } + + public function testEditByAddingTaskWithGitHubLinksWithExistingLinks(): void + { + $expectedPullRequestObjects = []; + + foreach (['1482', '1479', '1416'] as $pullNumber) { + $expectedPullRequestObjects[$pullNumber] = TodoStory::getExpectedPullRequest((int) $pullNumber); + } + + $expectedTasks = [ + '1482' => 'WIP - failing generator test with global ns collision', + '1479' => '[make:entity] Cannot create an entity called "Locale"', + '1416' => 'Adding `ORM\JoinTable` attribute to the `make:entity` command\'s `ManyToMany` field type', + ]; + + $mockService = $this->createMock(TaskService::class); + $mockService + ->expects(self::once()) + ->method('getGitHubDataFromUrl') + ->willReturn($expectedPullRequestObjects) + ; + + static::getContainer()->set(TaskService::class, $mockService); + $this->client->disableReboot(); + + TodoStory::load(); + TaskFactory::createOne(['name' => 'No Github Link', 'todoList' => TodoStory::getTodoList()]); + $path = sprintf('%s/%s/edit', $this->path, TodoStory::getTodoList()->getId()); + $this->client->loginUser(TodoStory::owner()->object()); + $this->client->request('GET', $path); + + self::assertResponseIsSuccessful(); + + $urls = [ + '1482' => 'https://github.com/rushlow-development/big-desk/pull/1482', + '1479' => 'https://github.com/rushlow-development/big-desk/pull/1479', + '1416' => 'https://github.com/rushlow-development/big-desk/pull/1416', + ]; + + $this->client->submitForm('Update', [ + 'todo_list[tasks][0][name]' => 'https://github.com/rushlow-development/big-desk/pull/1482', + 'todo_list[tasks][1][name]' => 'https://github.com/rushlow-development/big-desk/pull/1479', + 'todo_list[tasks][2][name]' => 'https://github.com/rushlow-development/big-desk/pull/1416', + ]); + + foreach ($expectedTasks as $taskId => $expectedFormattedName) { + self::assertInstanceOf(Task::class, $task = static::getContainer()->get(TaskRepository::class)->findOneBy(['name' => $urls[$taskId]])); + self::assertEquals(new ArrayCollection([$expectedPullRequestObjects[$taskId]]), $task->getGitHubObjects()); + } + + $todoList = static::getContainer()->get(TodoListRepository::class)->findAll()[0]; + self::assertEquals(TodoStory::getTodoList()->object(), $todoList); + self::assertCount(4, $todoList->getTasks()); + } + public function testRemove(): void { $user = UserFactory::createOne(); diff --git a/tests/Story/TodoStory.php b/tests/Story/TodoStory.php new file mode 100644 index 0000000..597a43a --- /dev/null +++ b/tests/Story/TodoStory.php @@ -0,0 +1,82 @@ + + * + * @method static Proxy owner() + * @method static Proxy getTodoList() + * @method static Proxy task1482() + * @method static Proxy task1479() + * @method static Proxy task1416() + */ +final class TodoStory extends Story +{ + #[\Override] + public function build(): void + { + $this->addState('owner', $user = UserFactory::createOne()); + $this->addState('getTodoList', $todoList = TodoListFactory::new()->withOwner($user)->create()); + + $tasks = [ + '1482' => 'WIP - failing generator test with global ns collision', + '1479' => '[make:entity] Cannot create an entity called "Locale"', + '1416' => 'Adding `ORM\JoinTable` attribute to the `make:entity` command\'s `ManyToMany` field type', + ]; + + foreach ($tasks as $gitHubNumber => $taskName) { + $this->addState(sprintf('task%s', $gitHubNumber), TaskFactory::new()->withName((string) $gitHubNumber)->forTodoList($todoList)->create()); + + // $this->addState(sprintf('expectedUrlData%s', $gitHubNumber), $urlData); + // + // $this->addState(sprintf('expectedPullRequest%s', $gitHubNumber), GitHubPullRequestFactory::new()->fromUrlData( + // urlData: $urlData, + // title: $taskName, + // )->create()); + } + } + + public static function getExpectedUrlData(int $gitHubNumber): GitHubUrlData + { + $pullUri = 'https://github.com/rushlow-development/big-desk/pull/'; + + return new GitHubUrlData( + owner: 'rushlow-development', + repository: 'big-desk', + type: TypeEnum::PULL_REQUEST, + identifier: $gitHubNumber, + uri: $pullUri.$gitHubNumber + ); + } + + public static function getExpectedPullRequest(int $gitHubNumber): GitHubPullRequest + { + $tasks = [ + '1482' => 'WIP - failing generator test with global ns collision', + '1479' => '[make:entity] Cannot create an entity called "Locale"', + '1416' => 'Adding `ORM\JoinTable` attribute to the `make:entity` command\'s `ManyToMany` field type', + ]; + + return new GitHubPullRequest( + uri: 'https://github.com/rushlow-development/big-desk/pull/'.$gitHubNumber, + owner: 'rushlow-development', + repo: 'big-desk', + number: $gitHubNumber, + title: $tasks[$gitHubNumber], + ); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 469dcce..f7fb8a1 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,5 +1,6 @@ bootEnv(dirname(__DIR__).'/.env'); } + +BypassFinals::enable(); diff --git a/tools/rector/composer.json b/tools/rector/composer.json index 056d015..01833af 100644 --- a/tools/rector/composer.json +++ b/tools/rector/composer.json @@ -1,5 +1,6 @@ { "require": { - "rector/rector": "^1.0" + "rector/rector": "^1.0", + "phpstan/phpstan-doctrine": "^1.3" } }