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
+