diff --git a/.env.test b/.env.test index d7913e79..5c8cda8c 100644 --- a/.env.test +++ b/.env.test @@ -4,3 +4,7 @@ APP_SECRET='$ecretf0rt3st' SYMFONY_DEPRECATIONS_HELPER=999999 PANTHER_APP_ENV=panther PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots + +DATABASE_URL="sqlite:///%kernel.project_dir%/var/test.db" +MAILER_DSN=null://null +REDIS_URL=redis://localhost/14 diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml new file mode 100644 index 00000000..0515421d --- /dev/null +++ b/.github/workflows/run_tests.yml @@ -0,0 +1,43 @@ +name: "Run Tests" + +on: + push: + paths: + - 'src/**' + - 'tests/**' + branches: + - master + pull_request: + +env: + APP_ENV: test + +jobs: + tests: + name: "Tests" + runs-on: ubuntu-20.04 + + steps: + - name: "Checkout" + uses: "actions/checkout@v3" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + extensions: "intl, zip, redis" + php-version: "8.1" + tools: composer + + - name: "Install dependencies" + uses: "ramsey/composer-install@v2" + with: + composer-options: "--ansi --no-interaction" + + - name: Start Redis + uses: supercharge/redis-github-action@1.2.0 + with: + redis-version: 6 + + - name: "Run tests" + run: "composer tests" diff --git a/composer.json b/composer.json index 649f5d76..e026b536 100644 --- a/composer.json +++ b/composer.json @@ -132,7 +132,8 @@ ], "post-update-cmd": [ "@auto-scripts" - ] + ], + "tests": "tests/phpunit" }, "conflict": { "symfony/symfony": "*" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index fae5a526..3da41023 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -11,11 +11,12 @@ + - - tests + + tests/Functional diff --git a/src/Command/UserManagerCommand.php b/src/Command/UserManagerCommand.php index 19c645bb..acb99f55 100644 --- a/src/Command/UserManagerCommand.php +++ b/src/Command/UserManagerCommand.php @@ -131,8 +131,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($input->hasOption('password')) { $io->writeln("Creating a user {$username}..."); - $password = $io->askHidden('Enter password'); - $input->setOption('password', $password); + + if (empty($input->getOption('password'))) { + $password = $io->askHidden('Enter password'); + $input->setOption('password', $password); + } } $password = $input->getOption('password') ?: hash('sha512', random_bytes(50)); diff --git a/src/Controller/WebController.php b/src/Controller/WebController.php index 3caab1ba..7b38d74c 100644 --- a/src/Controller/WebController.php +++ b/src/Controller/WebController.php @@ -196,7 +196,7 @@ public function statsAction(\Redis $redis): Response */ protected function getFilteredOrderedBys(Request $req) { - $orderBys = $req->query->get('orderBys', []); + $orderBys = $req->query->get('orderBys') ?: []; if (!$orderBys) { $orderBys = $req->query->get('search_query'); $orderBys = $orderBys['orderBys'] ?? []; diff --git a/src/Security/Api/ApiUsernamePasswordToken.php b/src/Security/Api/ApiUsernamePasswordToken.php index 5b96834f..cba69191 100644 --- a/src/Security/Api/ApiUsernamePasswordToken.php +++ b/src/Security/Api/ApiUsernamePasswordToken.php @@ -27,7 +27,7 @@ public function __construct($user, $providerKey, array $roles = []) $this->setUser($user); $this->providerKey = $providerKey; - parent::setAuthenticated(count($roles) > 0); + parent::setAuthenticated(count($roles) > 0, false); } /** diff --git a/tests/Controller/ProviderControllerTest.php b/tests/Controller/ProviderControllerTest.php deleted file mode 100644 index 7816fd81..00000000 --- a/tests/Controller/ProviderControllerTest.php +++ /dev/null @@ -1,16 +0,0 @@ -request('GET', '/about'); - static::assertResponseStatusCodeSame(302); - } -} diff --git a/tests/Functional/Controller/BaseAclControllerTest.php b/tests/Functional/Controller/BaseAclControllerTest.php new file mode 100644 index 00000000..75750ae0 --- /dev/null +++ b/tests/Functional/Controller/BaseAclControllerTest.php @@ -0,0 +1,93 @@ +getUser($user); + $client->loginUser($user); + + $client->request('GET', $url); + static::assertResponseStatusCodeSame($code); + } + + #[DataProvider('adminUrlProvider')] + public function testAdminAccess(string $url): void + { + $client = static::createClient(); + + $admin = $this->getUser('admin'); + + $client->loginUser($admin); + + $client->request('GET', $url); + static::assertResponseIsSuccessful(); + } + + public function testAnonymousAccess(): void + { + $client = static::createClient(); + $client->request('GET', '/'); + static::assertResponseRedirects(); + } + + public static function aclUserUrlProvider(): array + { + return [ + ['dev', '/', 200], + ['dev', '/', 200], + ['dev', '/packages/okvpn/cron-bundle', 200], + ['dev', '/packages/okvpn/satis-api', 200], + ['dev', '/packages/okvpn/cron-bundle/stats', 200], + ['dev', '/packages/submit', 200], + ['dev', '/groups', 403], + + ['user1', '/', 200], + ['user1', '/profile', 200], + ['user1', '/packages/okvpn/cron-bundle', 200], + ['user1', '/packages/okvpn/satis-api', 404], + ['user1', '/about', 403], + + ['user2', '/packages/okvpn/satis-api', 200], + ]; + } + + public static function adminUrlProvider(): array + { + return [ + ['/'], + ['/packages/okvpn/cron-bundle'], + ['/packages/okvpn/cron-bundle/stats'], + ['/packages/submit'], + ['/users/'], + ['/users/dev'], + ['/users/dev/update'], + ['/groups'], + ['/groups/1/update'], + ['/users/sshkey'], + ['/profile'], + ['/statistics'], + ['/explore'], + ['/about'], + ['/users/admin/packages'], + ['/users/admin/favorites'], + ['/apidoc'], + ['/proxies'], + ['/feeds/'], + ['/feeds/releases.rss'], + ]; + } +} diff --git a/tests/Functional/Controller/ProviderControllerTest.php b/tests/Functional/Controller/ProviderControllerTest.php new file mode 100644 index 00000000..1c7eddf2 --- /dev/null +++ b/tests/Functional/Controller/ProviderControllerTest.php @@ -0,0 +1,111 @@ +request('GET', '/packages.json'); + static::assertResponseStatusCodeSame(401); + + $this->basicLogin('admin', $client); + $client->request('GET', '/packages.json'); + static::assertResponseStatusCodeSame(200);; + } + + #[DataProvider('aclUserProvider')] + public function testAvailablePackages(string $user, int $count): void + { + $client = static::createClient(); + + $this->basicLogin($user, $client); + $client->request('GET', '/packages.json'); + + $content = $this->getJsonResponse($client); + static::assertCount($count, $content['available-packages']); + } + + public function testV2Packages(): void + { + $client = static::createClient(); + + $this->basicLogin('admin', $client); + + $client->request('GET', '/p2/okvpn/cron-bundle.json'); + $content = $this->getJsonResponse($client); + static::assertNotEmpty($content['packages']['okvpn/cron-bundle']); + + $client->request('GET', '/p2/okvpn/cron-bundle~dev.json'); + $content = $this->getJsonResponse($client); + static::assertNotEmpty($content['packages']['okvpn/cron-bundle']); + + $client->request('GET', '/p2/okvpn/cron-bundle222.json'); + static::assertResponseStatusCodeSame(404); + } + + public function testV2CustomerPackages(): void + { + $client = static::createClient(); + + $this->basicLogin('user1', $client); + + $client->request('GET', '/p2/okvpn/cron-bundle.json'); + $content = $this->getJsonResponse($client); + static::assertNotEmpty($content['packages']['okvpn/cron-bundle']); + + $client->request('GET', '/p2/okvpn/cron-bundle~dev.json'); + $content = $this->getJsonResponse($client); + static::assertNotEmpty($content['packages']['okvpn/cron-bundle']); + + $client->request('GET', '/p2/okvpn/satis-api.json'); + static::assertResponseStatusCodeSame(404); + } + + public function testProviderIncludesV1(): void + { + $client = static::createClient(); + + $this->basicLogin('admin', $client); + + $server = ['HTTP_USER_AGENT' => 'Composer/1.10.2 (Windows NT; 10.0; PHP 8.1.0)']; + $client->request('GET', '/packages.json', server: $server); + $content = $this->getJsonResponse($client); + static::assertNotEmpty($content['provider-includes']); + + $hash = reset($content['provider-includes'])['sha256']; + $client->request('GET', "/p/providers$$hash.json", server: $server); + static::assertEquals($hash, hash('sha256', $client->getResponse()->getContent())); + + $content = $this->getJsonResponse($client); + static::assertNotEmpty($content['providers']); + + $hash = $content['providers']['okvpn/cron-bundle']['sha256']; + $client->request('GET', "/p/okvpn/cron-bundle$$hash.json", server: $server); + static::assertEquals($hash, hash('sha256', $client->getResponse()->getContent())); + + $content = $this->getJsonResponse($client); + static::assertNotEmpty($content['packages']); + static::assertNotEmpty($content['packages']['okvpn/cron-bundle']); + $versions = array_column($content['packages']['okvpn/cron-bundle'], 'version_normalized'); + static::assertTrue(in_array('9999999-dev', $versions)); + } + + public static function aclUserProvider(): array + { + return [ + ['admin', 3], + ['dev', 3], + ['user1', 1], + ['user2', 3], + ]; + } +} diff --git a/tests/Phpunit/ErrorHandler.php b/tests/Phpunit/ErrorHandler.php new file mode 100644 index 00000000..4f9a2e98 --- /dev/null +++ b/tests/Phpunit/ErrorHandler.php @@ -0,0 +1,182 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Util; + +use const E_DEPRECATED; +use const E_NOTICE; +use const E_STRICT; +use const E_USER_DEPRECATED; +use const E_USER_NOTICE; +use const E_USER_WARNING; +use const E_WARNING; +use function debug_backtrace; +use function error_reporting; +use function in_array; +use function restore_error_handler; +use function set_error_handler; +use PHPUnit\Event; +use PHPUnit\Framework\TestCase; + +/** + * @internal This class is not covered by the backward compatibility promise for PHPUnit + */ +final class ErrorHandler +{ + private static ?self $instance = null; + private bool $enabled = false; + + public static function instance(): self + { + return self::$instance ?? self::$instance = new self; + } + + /** + * @throws Exception + */ + public function __invoke(int $errorNumber, string $errorString, string $errorFile, int $errorLine): bool + { + $suppressed = !($errorNumber & error_reporting()); + + if ($suppressed && + in_array($errorNumber, [E_DEPRECATED, E_NOTICE, E_STRICT, E_WARNING, E_USER_DEPRECATED], true)) { + return false; + } + + switch ($errorNumber) { + case E_NOTICE: + case E_STRICT: + Event\Facade::emitter()->testTriggeredPhpNotice( + $this->testValueObjectForEvents(), + $errorString, + $errorFile, + $errorLine + ); + + return true; + + case E_USER_NOTICE: + Event\Facade::emitter()->testTriggeredNotice( + $this->testValueObjectForEvents(), + $errorString, + $errorFile, + $errorLine + ); + + break; + + case E_WARNING: + Event\Facade::emitter()->testTriggeredPhpWarning( + $this->testValueObjectForEvents(), + $errorString, + $errorFile, + $errorLine + ); + + break; + + case E_USER_WARNING: + Event\Facade::emitter()->testTriggeredWarning( + $this->testValueObjectForEvents(), + $errorString, + $errorFile, + $errorLine + ); + + break; + + case E_DEPRECATED: + Event\Facade::emitter()->testTriggeredPhpDeprecation( + $this->testValueObjectForEvents(), + $errorString, + $errorFile, + $errorLine + ); + + break; + + case E_USER_DEPRECATED: + Event\Facade::emitter()->testTriggeredDeprecation( + $this->testValueObjectForEvents(), + $errorString, + $errorFile, + $errorLine + ); + + break; + + case E_USER_ERROR: + Event\Facade::emitter()->testTriggeredError( + $this->testValueObjectForEvents(), + $errorString, + $errorFile, + $errorLine + ); + + break; + + default: + // @codeCoverageIgnoreStart + return false; + // @codeCoverageIgnoreEnd + } + + return true; + } + + public function enable(): void + { + if ($this->enabled) { + // @codeCoverageIgnoreStart + return; + // @codeCoverageIgnoreEnd + } + + $oldErrorHandler = set_error_handler($this); + + if ($oldErrorHandler !== null) { + // @codeCoverageIgnoreStart + restore_error_handler(); + + return; + // @codeCoverageIgnoreEnd + } + + $this->enabled = true; + } + + public function disable(): void + { + if (!$this->enabled) { + // @codeCoverageIgnoreStart + return; + // @codeCoverageIgnoreEnd + } + + restore_error_handler(); + + $this->enabled = false; + } + + /** + * @throws NoTestCaseObjectOnCallStackException + */ + private function testValueObjectForEvents(): Event\Code\Test + { + foreach (debug_backtrace() as $frame) { + if (isset($frame['object']) && $frame['object'] instanceof TestCase) { + return $frame['object']->valueObjectForEvents(); + } + } + + // @codeCoverageIgnoreStart + throw new NoTestCaseObjectOnCallStackException; + // @codeCoverageIgnoreEnd + } +} diff --git a/tests/Phpunit/PacketonTestTrait.php b/tests/Phpunit/PacketonTestTrait.php new file mode 100644 index 00000000..03f86d7b --- /dev/null +++ b/tests/Phpunit/PacketonTestTrait.php @@ -0,0 +1,35 @@ +get(ManagerRegistry::class) + ->getRepository(User::class) + ->findOneBy(['username' => $username]); + } + + private function basicLogin(string $username, KernelBrowser $client): void + { + $client->setServerParameter('PHP_AUTH_USER', $username); + $client->setServerParameter('PHP_AUTH_PW', 'token123'); + } + + private function getJsonResponse(KernelBrowser $client, $statusCode = 200): array + { + static::assertResponseStatusCodeSame($statusCode); + $response = $client->getResponse(); + + $content = json_decode($response->getContent(), true); + static::assertNotNull($content); + return $content; + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 469dccee..cb147323 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -9,3 +9,25 @@ } elseif (method_exists(Dotenv::class, 'bootEnv')) { (new Dotenv())->bootEnv(dirname(__DIR__).'/.env'); } + +/** + * Executes a given command. + * + * @param string $command a command to execute + */ +function executeCommand(string $command): void { + $output = []; + $returnCode = null; + exec($command, $output, $returnCode); + if ($returnCode !== 0) { + throw new \Exception(sprintf('Error executing command "%s", return code was "%s".', $command, $returnCode)); + } +} + +$projectDir = dirname(__DIR__); + +$db = gzdecode(file_get_contents($projectDir.'/tests/dump/test.db.gz')); +file_put_contents($projectDir.'/var/test.db', $db); + +executeCommand('php ./bin/console doctrine:schema:update --force --complete --env=test -q'); +executeCommand('php ./bin/console redis:query flushall --env=test -n -q'); diff --git a/tests/dump/test.db.gz b/tests/dump/test.db.gz new file mode 100644 index 00000000..5001c751 Binary files /dev/null and b/tests/dump/test.db.gz differ diff --git a/tests/phpunit b/tests/phpunit new file mode 100755 index 00000000..5a58215a --- /dev/null +++ b/tests/phpunit @@ -0,0 +1,6 @@ +#!/usr/bin/env php +