diff --git a/.codecov.yml b/.codecov.yml index 261377d..3078c1c 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -7,6 +7,12 @@ ignore: - "src/Dto/*.php" - "src/Kernel.php" - "src/EvaluatorBundle.php" + - "**/*Dto.php" + - "**/*Collection.php" + - "**/*ConfigurationProvider.php" + - "src/ReleaseApp/Domain/Entities/*" + - "src/ReleaseApp/Domain/Client/*" + - "src/ReleaseApp/Infrastructure/Client/Request/*" - "bin" - "tests" - "**/*.md" diff --git a/composer.json b/composer.json index c01d7da..67c1e65 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,8 @@ "symfony/uid": "^5.4|^6.0", "symfony/serializer": "^5.4|^6.0", "symfony/stopwatch": "^5.4|^6.0", - "symfony/yaml": "^5.4|^6.0" + "symfony/yaml": "^5.4|^6.0", + "laminas/laminas-filter": "^2.11.0" }, "require-dev": { "phpstan/phpstan": "^1.10", diff --git a/config/checkers.yaml b/config/checkers.yaml index ec4dd08..e957b0a 100644 --- a/config/checkers.yaml +++ b/config/checkers.yaml @@ -10,7 +10,10 @@ parameters: dead_code_checker_doc_url: https://docs.spryker.com/docs/scos/dev/guidelines/keeping-a-project-upgradable/upgradability-guidelines/dead-code-checker.html # Security checker - security_checker_doc_url: https://docs.spryker.com/docs/scos/dev/guidelines/keeping-a-project-upgradable/upgradability-guidelines/security.html + open_source_vulnerabilities_checker_doc_url: https://docs.spryker.com/docs/scos/dev/guidelines/keeping-a-project-upgradable/upgradability-guidelines/open-source-vulnerabilities.html + + # Spryker security checker + spryker_open_source_vulnerabilities_checker_doc_url: https://docs.spryker.com/docs/scos/dev/guidelines/keeping-a-project-upgradable/upgradability-guidelines/spryker-security.html # Multidimensional array checker multidimensional_array_checker_doc_url: https://docs.spryker.com/docs/scos/dev/guidelines/keeping-a-project-upgradable/upgradability-guidelines/multidimensional-array.html diff --git a/config/services.yaml b/config/services.yaml index 4984b46..82cde9e 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -27,6 +27,15 @@ services: resource: '../src/' exclude: - '../src/Kernel.php' + - '../src/Dto/*' + - '../src/Report/Dto/*' + - '../src/Checker/PhpVersionChecker/CheckerStrategyResponse.php' + - '../src/ReleaseApp/Domain/Entities/*' + - '../src/ReleaseApp/Domain/Client/Request/*' + - '../src/ReleaseApp/Domain/Client/Response/*' + - '../src/ReleaseApp/Domain/Exception/*' + - '../src/ReleaseApp/Infrastructure/Client/Request/*' + - '../src/ReleaseApp/Infrastructure/Shared/Dto/*' symfony.console.application: class: Symfony\Bundle\FrameworkBundle\Console\Application @@ -76,11 +85,17 @@ services: - '@SprykerSdk\Evaluator\Checker\MultidimensionalArrayChecker\NestingStructure\ArrayMergeNestingStructure' - '@SprykerSdk\Evaluator\Checker\MultidimensionalArrayChecker\NestingStructure\ReturnArrayNestingStructure' - SprykerSdk\Evaluator\Checker\SecurityChecker\SecurityChecker: + SprykerSdk\Evaluator\Checker\OpenSourceVulnerabilitiesChecker\OpenSourceVulnerabilitiesChecker: arguments: - '@symfony.console.application' - '@SprykerSdk\Evaluator\Resolver\PathResolver' - - '%security_checker_doc_url%' + - '%open_source_vulnerabilities_checker_doc_url%' + + SprykerSdk\Evaluator\Checker\SprykerSecurityChecker\SprykerSecurityChecker: + arguments: + - '@SprykerSdk\Evaluator\Reader\ComposerReaderInterface' + - '@SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Service\ReleaseAppService' + - '%spryker_security_checker_doc_url%' SprykerSdk\Evaluator\Checker\DependencyProviderAdditionalLogicChecker\DependencyProviderAdditionalLogicChecker: arguments: @@ -147,3 +162,9 @@ services: - '%project_id%' - '%repository_name%' - '%organization_name%' + + SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Client\HttpRequestExecutor: + public: true + + SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Service\ReleaseAppService: + public: true diff --git a/phpstan.neon b/phpstan.neon index 0ad9c3e..404a6a3 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,3 +2,5 @@ parameters: level: 8 paths: - src/ + checkGenericClassInNonGenericObjectType: false + treatPhpDocTypesAsCertain: false diff --git a/src/Checker/SecurityChecker/SecurityChecker.php b/src/Checker/OpenSourceVulnerabilitiesChecker/OpenSourceVulnerabilitiesChecker.php similarity index 94% rename from src/Checker/SecurityChecker/SecurityChecker.php rename to src/Checker/OpenSourceVulnerabilitiesChecker/OpenSourceVulnerabilitiesChecker.php index 8ffc643..399dc5b 100644 --- a/src/Checker/SecurityChecker/SecurityChecker.php +++ b/src/Checker/OpenSourceVulnerabilitiesChecker/OpenSourceVulnerabilitiesChecker.php @@ -7,7 +7,7 @@ declare(strict_types=1); -namespace SprykerSdk\Evaluator\Checker\SecurityChecker; +namespace SprykerSdk\Evaluator\Checker\OpenSourceVulnerabilitiesChecker; use SprykerSdk\Evaluator\Checker\CheckerInterface; use SprykerSdk\Evaluator\Dto\CheckerInputDataDto; @@ -18,12 +18,12 @@ use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\BufferedOutput; -class SecurityChecker implements CheckerInterface +class OpenSourceVulnerabilitiesChecker implements CheckerInterface { /** * @var string */ - public const NAME = 'SECURITY_CHECKER'; + public const NAME = 'OPEN_SOURCE_VULNERABILITIES_CHECKER'; /** * @var string diff --git a/src/Checker/SecurityChecker/SprykerSecurityChecker.php b/src/Checker/SecurityChecker/SprykerSecurityChecker.php new file mode 100644 index 0000000..4f1ab49 --- /dev/null +++ b/src/Checker/SecurityChecker/SprykerSecurityChecker.php @@ -0,0 +1,159 @@ +composerReader = $composerReader; + $this->releaseAppService = $releaseAppService; + $this->checkerDocUrl = $checkerDocUrl; + } + + /** + * @return string + */ + public function getName(): string + { + return static::NAME; + } + + /** + * @param \SprykerSdk\Evaluator\Dto\CheckerInputDataDto $inputData + * + * @return \SprykerSdk\Evaluator\Dto\CheckerResponseDto + */ + public function check(CheckerInputDataDto $inputData): CheckerResponseDto + { + try { + $releaseAppResponse = $this->releaseAppService->getNewSecurityReleaseGroups($this->createDataProviderRequest()); + } catch (RuntimeException $exception) { + $violation = new ViolationDto( + sprintf( + 'Service is not available, please try latter. Error: %s %s', + $exception->getCode(), + $exception->getMessage(), + ), + $this->getName(), + ); + + return new CheckerResponseDto([$violation], $this->checkerDocUrl); + } + + return new CheckerResponseDto($this->buildViolations($releaseAppResponse), $this->checkerDocUrl); + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\ReleaseAppResponse $releaseAppResponse + * + * @return array<\SprykerSdk\Evaluator\Dto\ViolationDto> + */ + protected function buildViolations(ReleaseAppResponse $releaseAppResponse): array + { + $violations = []; + + foreach ($releaseAppResponse->getReleaseGroupCollection()->toArray() as $releaseGroupDto) { + $violations = [...$violations, ...$this->buildViolationsByReleaseGroup($releaseGroupDto)]; + } + + return $violations; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\ReleaseGroupDto $releaseGroupDto + * + * @return array<\SprykerSdk\Evaluator\Dto\ViolationDto> + */ + protected function buildViolationsByReleaseGroup(ReleaseGroupDto $releaseGroupDto): array + { + $violations = []; + + foreach ($releaseGroupDto->getModuleCollection()->toArray() as $moduleDto) { + $installedVersion = $this->composerReader->getPackageVersion($moduleDto->getName()); + if ($installedVersion === null) { + continue; + } + + $installedMajorVersion = SemanticVersionHelper::getMajorVersion($installedVersion); + $securityUpdateMajorVersion = SemanticVersionHelper::getMajorVersion($moduleDto->getVersion()); + if ($securityUpdateMajorVersion !== $installedMajorVersion) { + continue; + } + if (!Comparator::greaterThan($moduleDto->getVersion(), $installedVersion)) { + continue; + } + + $violations[] = new ViolationDto( + sprintf( + 'Security update available for the module %s, actual version %s', + $moduleDto->getName(), + $installedVersion, + ), + sprintf('%s:%s', $moduleDto->getName(), $moduleDto->getVersion()), + ); + } + + return $violations; + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Client\Request\UpgradeAnalysisRequest + */ + protected function createDataProviderRequest(): UpgradeAnalysisRequest + { + $projectName = $this->composerReader->getProjectName(); + $composerJson = $this->composerReader->getComposerData(); + $composerLock = $this->composerReader->getComposerLockData(); + + return new UpgradeAnalysisRequest($projectName, $composerJson, $composerLock); + } +} diff --git a/src/Helper/SemanticVersionHelper.php b/src/Helper/SemanticVersionHelper.php new file mode 100644 index 0000000..b25c5fc --- /dev/null +++ b/src/Helper/SemanticVersionHelper.php @@ -0,0 +1,44 @@ +attach($separateAbbreviation ? new CamelCaseToDash() : new CamelCaseToDashWithoutAbbreviation()); + $filterChain->attach(new StringToLower()); + + return $filterChain->filter($value); + } + + /** + * @param string $value + * @param bool $upperCaseFirst + * + * @return string + */ + public static function dashToCamelCase(string $value, bool $upperCaseFirst = true): string + { + $filterChain = new FilterChain(); + $filterChain->attach(new DashToCamelCase()); + + if ($upperCaseFirst) { + return ucfirst($filterChain->filter($value)); + } + + // Set first character in original case + + return mb_substr($value, 0, 1) . mb_substr($filterChain->filter($value), 1); + } + + /** + * Spryker.SymfonyMailer => spryker/symfony-mailer + * + * @param string $originName + * + * @return string + */ + public static function packageCamelCaseToDash(string $originName): string + { + [$organization, $package] = explode('.', $originName); + + return implode('/', [ + static::camelCaseToDash($organization), + static::camelCaseToDash($package), + ]); + } +} diff --git a/src/Helper/UtilText/Filter/CamelCaseToDash.php b/src/Helper/UtilText/Filter/CamelCaseToDash.php new file mode 100644 index 0000000..1726d49 --- /dev/null +++ b/src/Helper/UtilText/Filter/CamelCaseToDash.php @@ -0,0 +1,32 @@ +getSeparator(), '$') . '$2', $string); + + return is_array($value) ? (string)array_shift($value) : (string)$value; + } +} diff --git a/src/Reader/ComposerReader.php b/src/Reader/ComposerReader.php index fe480d7..1ca795b 100644 --- a/src/Reader/ComposerReader.php +++ b/src/Reader/ComposerReader.php @@ -24,6 +24,26 @@ class ComposerReader implements ComposerReaderInterface */ protected const COMPOSER_LOCK_FILE_NAME = 'composer.lock'; + /** + * @var string + */ + protected const PACKAGES_KEY = 'packages'; + + /** + * @var string + */ + protected const PACKAGES_DEV_KEY = 'packages-dev'; + + /** + * @var string + */ + protected const NAME_KEY = 'name'; + + /** + * @var string + */ + protected const VERSION_KEY = 'version'; + /** * @var \SprykerSdk\Evaluator\Resolver\PathResolverInterface */ @@ -70,4 +90,38 @@ protected function readFile(string $filePath): array return json_decode($content, true, 512, \JSON_THROW_ON_ERROR); } + + /** + * @param string $packageName + * + * @return string|null + */ + public function getPackageVersion(string $packageName): ?string + { + $composerLock = $this->getComposerLockData(); + + foreach ($composerLock[static::PACKAGES_KEY] as $package) { + if ($package[static::NAME_KEY] == $packageName) { + return $package[static::VERSION_KEY]; + } + } + + foreach ($composerLock[static::PACKAGES_DEV_KEY] as $package) { + if ($package[static::NAME_KEY] == $packageName) { + return $package[static::VERSION_KEY]; + } + } + + return null; + } + + /** + * @return string + */ + public function getProjectName(): string + { + $composerJsonContent = $this->getComposerData(); + + return $composerJsonContent[static::NAME_KEY]; + } } diff --git a/src/Reader/ComposerReaderInterface.php b/src/Reader/ComposerReaderInterface.php index 712078b..9daf7ca 100644 --- a/src/Reader/ComposerReaderInterface.php +++ b/src/Reader/ComposerReaderInterface.php @@ -20,4 +20,16 @@ public function getComposerData(): array; * @return array */ public function getComposerLockData(): array; + + /** + * @param string $packageName + * + * @return string|null + */ + public function getPackageVersion(string $packageName): ?string; + + /** + * @return string + */ + public function getProjectName(): string; } diff --git a/src/ReleaseApp/Application/Configuration/ConfigurationProviderInterface.php b/src/ReleaseApp/Application/Configuration/ConfigurationProviderInterface.php new file mode 100644 index 0000000..4388b80 --- /dev/null +++ b/src/ReleaseApp/Application/Configuration/ConfigurationProviderInterface.php @@ -0,0 +1,18 @@ +releaseAppClient = $releaseAppClient; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Client\Request\UpgradeAnalysisRequest $upgradeAnalysisRequest + * + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\Collection\UpgradeInstructionsReleaseGroupCollection + */ + public function getNewReleaseGroupsSortedByReleaseDate( + UpgradeAnalysisRequest $upgradeAnalysisRequest + ): UpgradeInstructionsReleaseGroupCollection { + $moduleVersionCollection = $this->getModuleVersionCollection($upgradeAnalysisRequest)->getSecurityFixes(); + + $releaseGroupCollection = $this->getReleaseGroupCollection($moduleVersionCollection) + ->getOnlyWithReleasedDate()->getSecurityFixes()->sortByReleasedDate(); + + return $releaseGroupCollection; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\Collection\UpgradeAnalysisModuleVersionCollection $moduleVersionCollection + * + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\Collection\UpgradeInstructionsReleaseGroupCollection + */ + protected function getReleaseGroupCollection( + UpgradeAnalysisModuleVersionCollection $moduleVersionCollection + ): UpgradeInstructionsReleaseGroupCollection { + $releaseGroupCollection = new UpgradeInstructionsReleaseGroupCollection(); + + foreach ($moduleVersionCollection->toArray() as $moduleVersion) { + $request = new UpgradeInstructionsRequest($moduleVersion->getId()); + $response = $this->releaseAppClient->getUpgradeInstructions($request); + $releaseGroupCollection->add($response->getReleaseGroup()); + } + + return $releaseGroupCollection; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Client\Request\UpgradeAnalysisRequest $request + * + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\Collection\UpgradeAnalysisModuleVersionCollection + */ + protected function getModuleVersionCollection( + UpgradeAnalysisRequest $request + ): UpgradeAnalysisModuleVersionCollection { + $response = $this->releaseAppClient->getUpgradeAnalysis($request); + + return $response->getModuleCollection() + ->getModulesWithVersions() + ->getModuleVersions(); + } +} diff --git a/src/ReleaseApp/Application/Service/ReleaseAppServiceInterface.php b/src/ReleaseApp/Application/Service/ReleaseAppServiceInterface.php new file mode 100644 index 0000000..3cd6f07 --- /dev/null +++ b/src/ReleaseApp/Application/Service/ReleaseAppServiceInterface.php @@ -0,0 +1,23 @@ + + */ + protected array $composerJson; + + /** + * @var array + */ + protected array $composerLock; + + /** + * @param string $projectName + * @param array $composerJson + * @param array $composerLock + */ + public function __construct(string $projectName, array $composerJson, array $composerLock) + { + $this->projectName = $projectName; + $this->composerJson = $composerJson; + $this->composerLock = $composerLock; + } + + /** + * @return string + */ + public function getBody(): string + { + $json_encode = (string)json_encode($this->getBodyArray()); + + file_put_contents('./request.json', $json_encode); + + return $json_encode; + } + + /** + * @return string + */ + public function getResponseClass(): string + { + return UpgradeAnalysis::class; + } + + /** + * @return string|null + */ + public function getParameters(): ?string + { + return null; + } + + /** + * @return array + */ + protected function getBodyArray(): array + { + $composerJsonContent = json_encode($this->composerJson); + $composerLockContent = json_encode($this->composerLock); + + return [ + static::PROJECT_NAME_KEY => $this->projectName, + static::COMPOSER_JSON_KEY => $composerJsonContent, + static::COMPOSER_LOCK_KEY => $composerLockContent, + ]; + } +} diff --git a/src/ReleaseApp/Domain/Client/Request/UpgradeInstructionsRequest.php b/src/ReleaseApp/Domain/Client/Request/UpgradeInstructionsRequest.php new file mode 100644 index 0000000..9c439da --- /dev/null +++ b/src/ReleaseApp/Domain/Client/Request/UpgradeInstructionsRequest.php @@ -0,0 +1,52 @@ +idModuleVersion = $moduleVersionId; + } + + /** + * @return string|null + */ + public function getBody(): ?string + { + return null; + } + + /** + * @return string + */ + public function getResponseClass(): string + { + return UpgradeInstructions::class; + } + + /** + * @return string|null + */ + public function getParameters(): ?string + { + return sprintf('%s=%s', 'module_version_id', $this->idModuleVersion); + } +} diff --git a/src/ReleaseApp/Domain/Client/Response/Response.php b/src/ReleaseApp/Domain/Client/Response/Response.php new file mode 100644 index 0000000..866dd04 --- /dev/null +++ b/src/ReleaseApp/Domain/Client/Response/Response.php @@ -0,0 +1,54 @@ + + */ + protected array $body; + + /** + * @param int $code + * @param string $body + */ + public function __construct(int $code, string $body) + { + $this->code = $code; + $this->body = json_decode($body, true); + } + + /** + * @return int + */ + public function getCode(): int + { + return $this->code; + } + + /** + * @return array|null + */ + public function getBody(): ?array + { + return $this->body[static::RESULT_KEY]; + } +} diff --git a/src/ReleaseApp/Domain/Client/Response/ResponseInterface.php b/src/ReleaseApp/Domain/Client/Response/ResponseInterface.php new file mode 100644 index 0000000..cb59dfa --- /dev/null +++ b/src/ReleaseApp/Domain/Client/Response/ResponseInterface.php @@ -0,0 +1,23 @@ +|null + */ + public function getBody(): ?array; +} diff --git a/src/ReleaseApp/Domain/Entities/Collection/UpgradeAnalysisModuleCollection.php b/src/ReleaseApp/Domain/Entities/Collection/UpgradeAnalysisModuleCollection.php new file mode 100644 index 0000000..5d9709f --- /dev/null +++ b/src/ReleaseApp/Domain/Entities/Collection/UpgradeAnalysisModuleCollection.php @@ -0,0 +1,69 @@ + + */ + protected array $elements = []; + + /** + * @param array<\SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeAnalysisModule> $elements + */ + public function __construct(array $elements = []) + { + $this->elements = $elements; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeAnalysisModule $element + * + * @return void + */ + public function add(UpgradeAnalysisModule $element): void + { + $this->elements[] = $element; + } + + /** + * @return self + */ + public function getModulesWithVersions(): self + { + $collection = new self(); + foreach ($this->elements as $module) { + if (!$module->getModuleVersionCollection()->isEmpty()) { + $collection->add($module); + } + } + + return $collection; + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\Collection\UpgradeAnalysisModuleVersionCollection + */ + public function getModuleVersions(): UpgradeAnalysisModuleVersionCollection + { + $collection = new UpgradeAnalysisModuleVersionCollection(); + + foreach ($this->elements as $module) { + foreach ($module->getModuleVersionCollection()->toArray() as $moduleVersion) { + $collection->add($moduleVersion); + } + } + + return $collection; + } +} diff --git a/src/ReleaseApp/Domain/Entities/Collection/UpgradeAnalysisModuleVersionCollection.php b/src/ReleaseApp/Domain/Entities/Collection/UpgradeAnalysisModuleVersionCollection.php new file mode 100644 index 0000000..94e0e1f --- /dev/null +++ b/src/ReleaseApp/Domain/Entities/Collection/UpgradeAnalysisModuleVersionCollection.php @@ -0,0 +1,75 @@ + + */ + protected array $elements = []; + + /** + * @param array<\SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeAnalysisModuleVersion> $elements + */ + public function __construct(array $elements = []) + { + $this->elements = $elements; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeAnalysisModuleVersion $element + * + * @return void + */ + public function add(UpgradeAnalysisModuleVersion $element): void + { + $this->elements[] = $element; + } + + /** + * @return array<\SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeAnalysisModuleVersion> + */ + public function toArray(): array + { + return $this->elements; + } + + /** + * @return int + */ + public function count(): int + { + return count($this->elements); + } + + /** + * @return bool + */ + public function isEmpty(): bool + { + return !$this->elements; + } + + /** + * @return self + */ + public function getSecurityFixes(): self + { + return new self( + array_filter( + $this->elements, + fn (UpgradeAnalysisModuleVersion $element): bool => $element->isSecurity() + ), + ); + } +} diff --git a/src/ReleaseApp/Domain/Entities/Collection/UpgradeInstructionModuleCollection.php b/src/ReleaseApp/Domain/Entities/Collection/UpgradeInstructionModuleCollection.php new file mode 100644 index 0000000..a61ea87 --- /dev/null +++ b/src/ReleaseApp/Domain/Entities/Collection/UpgradeInstructionModuleCollection.php @@ -0,0 +1,102 @@ + + */ + protected array $elements = []; + + /** + * @param array<\SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeInstructionModule> $elements + */ + public function __construct(array $elements = []) + { + $this->elements = $elements; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeInstructionModule $element + * + * @return void + */ + public function add(UpgradeInstructionModule $element): void + { + $this->elements[] = $element; + } + + /** + * @return array<\SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeInstructionModule> + */ + public function toArray(): array + { + return $this->elements; + } + + /** + * @return int + */ + public function count(): int + { + return count($this->elements); + } + + /** + * @return bool + */ + public function isEmpty(): bool + { + return !$this->elements; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\Collection\UpgradeInstructionModuleCollection|self $collectionToMerge + * + * @return void + */ + public function addCollection(self $collectionToMerge): void + { + $this->elements = array_merge($this->elements, $collectionToMerge->toArray()); + } + + /** + * @param string $name + * + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeInstructionModule|null + */ + public function getFirstByName(string $name): ?UpgradeInstructionModule + { + foreach ($this->elements as $module) { + if ($module->getName() === $name) { + return $module; + } + } + + return null; + } + + /** + * @param string $name + * + * @return void + */ + public function deleteByName(string $name): void + { + foreach ($this->elements as $key => $module) { + if ($module->getName() === $name) { + unset($this->elements[$key]); + } + } + } +} diff --git a/src/ReleaseApp/Domain/Entities/Collection/UpgradeInstructionsReleaseGroupCollection.php b/src/ReleaseApp/Domain/Entities/Collection/UpgradeInstructionsReleaseGroupCollection.php new file mode 100644 index 0000000..f3ab3b0 --- /dev/null +++ b/src/ReleaseApp/Domain/Entities/Collection/UpgradeInstructionsReleaseGroupCollection.php @@ -0,0 +1,138 @@ + + */ + protected array $elements = []; + + /** + * @param array<\SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeInstructionsReleaseGroup> $elements + */ + public function __construct(array $elements = []) + { + $this->elements = $elements; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeInstructionsReleaseGroup $element + * + * @return void + */ + public function add(UpgradeInstructionsReleaseGroup $element): void + { + $this->elements[] = $element; + } + + /** + * @return array<\SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeInstructionsReleaseGroup> + */ + public function toArray(): array + { + return $this->elements; + } + + /** + * @return int + */ + public function count(): int + { + return count($this->elements); + } + + /** + * @return bool + */ + public function isEmpty(): bool + { + return !$this->elements; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\Collection\UpgradeInstructionsReleaseGroupCollection|self $collectionToMerge + * + * @return void + */ + public function addCollection(self $collectionToMerge): void + { + $this->elements = array_merge($this->elements, $collectionToMerge->toArray()); + } + + /** + * @return self + */ + public function sortByReleasedDate(): self + { + $sortData = []; + + foreach ($this->elements as $releaseGroup) { + $timestamp = $releaseGroup->getReleased()->getTimestamp(); + $sortData[$timestamp] = $releaseGroup; + } + + ksort($sortData); + + return new self(array_values($sortData)); + } + + /** + * @return self + */ + public function getSecurityFixes(): self + { + return new self( + array_filter( + $this->elements, + fn (UpgradeInstructionsReleaseGroup $releaseGroup): bool => $releaseGroup->isSecurity() + ), + ); + } + + /** + * @return self + */ + public function getNonSecurityFixes(): self + { + return new self( + array_filter( + $this->elements, + fn (UpgradeInstructionsReleaseGroup $releaseGroup): bool => !$releaseGroup->isSecurity() + ), + ); + } + + /** + * @return self + */ + public function getOnlyWithReleasedDate(): self + { + $result = new self(); + + foreach ($this->elements as $releaseGroup) { + try { + $dateTime = $releaseGroup->getReleased(); + } catch (RuntimeException $exception) { + $dateTime = null; + } + + if ($dateTime) { + $result->add($releaseGroup); + } + } + + return $result; + } +} diff --git a/src/ReleaseApp/Domain/Entities/UpgradeAnalysis.php b/src/ReleaseApp/Domain/Entities/UpgradeAnalysis.php new file mode 100644 index 0000000..ff6d58b --- /dev/null +++ b/src/ReleaseApp/Domain/Entities/UpgradeAnalysis.php @@ -0,0 +1,65 @@ +moduleCollection) { + return $this->moduleCollection; + } + + $moduleList = []; + foreach ($this->getModules() as $moduleData) { + $moduleList[] = new UpgradeAnalysisModule($moduleData); + } + $this->moduleCollection = new UpgradeAnalysisModuleCollection($moduleList); + + return $this->moduleCollection; + } + + /** + * @throws \SprykerSdk\Evaluator\ReleaseApp\Domain\Exception\ReleaseAppException + * + * @return array + */ + protected function getModules(): array + { + $body = $this->getBody(); + + if (!$body) { + throw new ReleaseAppException('Response body not found'); + } + + if (!array_key_exists(static::MODULES_KEY, $body)) { + throw new ReleaseAppException('Key modules not found'); + } + + return $body[static::MODULES_KEY]; + } +} diff --git a/src/ReleaseApp/Domain/Entities/UpgradeAnalysisModule.php b/src/ReleaseApp/Domain/Entities/UpgradeAnalysisModule.php new file mode 100644 index 0000000..6e9f251 --- /dev/null +++ b/src/ReleaseApp/Domain/Entities/UpgradeAnalysisModule.php @@ -0,0 +1,76 @@ + + */ + protected array $body; + + /** + * @var \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\Collection\UpgradeAnalysisModuleVersionCollection|null + */ + protected ?UpgradeAnalysisModuleVersionCollection $moduleVersionCollection = null; + + /** + * @param array $bodyArray + */ + public function __construct(array $bodyArray) + { + $this->body = $bodyArray; + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\Collection\UpgradeAnalysisModuleVersionCollection + */ + public function getModuleVersionCollection(): UpgradeAnalysisModuleVersionCollection + { + if ($this->moduleVersionCollection) { + return $this->moduleVersionCollection; + } + + $moduleVersionList = []; + foreach ($this->body[static::MODULE_VERSIONS_KEY] as $moduleVersionData) { + $moduleVersionList[] = new UpgradeAnalysisModuleVersion($moduleVersionData); + } + $this->moduleVersionCollection = new UpgradeAnalysisModuleVersionCollection($moduleVersionList); + + return $this->moduleVersionCollection; + } + + /** + * @throws \SprykerSdk\Evaluator\ReleaseApp\Domain\Exception\ReleaseAppException + * + * @return string + */ + public function getPackage(): string + { + if (!array_key_exists(static::PACKAGE_KEY, $this->body)) { + throw new ReleaseAppException(sprintf('Key %s not found', static::PACKAGE_KEY)); + } + + return $this->body[static::PACKAGE_KEY]; + } +} diff --git a/src/ReleaseApp/Domain/Entities/UpgradeAnalysisModuleVersion.php b/src/ReleaseApp/Domain/Entities/UpgradeAnalysisModuleVersion.php new file mode 100644 index 0000000..f175056 --- /dev/null +++ b/src/ReleaseApp/Domain/Entities/UpgradeAnalysisModuleVersion.php @@ -0,0 +1,112 @@ + + */ + protected array $body; + + /** + * @param array $bodyArray + */ + public function __construct(array $bodyArray) + { + $this->body = $bodyArray; + } + + /** + * @throws \SprykerSdk\Evaluator\ReleaseApp\Domain\Exception\ReleaseAppException + * + * @return int + */ + public function getId(): int + { + if (!array_key_exists(static::ID_KEY, $this->body)) { + throw new ReleaseAppException(sprintf('Key %s not found', static::ID_KEY)); + } + + return $this->body[static::ID_KEY]; + } + + /** + * @throws \SprykerSdk\Evaluator\ReleaseApp\Domain\Exception\ReleaseAppException + * + * @return int + */ + public function getName(): int + { + if (!array_key_exists(static::NAME_KEY, $this->body)) { + throw new ReleaseAppException(sprintf('Key %s not found', static::NAME_KEY)); + } + + return $this->body[static::NAME_KEY]; + } + + /** + * @return bool + */ + public function isSecurity(): bool + { + if (!array_key_exists(static::SECURITY_KEY, $this->body)) { + return false; + } + + return $this->body[static::SECURITY_KEY]; + } + + /** + * @throws \SprykerSdk\Evaluator\ReleaseApp\Domain\Exception\ReleaseAppException + * + * @return \DateTimeInterface + */ + public function getCreated(): DateTimeInterface + { + $dataTime = DateTime::createFromFormat( + ReleaseAppConstant::RESPONSE_DATA_TIME_FORMAT, + $this->body[static::CREATED_KEY], + ); + + if (!$dataTime) { + $message = sprintf('%s %s', 'Invalid datatime format:', $this->body[static::CREATED_KEY]); + + throw new ReleaseAppException($message); + } + + return $dataTime; + } +} diff --git a/src/ReleaseApp/Domain/Entities/UpgradeInstructionMeta.php b/src/ReleaseApp/Domain/Entities/UpgradeInstructionMeta.php new file mode 100644 index 0000000..24f2dc8 --- /dev/null +++ b/src/ReleaseApp/Domain/Entities/UpgradeInstructionMeta.php @@ -0,0 +1,114 @@ + + */ + protected array $body; + + /** + * @param array $body + */ + public function __construct(array $body) + { + $this->body = $body; + $this->includeModuleCollection = new UpgradeInstructionModuleCollection( + $this->getModuleListByKey(static::INCLUDE_KEY), + ); + $this->excludeModuleCollection = new UpgradeInstructionModuleCollection( + $this->getModuleListByKey(static::EXCLUDE_KEY), + ); + $this->conflictModuleCollection = new UpgradeInstructionModuleCollection( + $this->getModuleListByKey(static::CONFLICT_KEY), + ); + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\Collection\UpgradeInstructionModuleCollection + */ + public function getInclude(): UpgradeInstructionModuleCollection + { + return $this->includeModuleCollection; + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\Collection\UpgradeInstructionModuleCollection + */ + public function getExclude(): UpgradeInstructionModuleCollection + { + return $this->excludeModuleCollection; + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\Collection\UpgradeInstructionModuleCollection + */ + public function getConflict(): UpgradeInstructionModuleCollection + { + return $this->conflictModuleCollection; + } + + /** + * @param string $key + * + * @return array<\SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeInstructionModule> + */ + protected function getModuleListByKey(string $key): array + { + $list = []; + if (!isset($this->body[$key])) { + return $list; + } + + foreach ($this->body[$key] as $name => $version) { + $list[] = new UpgradeInstructionModule( + [UpgradeInstructionModule::VERSION_KEY => $version], + TextCaseHelper::packageCamelCaseToDash($name), + ); + } + + return $list; + } +} diff --git a/src/ReleaseApp/Domain/Entities/UpgradeInstructionModule.php b/src/ReleaseApp/Domain/Entities/UpgradeInstructionModule.php new file mode 100644 index 0000000..598147e --- /dev/null +++ b/src/ReleaseApp/Domain/Entities/UpgradeInstructionModule.php @@ -0,0 +1,101 @@ + + */ + protected array $body; + + /** + * @var string + */ + protected string $name; + + /** + * @param array $body + * @param string $name + */ + public function __construct(array $body, string $name) + { + $this->body = $body; + $this->name = $name; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @throws \SprykerSdk\Evaluator\ReleaseApp\Domain\Exception\ReleaseAppException + * + * @return string + */ + public function getVersion(): string + { + if (!array_key_exists(static::VERSION_KEY, $this->body)) { + throw new ReleaseAppException(sprintf('Key %s not found', static::VERSION_KEY)); + } + + return $this->body[static::VERSION_KEY]; + } + + /** + * @param string $version + * + * @return void + */ + public function setVersion(string $version): void + { + $this->body[static::VERSION_KEY] = $version; + } + + /** + * @throws \SprykerSdk\Evaluator\ReleaseApp\Domain\Exception\ReleaseAppException + * + * @return string + */ + public function getType(): string + { + if (!array_key_exists(static::TYPE_KEY, $this->body)) { + throw new ReleaseAppException(sprintf('Key %s not found', static::TYPE_KEY)); + } + + return $this->body[static::TYPE_KEY]; + } + + /** + * @param string $type + * + * @return void + */ + public function setType(string $type): void + { + $this->body[static::TYPE_KEY] = $type; + } +} diff --git a/src/ReleaseApp/Domain/Entities/UpgradeInstructions.php b/src/ReleaseApp/Domain/Entities/UpgradeInstructions.php new file mode 100644 index 0000000..a6d6595 --- /dev/null +++ b/src/ReleaseApp/Domain/Entities/UpgradeInstructions.php @@ -0,0 +1,37 @@ +getBody(); + + if (!$bodyArray) { + throw new ReleaseAppException('Response body not found'); + } + + return new UpgradeInstructionsReleaseGroup($bodyArray[static::RELEASE_GROUP_KEY]); + } +} diff --git a/src/ReleaseApp/Domain/Entities/UpgradeInstructionsReleaseGroup.php b/src/ReleaseApp/Domain/Entities/UpgradeInstructionsReleaseGroup.php new file mode 100644 index 0000000..2b5d3bb --- /dev/null +++ b/src/ReleaseApp/Domain/Entities/UpgradeInstructionsReleaseGroup.php @@ -0,0 +1,196 @@ + + */ + protected array $body; + + /** + * @var \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\Collection\UpgradeInstructionModuleCollection|null + */ + protected ?UpgradeInstructionModuleCollection $moduleCollection = null; + + /** + * @var \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeInstructionMeta|null + */ + protected ?UpgradeInstructionMeta $meta = null; + + /** + * @param array $bodyArray + */ + public function __construct(array $bodyArray) + { + $this->body = $bodyArray; + if (isset($this->body[static::META_KEY])) { + $this->meta = new UpgradeInstructionMeta($this->body[static::META_KEY]); + } + } + + /** + * @return string + */ + public function getName(): string + { + return $this->body[static::NAME_KEY]; + } + + /** + * @return bool + */ + public function hasProjectChanges(): bool + { + return $this->body[static::PROJECT_CHANGES_KEY]; + } + + /** + * @return int + */ + public function getId(): int + { + return (int)$this->body[static::ID_KEY]; + } + + /** + * @throws \SprykerSdk\Evaluator\ReleaseApp\Domain\Exception\ReleaseAppException + * + * @return \DateTimeInterface + */ + public function getReleased(): DateTimeInterface + { + if (!isset($this->body[static::RELEASED_KEY])) { + $message = sprintf('%s %s', 'Undefined key:', static::RELEASED_KEY); + + throw new ReleaseAppException($message); + } + + $dataTime = DateTime::createFromFormat( + ReleaseAppConstant::RESPONSE_DATA_TIME_FORMAT, + $this->body[static::RELEASED_KEY], + ); + + if (!$dataTime) { + $message = sprintf('%s %s', 'API invalid datatime format:', $this->body[static::RELEASED_KEY]); + + throw new ReleaseAppException($message); + } + + return $dataTime; + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\Collection\UpgradeInstructionModuleCollection + */ + public function getModuleCollection(): UpgradeInstructionModuleCollection + { + if ($this->moduleCollection) { + return $this->moduleCollection; + } + + $moduleList = []; + foreach ($this->body[static::MODULES_KEY] as $name => $moduleData) { + $moduleList[] = new UpgradeInstructionModule($moduleData, $name); + } + $this->moduleCollection = new UpgradeInstructionModuleCollection($moduleList); + + return $this->moduleCollection; + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeInstructionMeta|null + */ + public function getMeta(): ?UpgradeInstructionMeta + { + return $this->meta; + } + + /** + * @return string|null + */ + public function getJiraIssue(): ?string + { + return isset($this->body[static::JIRA_KEY]) ? $this->body[static::JIRA_KEY][static::ISSUE_KEY] : null; + } + + /** + * @return string|null + */ + public function getJiraIssueLink(): ?string + { + return isset($this->body[static::JIRA_KEY]) ? $this->body[static::JIRA_KEY][static::ISSUE_LINK_KEY] : null; + } + + /** + * @return bool + */ + public function isSecurity(): bool + { + return (bool)($this->body[static::SECURITY_KEY] ?? false); + } +} diff --git a/src/ReleaseApp/Domain/Exception/ReleaseAppException.php b/src/ReleaseApp/Domain/Exception/ReleaseAppException.php new file mode 100644 index 0000000..aec7f97 --- /dev/null +++ b/src/ReleaseApp/Domain/Exception/ReleaseAppException.php @@ -0,0 +1,16 @@ + + */ + protected const HTTP_HEADER_LIST = ['Content-Type' => 'application/json']; + + /** + * @var \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Configuration\ConfigurationProvider + */ + protected ConfigurationProvider $releaseAppConfig; + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Configuration\ConfigurationProvider $configurationProvider + */ + public function __construct(ConfigurationProvider $configurationProvider) + { + $this->releaseAppConfig = $configurationProvider; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Client\Request\HttpRequestInterface $request + * + * @return \GuzzleHttp\Psr7\Request + */ + public function createRequest(HttpRequestInterface $request): Request + { + return new Request( + $request->getMethod(), + $this->getBaseUrl() . $request->getEndpoint(), + static::HTTP_HEADER_LIST, + $request->getRequest()->getBody(), + ); + } + + /** + * @return string + */ + protected function getBaseUrl(): string + { + return $this->releaseAppConfig->getReleaseAppUrl(); + } +} diff --git a/src/ReleaseApp/Infrastructure/Client/Builder/HttpRequestBuilderInterface.php b/src/ReleaseApp/Infrastructure/Client/Builder/HttpRequestBuilderInterface.php new file mode 100644 index 0000000..a09b300 --- /dev/null +++ b/src/ReleaseApp/Infrastructure/Client/Builder/HttpRequestBuilderInterface.php @@ -0,0 +1,23 @@ +getRequest()->getResponseClass(); + $response = new $responseClass($guzzleResponse->getStatusCode(), $this->getBody($guzzleResponse)); + + return $response; + } + + /** + * @param \Psr\Http\Message\ResponseInterface $guzzleResponse + * + * @return string + */ + protected function getBody(ResponseInterface $guzzleResponse): string + { + $responseStream = $guzzleResponse->getBody(); + $responseStream->seek(0); + $length = $responseStream->getSize(); + $body = $responseStream->read((int)$length); + + return $body; + } +} diff --git a/src/ReleaseApp/Infrastructure/Client/Builder/HttpResponseBuilderInterface.php b/src/ReleaseApp/Infrastructure/Client/Builder/HttpResponseBuilderInterface.php new file mode 100644 index 0000000..0dbab72 --- /dev/null +++ b/src/ReleaseApp/Infrastructure/Client/Builder/HttpResponseBuilderInterface.php @@ -0,0 +1,25 @@ +requestBuilder = $requestBuilder; + $this->responseBuilder = $responseBuilder; + $this->requestExecutor = $requestExecutor; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Client\Request\UpgradeInstructionsRequest $instructionsRequest + * + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeInstructions + */ + public function getUpgradeInstructions(UpgradeInstructionsRequest $instructionsRequest): UpgradeInstructions + { + /** @var \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeInstructions $response */ + $response = $this->getResponse(new HttpUpgradeInstructionsRequest($instructionsRequest)); + + return $response; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Client\Request\UpgradeAnalysisRequest $upgradeAnalysisRequest + * + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeAnalysis + */ + public function getUpgradeAnalysis(UpgradeAnalysisRequest $upgradeAnalysisRequest): UpgradeAnalysis + { + /** @var \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeAnalysis $response */ + $response = $this->getResponse(new HttpUpgradeAnalysisHttpRequest($upgradeAnalysisRequest)); + + return $response; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Client\Request\HttpRequestInterface $request + * + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Client\Response\ResponseInterface + */ + protected function getResponse(HttpRequestInterface $request): ResponseInterface + { + $guzzleRequest = $this->requestBuilder->createRequest($request); + $guzzleResponse = $this->requestExecutor->execute($guzzleRequest); + $response = $this->responseBuilder->createHttpResponse($request, $guzzleResponse); + + return $response; + } +} diff --git a/src/ReleaseApp/Infrastructure/Client/HttpRequestExecutor.php b/src/ReleaseApp/Infrastructure/Client/HttpRequestExecutor.php new file mode 100644 index 0000000..c1213a5 --- /dev/null +++ b/src/ReleaseApp/Infrastructure/Client/HttpRequestExecutor.php @@ -0,0 +1,75 @@ +guzzleClient = $guzzleClient ?? new GuzzleHttp(); + $this->config = $config; + } + + /** + * @param \Psr\Http\Message\RequestInterface $request + * + * @throws \GuzzleHttp\Exception\GuzzleException + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function execute(RequestInterface $request): ResponseInterface + { + $attempts = 0; + $exception = null; + $guzzleResponse = null; + + do { + try { + $guzzleResponse = $this->guzzleClient->send($request); + } catch (ServerException $currentException) { + $exception = $currentException; + sleep($this->config->getHttpRetrieveRetryDelay()); + } finally { + ++$attempts; + } + } while ($attempts < $this->config->getHttpRetrieveAttemptsCount() && $guzzleResponse == null); + + if ($guzzleResponse === null) { + if ($exception) { + throw $exception; + } + + throw new ReleaseAppException('Failed to request url ' . $request->getUri()); + } + + return $guzzleResponse; + } +} diff --git a/src/ReleaseApp/Infrastructure/Client/HttpRequestExecutorInterface.php b/src/ReleaseApp/Infrastructure/Client/HttpRequestExecutorInterface.php new file mode 100644 index 0000000..258c44e --- /dev/null +++ b/src/ReleaseApp/Infrastructure/Client/HttpRequestExecutorInterface.php @@ -0,0 +1,25 @@ +domainRequest = $domainRequest; + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Client\Request\RequestInterface + */ + public function getRequest(): RequestInterface + { + return $this->domainRequest; + } + + /** + * @return string + */ + public function getEndpoint(): string + { + return static::REQUEST_ENDPOINT; + } + + /** + * @return string + */ + public function getMethod(): string + { + return static::REQUEST_METHOD_POST; + } +} diff --git a/src/ReleaseApp/Infrastructure/Client/Request/HttpUpgradeInstructionsRequest.php b/src/ReleaseApp/Infrastructure/Client/Request/HttpUpgradeInstructionsRequest.php new file mode 100644 index 0000000..8c04554 --- /dev/null +++ b/src/ReleaseApp/Infrastructure/Client/Request/HttpUpgradeInstructionsRequest.php @@ -0,0 +1,57 @@ +request = $domainRequest; + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Client\Request\RequestInterface + */ + public function getRequest(): RequestInterface + { + return $this->request; + } + + /** + * @return string + */ + public function getEndpoint(): string + { + return sprintf('%s?%s', static::REQUEST_ENDPOINT, $this->request->getParameters()); + } + + /** + * @return string + */ + public function getMethod(): string + { + return static::REQUEST_METHOD_POST; + } +} diff --git a/src/ReleaseApp/Infrastructure/Client/Request/HttpUpgradeReleaseGroupInstructionsRequest.php b/src/ReleaseApp/Infrastructure/Client/Request/HttpUpgradeReleaseGroupInstructionsRequest.php new file mode 100644 index 0000000..883062a --- /dev/null +++ b/src/ReleaseApp/Infrastructure/Client/Request/HttpUpgradeReleaseGroupInstructionsRequest.php @@ -0,0 +1,57 @@ +request = $domainRequest; + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Client\Request\RequestInterface + */ + public function getRequest(): RequestInterface + { + return $this->request; + } + + /** + * @return string + */ + public function getEndpoint(): string + { + return sprintf('%s?%s', static::REQUEST_ENDPOINT, $this->request->getParameters()); + } + + /** + * @return string + */ + public function getMethod(): string + { + return static::REQUEST_METHOD_POST; + } +} diff --git a/src/ReleaseApp/Infrastructure/Configuration/ConfigurationProvider.php b/src/ReleaseApp/Infrastructure/Configuration/ConfigurationProvider.php new file mode 100644 index 0000000..6854ee2 --- /dev/null +++ b/src/ReleaseApp/Infrastructure/Configuration/ConfigurationProvider.php @@ -0,0 +1,54 @@ +releaseApp = $releaseApp; + $this->releaseGroupDtoCollectionMapper = $releaseGroupDtoCollectionMapper; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Client\Request\UpgradeAnalysisRequest $upgradeAnalysisRequest + * + * @return \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\ReleaseAppResponse + */ + public function getNewSecurityReleaseGroups(UpgradeAnalysisRequest $upgradeAnalysisRequest): ReleaseAppResponse + { + $releaseGroupCollection = $this->releaseGroupDtoCollectionMapper->mapReleaseGroupTransferCollection( + $this->releaseApp->getNewReleaseGroupsSortedByReleaseDate($upgradeAnalysisRequest), + ); + + return new ReleaseAppResponse($releaseGroupCollection); + } +} diff --git a/src/ReleaseApp/Infrastructure/Service/ReleaseAppServiceInterface.php b/src/ReleaseApp/Infrastructure/Service/ReleaseAppServiceInterface.php new file mode 100644 index 0000000..5af547a --- /dev/null +++ b/src/ReleaseApp/Infrastructure/Service/ReleaseAppServiceInterface.php @@ -0,0 +1,23 @@ + + */ + protected array $elements = []; + + /** + * @param array<\SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\ModuleDto> $elements + */ + public function __construct(array $elements = []) + { + $this->elements = $elements; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\ModuleDto $element + * + * @return void + */ + public function add(ModuleDto $element): void + { + $this->elements[] = $element; + } + + /** + * @return array<\SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\ModuleDto> + */ + public function toArray(): array + { + return $this->elements; + } + + /** + * @return int + */ + public function count(): int + { + return count($this->elements); + } + + /** + * @return bool + */ + public function isEmpty(): bool + { + return !$this->elements; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\Collection\ModuleDtoCollection|self $collectionToMerge + * + * @return void + */ + public function addCollection(self $collectionToMerge): void + { + $this->elements = array_merge($this->elements, $collectionToMerge->toArray()); + } + + /** + * @return array<\SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\ModuleDto> + */ + public function getMajors(): array + { + return array_filter( + $this->elements, + static fn (ModuleDto $module): bool => $module->getVersionType() === ReleaseAppConstant::MODULE_TYPE_MAJOR, + ); + } + + /** + * @return int + */ + public function getMajorAmount(): int + { + $result = 0; + foreach ($this->elements as $module) { + if ($module->getVersionType() === ReleaseAppConstant::MODULE_TYPE_MAJOR) { + $result++; + } + } + + return $result; + } + + /** + * @return int + */ + public function getMinorAmount(): int + { + $result = 0; + foreach ($this->elements as $module) { + if ($module->getVersionType() === ReleaseAppConstant::MODULE_TYPE_MINOR) { + $result++; + } + } + + return $result; + } + + /** + * @return int + */ + public function getPatchAmount(): int + { + $result = 0; + foreach ($this->elements as $module) { + if ($module->getVersionType() === ReleaseAppConstant::MODULE_TYPE_PATCH) { + $result++; + } + } + + return $result; + } +} diff --git a/src/ReleaseApp/Infrastructure/Shared/Dto/Collection/ReleaseGroupDtoCollection.php b/src/ReleaseApp/Infrastructure/Shared/Dto/Collection/ReleaseGroupDtoCollection.php new file mode 100644 index 0000000..0b8f58f --- /dev/null +++ b/src/ReleaseApp/Infrastructure/Shared/Dto/Collection/ReleaseGroupDtoCollection.php @@ -0,0 +1,98 @@ + + */ + protected array $elements = []; + + /** + * @param array<\SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\ReleaseGroupDto> $elements + */ + public function __construct(array $elements = []) + { + $this->elements = $elements; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\ReleaseGroupDto $element + * + * @return void + */ + public function add(ReleaseGroupDto $element): void + { + $this->elements[] = $element; + } + + /** + * @return array<\SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\ReleaseGroupDto> + */ + public function toArray(): array + { + return $this->elements; + } + + /** + * @return int + */ + public function count(): int + { + return count($this->elements); + } + + /** + * @return bool + */ + public function isEmpty(): bool + { + return !$this->elements; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\Collection\ReleaseGroupDtoCollection|self $collectionToMerge + * + * @return void + */ + public function addCollection(self $collectionToMerge): void + { + $this->elements = array_merge($this->elements, $collectionToMerge->toArray()); + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\Collection\ModuleDtoCollection + */ + public function getCommonModuleCollection(): ModuleDtoCollection + { + $resultCollection = new ModuleDtoCollection(); + foreach ($this->elements as $releaseGroup) { + $resultCollection->addCollection($releaseGroup->getModuleCollection()); + } + + return $resultCollection; + } + + /** + * @return self + */ + public function getSecurityFixes(): self + { + return new self( + array_filter( + $this->elements, + fn (ReleaseGroupDto $releaseGroup): bool => $releaseGroup->isSecurity() + ), + ); + } +} diff --git a/src/ReleaseApp/Infrastructure/Shared/Dto/ModuleDto.php b/src/ReleaseApp/Infrastructure/Shared/Dto/ModuleDto.php new file mode 100644 index 0000000..68ced2c --- /dev/null +++ b/src/ReleaseApp/Infrastructure/Shared/Dto/ModuleDto.php @@ -0,0 +1,64 @@ +name = $name; + $this->version = $version; + $this->versionType = $versionType; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return string + */ + public function getVersion(): string + { + return $this->version; + } + + /** + * @return string + */ + public function getVersionType(): string + { + return $this->versionType; + } +} diff --git a/src/ReleaseApp/Infrastructure/Shared/Dto/ReleaseAppResponse.php b/src/ReleaseApp/Infrastructure/Shared/Dto/ReleaseAppResponse.php new file mode 100644 index 0000000..5157373 --- /dev/null +++ b/src/ReleaseApp/Infrastructure/Shared/Dto/ReleaseAppResponse.php @@ -0,0 +1,36 @@ +releaseGroupCollection = $releaseGroupCollection; + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\Collection\ReleaseGroupDtoCollection + */ + public function getReleaseGroupCollection(): ReleaseGroupDtoCollection + { + return $this->releaseGroupCollection; + } +} diff --git a/src/ReleaseApp/Infrastructure/Shared/Dto/ReleaseGroupDto.php b/src/ReleaseApp/Infrastructure/Shared/Dto/ReleaseGroupDto.php new file mode 100644 index 0000000..7c8a737 --- /dev/null +++ b/src/ReleaseApp/Infrastructure/Shared/Dto/ReleaseGroupDto.php @@ -0,0 +1,193 @@ +name = $name; + $this->link = $link; + $this->moduleCollection = $moduleCollection; + $this->containsProjectChanges = $containsProjectChanges; + $this->hasConflict = $hasConflict; + $this->isSecurity = $isSecurity; + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\Collection\ModuleDtoCollection + */ + public function getModuleCollection(): ModuleDtoCollection + { + return $this->moduleCollection; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\Collection\ModuleDtoCollection $moduleCollection + * + * @return void + */ + public function setModuleCollection(ModuleDtoCollection $moduleCollection): void + { + $this->moduleCollection = $moduleCollection; + } + + /** + * @return bool + */ + public function hasProjectChanges(): bool + { + return $this->containsProjectChanges; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return string + */ + public function getLink(): string + { + return $this->link; + } + + /** + * @return bool + */ + public function hasConflict(): bool + { + return $this->hasConflict; + } + + /** + * @param bool $hasConflict + * + * @return void + */ + public function setHasConflict(bool $hasConflict): void + { + $this->hasConflict = $hasConflict; + } + + /** + * @return string|null + */ + public function getJiraIssue(): ?string + { + return $this->jiraIssue; + } + + /** + * @param string|null $jiraIssue + * + * @return void + */ + public function setJiraIssue(?string $jiraIssue): void + { + $this->jiraIssue = $jiraIssue; + } + + /** + * @return string|null + */ + public function getJiraIssueLink(): ?string + { + return $this->jiraIssueLink; + } + + /** + * @param string|null $jiraIssueLink + * + * @return void + */ + public function setJiraIssueLink(?string $jiraIssueLink): void + { + $this->jiraIssueLink = $jiraIssueLink; + } + + /** + * @param bool $isSecurity + * + * @return void + */ + public function setIsSecurity(bool $isSecurity): void + { + $this->isSecurity = $isSecurity; + } + + /** + * @return bool + */ + public function isSecurity(): bool + { + return $this->isSecurity; + } +} diff --git a/src/ReleaseApp/Infrastructure/Shared/Mapper/ReleaseGroupDtoCollectionMapper.php b/src/ReleaseApp/Infrastructure/Shared/Mapper/ReleaseGroupDtoCollectionMapper.php new file mode 100644 index 0000000..796f5d4 --- /dev/null +++ b/src/ReleaseApp/Infrastructure/Shared/Mapper/ReleaseGroupDtoCollectionMapper.php @@ -0,0 +1,146 @@ +configurationProvider = $configurationProvider; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\Collection\UpgradeInstructionsReleaseGroupCollection $releaseGroupCollection + * + * @return \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\Collection\ReleaseGroupDtoCollection + */ + public function mapReleaseGroupTransferCollection( + UpgradeInstructionsReleaseGroupCollection $releaseGroupCollection + ): ReleaseGroupDtoCollection { + $dataProviderReleaseGroupCollection = new ReleaseGroupDtoCollection(); + + foreach ($releaseGroupCollection->toArray() as $releaseGroup) { + $dataProviderReleaseGroupCollection->add($this->mapReleaseGroupDto($releaseGroup)); + } + + return $dataProviderReleaseGroupCollection; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeInstructionsReleaseGroup $releaseGroup + * + * @return \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\Collection\ReleaseGroupDtoCollection + */ + public function mapReleaseGroupDtoIntoCollection(UpgradeInstructionsReleaseGroup $releaseGroup): ReleaseGroupDtoCollection + { + $dataProviderReleaseGroupCollection = new ReleaseGroupDtoCollection(); + $dataProviderReleaseGroupCollection->add($this->mapReleaseGroupDto($releaseGroup)); + + return $dataProviderReleaseGroupCollection; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeInstructionsReleaseGroup $releaseGroup + * + * @return \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\ReleaseGroupDto + */ + protected function mapReleaseGroupDto(UpgradeInstructionsReleaseGroup $releaseGroup): ReleaseGroupDto + { + $dataProviderReleaseGroup = new ReleaseGroupDto( + $releaseGroup->getName(), + $this->buildModuleTransferCollection($releaseGroup), + $releaseGroup->hasProjectChanges(), + $this->getReleaseGroupLink($releaseGroup->getId()), + ); + $dataProviderReleaseGroup->setHasConflict( + $releaseGroup->getMeta() && $releaseGroup->getMeta()->getConflict()->count(), + ); + $dataProviderReleaseGroup->setJiraIssue($releaseGroup->getJiraIssue()); + $dataProviderReleaseGroup->setJiraIssueLink($releaseGroup->getJiraIssueLink()); + $dataProviderReleaseGroup->setIsSecurity($releaseGroup->isSecurity()); + + return $dataProviderReleaseGroup; + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeInstructionsReleaseGroup $releaseGroup + * + * @return \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Shared\Dto\Collection\ModuleDtoCollection + */ + protected function buildModuleTransferCollection(UpgradeInstructionsReleaseGroup $releaseGroup): ModuleDtoCollection + { + $releaseGroupModuleCollection = $releaseGroup->getModuleCollection(); + if ($releaseGroup->getMeta()) { + $releaseGroupModuleCollection = $this->applyMeta($releaseGroupModuleCollection, $releaseGroup->getMeta()); + } + + $dataProviderModuleCollection = new ModuleDtoCollection(); + foreach ($releaseGroupModuleCollection->toArray() as $module) { + $dataProviderModule = new ModuleDto($module->getName(), $module->getVersion(), $module->getType()); + $dataProviderModuleCollection->add($dataProviderModule); + } + + return $dataProviderModuleCollection; + } + + /** + * @param int $id + * + * @return string + */ + protected function getReleaseGroupLink(int $id): string + { + return sprintf(ReleaseAppConstant::RELEASE_GROUP_LINK_PATTERN, $this->configurationProvider->getReleaseAppUrl(), $id); + } + + /** + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\Collection\UpgradeInstructionModuleCollection $moduleCollection + * @param \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\UpgradeInstructionMeta $meta + * + * @return \SprykerSdk\Evaluator\ReleaseApp\Domain\Entities\Collection\UpgradeInstructionModuleCollection + */ + protected function applyMeta( + UpgradeInstructionModuleCollection $moduleCollection, + UpgradeInstructionMeta $meta + ): UpgradeInstructionModuleCollection { + foreach ($meta->getInclude()->toArray() as $moduleInclude) { + $module = $moduleCollection->getFirstByName($moduleInclude->getName()); + if ($module) { + $module->setVersion($moduleInclude->getVersion()); + + continue; + } + + $moduleInclude->setType(ReleaseAppConstant::MODULE_TYPE_PATCH); + $moduleCollection->add($moduleInclude); + } + foreach ($meta->getExclude()->toArray() as $moduleExclude) { + $moduleCollection->deleteByName($moduleExclude->getName()); + } + + return $moduleCollection; + } +} diff --git a/tests/Unit/Checker/SecurityChecker/SecurityCheckerTest.php b/tests/Unit/Checker/OpenSourceVulnerabilitiesChecker/OpenSourceVulnerabilitiesCheckerTest.php similarity index 78% rename from tests/Unit/Checker/SecurityChecker/SecurityCheckerTest.php rename to tests/Unit/Checker/OpenSourceVulnerabilitiesChecker/OpenSourceVulnerabilitiesCheckerTest.php index 74baf1d..6511247 100644 --- a/tests/Unit/Checker/SecurityChecker/SecurityCheckerTest.php +++ b/tests/Unit/Checker/OpenSourceVulnerabilitiesChecker/OpenSourceVulnerabilitiesCheckerTest.php @@ -7,10 +7,10 @@ declare(strict_types=1); -namespace SprykerSdkTest\Evaluator\Unit\Checker\SecurityChecker; +namespace SprykerSdkTest\Evaluator\Unit\Checker\OpenSourceVulnerabilitiesChecker; use PHPUnit\Framework\TestCase; -use SprykerSdk\Evaluator\Checker\SecurityChecker\SecurityChecker; +use SprykerSdk\Evaluator\Checker\OpenSourceVulnerabilitiesChecker\OpenSourceVulnerabilitiesChecker; use SprykerSdk\Evaluator\Dto\CheckerInputDataDto; use SprykerSdk\Evaluator\Resolver\PathResolverInterface; use Symfony\Component\Console\Application; @@ -25,7 +25,7 @@ * @group SecurityChecker * @group SecurityCheckerTest */ -class SecurityCheckerTest extends TestCase +class OpenSourceVulnerabilitiesCheckerTest extends TestCase { /** * @return void @@ -43,12 +43,12 @@ public function testReturnInternalError(): void ]), new BufferedOutput(), ); - $securityChecker = new SecurityChecker($applicationMock, $this->createPathResolverMock()); + $securityChecker = new OpenSourceVulnerabilitiesChecker($applicationMock, $this->createPathResolverMock()); $result = $securityChecker->check(new CheckerInputDataDto('/path')); $this->assertCount(1, $result->getViolations()); $this->assertSame('Internal error. Original error: ', $result->getViolations()[0]->getMessage()); - $this->assertSame(SecurityChecker::NAME, $result->getViolations()[0]->getTarget()); + $this->assertSame(OpenSourceVulnerabilitiesChecker::NAME, $result->getViolations()[0]->getTarget()); } /** diff --git a/tests/Unit/Checker/SprykerSecurityChecker/SprykerSecurityCheckerTest.php b/tests/Unit/Checker/SprykerSecurityChecker/SprykerSecurityCheckerTest.php new file mode 100644 index 0000000..c7c1c3f --- /dev/null +++ b/tests/Unit/Checker/SprykerSecurityChecker/SprykerSecurityCheckerTest.php @@ -0,0 +1,186 @@ +createPathResolverMock(static::INVALID_PROJECT_PATH)), + $this->createReleaseAppServiceMock(), + 'doc url', + ); + + //Act + $response = $checker->check(new CheckerInputDataDto('')); + + //Assert + $this->assertEquals( + new CheckerResponseDto( + [ + new ViolationDto( + 'Security update available for the module spryker/availability-gui, actual version 6.5.3', + 'spryker/availability-gui:6.6.0', + ), + ], + 'doc url', + ), + $response, + ); + } + + /** + * @return void + */ + public function testCheckNoViolation(): void + { + //Arrange + $checker = new SprykerSecurityChecker( + new ComposerReader($this->createPathResolverMock(static::VALID_PROJECT_PATH)), + $this->createReleaseAppServiceMock(), + 'doc url', + ); + + //Act + $response = $checker->check(new CheckerInputDataDto('')); + + //Assert + $this->assertEquals(new CheckerResponseDto([], 'doc url'), $response); + } + + /** + * @return void + */ + public function testCheckServiceUnavailable(): void + { + //Arrange + $checker = new SprykerSecurityChecker( + new ComposerReader($this->createPathResolverMock(static::INVALID_PROJECT_PATH)), + $this->createReleaseAppServiceMockThrowException(), + 'doc url', + ); + + //Act + $response = $checker->check(new CheckerInputDataDto('tests/Unit/Acceptance/InvalidProject')); + + //Assert + $this->assertEquals( + new CheckerResponseDto( + [ + new ViolationDto( + 'Service is not available, please try latter. Error: 0 Something went wrong', + 'SPRYKER_SECURITY_CHECKER', + ), + ], + 'doc url', + ), + $response, + ); + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Service\ReleaseAppServiceInterface + */ + protected function createReleaseAppServiceMock(): ReleaseAppServiceInterface + { + $executorMock = $this->createMock(ReleaseAppService::class); + $executorMock->expects($this->any()) + ->method('getNewSecurityReleaseGroups') + ->willReturn( + new ReleaseAppResponse( + new ReleaseGroupDtoCollection( + [ + new ReleaseGroupDto( + 'RG1', + new ModuleDtoCollection([ + new ModuleDto('spryker/availability-gui', '6.6.0', 'minor'), + new ModuleDto('spryker/store-gui', '4.2.1', 'path'), + new ModuleDto('cart-gui', '1.2.1', 'path'), + ]), + false, + 'RG1 link', + false, + true, + ), + ], + ), + ), + ); + + return $executorMock; + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Service\ReleaseAppServiceInterface + */ + protected function createReleaseAppServiceMockThrowException(): ReleaseAppServiceInterface + { + $executorMock = $this->createMock(ReleaseAppService::class); + $executorMock->expects($this->any()) + ->method('getNewSecurityReleaseGroups') + ->willThrowException( + new ReleaseAppException('Something went wrong'), + ); + + return $executorMock; + } + + /** + * @param string $path + * + * @return \SprykerSdk\Evaluator\Resolver\PathResolverInterface + */ + protected function createPathResolverMock(string $path): PathResolverInterface + { + $pathResolver = $this->createMock(PathResolverInterface::class); + $pathResolver->method('resolvePath')->willReturn($path); + + return $pathResolver; + } +} diff --git a/tests/Unit/Helper/SemanticVersionHelperTest.php b/tests/Unit/Helper/SemanticVersionHelperTest.php new file mode 100644 index 0000000..2e1be60 --- /dev/null +++ b/tests/Unit/Helper/SemanticVersionHelperTest.php @@ -0,0 +1,64 @@ +assertSame($expectedResult, SemanticVersionHelper::getMajorVersion((string)$value)); + } + + /** + * @return array + */ + public function getMajorVersionProvider(): array + { + $validSemanticVersions = [ + ['1.0.0', 1], + ['0.1.0', 0], + ['0.0.1', 0], + ['10.20.30', 10], + ['2.3.4-beta', 2], + ['5.6.7-alpha.123', 5], + ['1.2.3-rc.456+build789', 1], + ['3.2.1+build123', 3], + ['4.5.6-beta.789+build999', 4], + ['1.2', 1], + ['1.2.a', 1], + ['1.2.3.4', 1], + ['1.2.3-rc!', 1], + ['1.2.3-beta@456', 1], + ['1.2.3+build_789', 1], + ['1.2.3-rc.456+build_789', 1], + ['1.2.3-beta+build!999', 1], + ['1.2.3-beta+build.999-456', 1], + ]; + + $invalidSemanticVersions = [ + ['', null], + ['foo-bar-baz', null], + ['-55.28888.222', null], + ]; + + return [...$validSemanticVersions, ...$invalidSemanticVersions]; + } +} diff --git a/tests/Unit/Helper/TextCaseHelperTest.php b/tests/Unit/Helper/TextCaseHelperTest.php new file mode 100644 index 0000000..2003f8e --- /dev/null +++ b/tests/Unit/Helper/TextCaseHelperTest.php @@ -0,0 +1,96 @@ +assertSame($expectedResult, TextCaseHelper::camelCaseToDash($value, $separateAbbreviation)); + } + + /** + * @return array + */ + public function camelCaseToDashDataProvider(): array + { + return [ + ['foo-bar', 'FooBar', true], + ['foo-bar-baz', 'FooBarBaz', true], + ['foo-bar-baz', 'FooBarBaz', false], + ['foo-bar', 'FooBar', false], + ['foo-bar', 'FOOBar', true], + ['foobar', 'FOOBar', false], + ]; + } + + /** + * @dataProvider dashToCamelCaseDataProvider + * + * @param string $expectedResult + * @param string $value + * @param bool $upperCaseFirst + * + * @return void + */ + public function testDashToCamelCase(string $expectedResult, string $value, bool $upperCaseFirst): void + { + $this->assertSame($expectedResult, TextCaseHelper::dashToCamelCase($value, $upperCaseFirst)); + } + + /** + * @return array + */ + public function dashToCamelCaseDataProvider(): array + { + return [ + ['FooBar', 'foo-bar', true], + ['FooBarBaz', 'foo-bar-baz', true], + ['fooBarBaz', 'foo-bar-baz', false], + ['fooBar', 'foo-bar', false], + ]; + } + + /** + * @dataProvider packageCamelCaseToDashDataProvider + * + * @param string $expectedResult + * @param string $originName + * + * @return void + */ + public function testPackageCamelCaseToDash(string $expectedResult, string $originName): void + { + $this->assertSame($expectedResult, TextCaseHelper::packageCamelCaseToDash($originName)); + } + + /** + * @return array + */ + public function packageCamelCaseToDashDataProvider(): array + { + return [ + ['spryker/symfony-mailer', 'Spryker.SymfonyMailer'], + ['my-company/my-package', 'MyCompany.MyPackage'], + ]; + } +} diff --git a/tests/Unit/ReleaseApp/Infrastructure/Service/Client/HttpRequestExecutorTest.php b/tests/Unit/ReleaseApp/Infrastructure/Service/Client/HttpRequestExecutorTest.php new file mode 100644 index 0000000..071e234 --- /dev/null +++ b/tests/Unit/ReleaseApp/Infrastructure/Service/Client/HttpRequestExecutorTest.php @@ -0,0 +1,85 @@ +createMock(ResponseInterface::class); + $mockRequest = $this->createMock(RequestInterface::class); + $mockGuzzleClient = $this->createMock(Client::class); + + $mockGuzzleClient + ->expects($this->once()) + ->method('send') + ->with($mockRequest) + ->willReturn($mockGuzzleResponse); + + $config = $this->createMock(ConfigurationProvider::class); + + $config + ->expects($this->once()) + ->method('getHttpRetrieveAttemptsCount') + ->willReturn(3); + + $httpRequestExecutor = new HttpRequestExecutor($config, $mockGuzzleClient); + + // Act + $result = $httpRequestExecutor->execute($mockRequest); + + // Assert + $this->assertInstanceOf(ResponseInterface::class, $result); + } + + /** + * @return void + */ + public function testExecuteWithServerException(): void + { + // Arrange + $mockRequest = $this->createMock(RequestInterface::class); + $mockGuzzleClient = $this->createMock(Client::class); + + $serverException = $this->createMock(ServerException::class); + + $mockGuzzleClient + ->expects($this->exactly(3)) + ->method('send') + ->with($mockRequest) + ->willThrowException($serverException); + + $config = $this->createMock(ConfigurationProvider::class); + $config + ->expects($this->exactly(3)) + ->method('getHttpRetrieveRetryDelay') + ->willReturn(1); + + $config + ->expects($this->exactly(3)) + ->method('getHttpRetrieveAttemptsCount') + ->willReturn(3); + + $httpRequestExecutor = new HttpRequestExecutor($config, $mockGuzzleClient); + + // Assert + $this->expectException(ServerException::class); + + // Act + $httpRequestExecutor->execute($mockRequest); + } +} diff --git a/tests/Unit/ReleaseApp/Infrastructure/Service/ReleaseAppServiceTest.php b/tests/Unit/ReleaseApp/Infrastructure/Service/ReleaseAppServiceTest.php new file mode 100644 index 0000000..e572aef --- /dev/null +++ b/tests/Unit/ReleaseApp/Infrastructure/Service/ReleaseAppServiceTest.php @@ -0,0 +1,130 @@ +getContainer(); + $container->set(HttpRequestExecutor::class, $this->createRequestExecutorMock()); + $request = new UpgradeAnalysisRequest('project-name', [], []); + + // Act + $releaseGroups = $container->get(ReleaseAppService::class)->getNewSecurityReleaseGroups($request); + + // Assert + $this->assertEquals( + new ReleaseAppResponse( + new ReleaseGroupDtoCollection([ + new ReleaseGroupDto( + 'FRW-229 Replace Swiftmailer dependency with SymfonyMailer', + new ModuleDtoCollection([ + new ModuleDto('spryker/mail-extension', '1.0.0', 'major'), + new ModuleDto('spryker/symfony-mailer', '1.0.2', 'major'), + ]), + true, + 'https://api.release.spryker.com/release-group/4395', + true, + true, + ), + ]), + ), + $releaseGroups, + ); + } + + /** + * @return void + */ + public function testGetNewReleaseGroupsError(): void + { + // Assert + $this->expectException(ServerException::class); + + // Arrange + $container = static::bootKernel()->getContainer(); + $container->set(HttpRequestExecutor::class, $this->createRequestExecutorMockWithError()); + $request = new UpgradeAnalysisRequest('project-name', [], []); + + // Act + $container->get(ReleaseAppService::class)->getNewSecurityReleaseGroups($request); + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Client\HttpRequestExecutor + */ + protected function createRequestExecutorMock(): HttpRequestExecutor + { + $callback = function (Request $request) { + return $this->createHttpResponse($request->getUri()->getPath()); + }; + + $executorMock = $this->createMock(HttpRequestExecutor::class); + $executorMock->expects($this->any()) + ->method('execute') + ->will($this->returnCallback($callback)); + + return $executorMock; + } + + /** + * @return \SprykerSdk\Evaluator\ReleaseApp\Infrastructure\Client\HttpRequestExecutor + */ + protected function createRequestExecutorMockWithError(): HttpRequestExecutor + { + $executorMock = $this->createMock(HttpRequestExecutor::class); + $executorMock->expects($this->any()) + ->method('execute') + ->willThrowException( + new ServerException( + '500 Service is unavailable', + new Request('GET', 'https://api.release.spryker.com'), + new Response(), + ), + ); + + return $executorMock; + } + + /** + * @param string $endpoint + * + * @return \GuzzleHttp\Psr7\Response + */ + protected function createHttpResponse(string $endpoint): Response + { + $contents = file_get_contents(static::API_RESPONSE_DIR . $endpoint); + + return new Response(200, [], $contents); + } +} diff --git a/tests/Unit/_data/InvalidProject/composer.json b/tests/Unit/_data/InvalidProject/composer.json new file mode 100644 index 0000000..25b29e3 --- /dev/null +++ b/tests/Unit/_data/InvalidProject/composer.json @@ -0,0 +1,24 @@ +{ + "name": "spryker-shop/shop-test", + "description": "Spryker Shop", + "license": "proprietary", + "require": { + "php": ">=7.4" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "preferred-install": "dist", + "platform": { + "php": "7.4.20" + }, + "use-include-path": true, + "sort-packages": true, + "github-protocols": [ + "https" + ], + "process-timeout": 900, + "chromium-revision": 814168, + "allow-plugins": {} + } +} diff --git a/tests/Unit/_data/InvalidProject/composer.lock b/tests/Unit/_data/InvalidProject/composer.lock new file mode 100644 index 0000000..501ffac --- /dev/null +++ b/tests/Unit/_data/InvalidProject/composer.lock @@ -0,0 +1,36 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "2a41eec25ae50788edae6d5a64aa6540", + "packages": [ + { + "name": "spryker/availability-gui", + "version": "6.5.3" + }, + { + "name": "spryker/store-gui", + "version": "3.2.0" + }, + { + "name": "spryker/cart-gui", + "version": "2.2.0" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=7.4", + "ext-ctype": "*", + "ext-iconv": "*", + "ext-json": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/tests/Unit/_data/ReleaseApp/Api/Response/upgrade-analysis.json b/tests/Unit/_data/ReleaseApp/Api/Response/upgrade-analysis.json new file mode 100644 index 0000000..8443946 --- /dev/null +++ b/tests/Unit/_data/ReleaseApp/Api/Response/upgrade-analysis.json @@ -0,0 +1,66 @@ +{ + "result": { + "modules": [ + { + "id": 1, + "name": "Acl", + "version": "3.15.0", + "type": 0, + "deprecated": false, + "description": "Acl is part of the Store Administration functionality. With this module you will define roles, groups, privileges and resources for managing access privileges to Zed Administration Interface.", + "doc_url": "https://academy.spryker.com/developing_with_spryker/module_guide/user_rights_management.html", + "module_versions": [ + { + "id": 1515, + "name": "3.15.0", + "description": "Included commits: https://github.com/spryker/acl/compare/3.14.0...3.15.0\n\n### Improvements\r\n\r\n- Added `UserFacade::getUserCollection()` to dependencies.\r\n- Removed `UserFacade::getUserByUsername()` from dependencies.\r\n- Introduced `UserCollection` transfer.\r\n- Introduced `UserConditions` transfer.\r\n- Introduced `UserCriteria` transfer.\r\n- Increased `User` module version dependency.\r\n- Increased `Transfer` module version dependency.", + "dependencies": {}, + "guide_url": null, + "is_security": true + }, + { + "id": 1521, + "name": "3.14.0", + "description": "Included commits: https://github.com/spryker/acl/compare/3.13.0...3.14.0\n\n### Improvements\r\n\r\n- Adjusted `Spryker\\Zed\\Acl\\Communication\\Plugin\\EventDispatcher\\AccessControlEventDispatcherPlugin` for compatibility with Symfony 6.", + "dependencies": {}, + "guide_url": null, + "is_security": true + } + ], + "tag_list": "", + "package": "spryker/acl", + "type_string": "Core Splitted", + "functional_type_string": null, + "is_functional_dependency": null, + "short_name": "Acl", + "full_name": "Spryker.Acl" + }, + { + "id": 5101, + "name": "AclDataImport", + "version": "0.1.0", + "type": 0, + "deprecated": false, + "description": null, + "doc_url": null, + "module_versions": [ + { + "id": 20130, + "name": "0.1.0", + "description": "Included commits: https://github.com/spryker/acl-data-import/tree/0.1.0\n\n### Initial Release\r\n\r\n- Introduced `AclGroupDataImportPlugin` plugin to import Acl Group data to `spy_acl_group` table.\r\n- Introduced `AclGroupRoleDataImportPlugin ` plugin to\r\nto import Acl Group Role data to `spy_acl_groups_has_roles` table.\r\n- Introduced `AclRoleDataImportPlugin ` plugin to import Acl Role data to `spy_acl_role` table.\r\n- Introduced `AclDataImportFacade::importAclRoles()`.\r\n- Introduced `AclDataImportFacade::importAclGroups()`.\r\n- Introduced `AclDataImportFacade::importAclGroupRole()`.\r\n- Introduced `AclDataImportConfig::getAclRoleDataImporterConfiguration()`.\r\n- Introduced `AclDataImportConfig::getAclGroupDataImporterConfiguration()`.\r\n- Introduced `AclDataImportConfig::getAclGroupRoleDataImporterConfiguration()`.\r\n- Introduced `Role` transfer.\r\n- Introduced `Role.reference` transfer property.\r\n- Introduced `RoleCriteria ` transfer.\r\n- Introduced `RoleCriteria.reference` transfer property.\r\n- Introduced `GroupCriteria` transfer.\r\n- Introduced `GroupCriteria.names` transfer property.", + "dependencies": {}, + "guide_url": null, + "is_security": true + } + ], + "tag_list": "", + "package": "spryker/acl-data-import", + "type_string": "Core Splitted", + "functional_type_string": null, + "is_functional_dependency": null, + "short_name": "AclDataImport", + "full_name": "Spryker.AclDataImport" + } + ] + } +} diff --git a/tests/Unit/_data/ReleaseApp/Api/Response/upgrade-instructions.json b/tests/Unit/_data/ReleaseApp/Api/Response/upgrade-instructions.json new file mode 100644 index 0000000..f5fcc73 --- /dev/null +++ b/tests/Unit/_data/ReleaseApp/Api/Response/upgrade-instructions.json @@ -0,0 +1,39 @@ +{ + "result": { + "release_group": { + "id": 4395, + "name": "FRW-229 Replace Swiftmailer dependency with SymfonyMailer", + "released": "2022-11-13T18:12:50+00:00", + "project_changes": true, + "is_security": true, + "modules": { + "spryker/mail-extension": { + "version": "1.0.0", + "type": "major", + "files": 16 + }, + "spryker/symfony-mailer": { + "version": "1.0.0", + "type": "major", + "files": 43 + }, + "spryker/availability-notification": { + "version": "1.2.0", + "type": "minor", + "files": 9 + } + }, + "meta": { + "include": { + "Spryker.SymfonyMailer": "1.0.2" + }, + "exclude": { + "Spryker.AvailabilityNotification": "1.2.0" + }, + "conflict": { + "Spryker.MailExtension": "1.0.0" + } + } + } + } +} diff --git a/tests/Unit/_data/ValidProject/composer.json b/tests/Unit/_data/ValidProject/composer.json new file mode 100644 index 0000000..25b29e3 --- /dev/null +++ b/tests/Unit/_data/ValidProject/composer.json @@ -0,0 +1,24 @@ +{ + "name": "spryker-shop/shop-test", + "description": "Spryker Shop", + "license": "proprietary", + "require": { + "php": ">=7.4" + }, + "minimum-stability": "dev", + "prefer-stable": true, + "config": { + "preferred-install": "dist", + "platform": { + "php": "7.4.20" + }, + "use-include-path": true, + "sort-packages": true, + "github-protocols": [ + "https" + ], + "process-timeout": 900, + "chromium-revision": 814168, + "allow-plugins": {} + } +} diff --git a/tests/Unit/_data/ValidProject/composer.lock b/tests/Unit/_data/ValidProject/composer.lock new file mode 100644 index 0000000..9c919cd --- /dev/null +++ b/tests/Unit/_data/ValidProject/composer.lock @@ -0,0 +1,36 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "2a41eec25ae50788edae6d5a64aa6540", + "packages": [ + { + "name": "spryker/availability-gui", + "version": "6.6.0" + }, + { + "name": "spryker/store-gui", + "version": "3.2.0" + }, + { + "name": "spryker/cart-gui", + "version": "2.2.0" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=7.4", + "ext-ctype": "*", + "ext-iconv": "*", + "ext-json": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 5160486..26bc176 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -2,6 +2,8 @@ use Symfony\Component\Dotenv\Dotenv; +define('ROOT_TESTS', __DIR__); + require dirname(__DIR__) . '/vendor/autoload.php'; if (file_exists(dirname(__DIR__) . '/config/bootstrap.php')) {