tests/Functional/Commands/AppKeyCommandsTest.php
Rama main · réplica de lectura del repositorio original.
<?php
declare(strict_types=1);
namespace App\Tests\Functional\Commands;
use App\Command\AppKeyPruneCommand;
use App\Entity\AppKey;
use App\Services\BasicAuth;
use Doctrine\ORM\EntityManagerInterface;
use Sabre\HTTP\Request;
use Sabre\HTTP\Response;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Tester\CommandTester;
class AppKeyCommandsTest extends KernelTestCase
{
private function runCommand(string $name, array $input = []): CommandTester
{
$tester = new CommandTester((new Application(self::bootKernel()))->find($name));
$tester->execute($input);
return $tester;
}
private static function findKeyByAppId(string $appId): ?AppKey
{
$em = static::getContainer()->get(EntityManagerInterface::class);
$em->clear();
return $em->getRepository(AppKey::class)->findOneByAppId($appId);
}
private static function keyByAppId(string $appId): AppKey
{
$key = self::findKeyByAppId($appId);
self::assertNotNull($key, "No app key with app id $appId");
return $key;
}
private static function countKeys(): int
{
return count(static::getContainer()->get(EntityManagerInterface::class)->getRepository(AppKey::class)->findAll());
}
/**
* Saves a key, with one usage row so that the tests can check the history goes away with it.
*/
private static function persistKey(string $name, ?\DateTimeImmutable $revokedAt): AppKey
{
$em = static::getContainer()->get(EntityManagerInterface::class);
$key = AppKey::create($name, [AppKey::ALL_USERS], $revokedAt);
$key->issueSecret();
$em->persist($key);
$em->flush();
$connection = $em->getConnection();
$format = $connection->getDatabasePlatform()->getDateTimeFormatString();
$connection->insert('app_key_usages', [
'app_key_id' => $key->getId(),
'login' => 'test_user',
'first_used_at' => (new \DateTimeImmutable('-2 days'))->format($format),
'last_used_at' => (new \DateTimeImmutable('-1 hour'))->format($format),
]);
return $key;
}
private static function countUsages(AppKey $key): int
{
return (int) static::getContainer()->get(EntityManagerInterface::class)->getConnection()->fetchOne(
'SELECT COUNT(*) FROM app_key_usages WHERE app_key_id = ?',
[$key->getId()]
);
}
/**
* @return array{0: string, 1: string} the app id and the secret
*/
private function credentialsFrom(string $display): array
{
$this->assertSame(1, preg_match('/\b(app_[0-9a-f]{24})\b/', $display, $appId));
$this->assertSame(1, preg_match('/\b([0-9a-f]{64})\b/', $display, $secret));
return [$appId[1], $secret[1]];
}
public function testCreatePrintsTheAppIdAndTheSecretOnce(): void
{
$tester = $this->runCommand('davis:app-key:create', ['name' => 'cal.com', '--all-users' => true]);
$this->assertSame(Command::SUCCESS, $tester->getStatusCode());
$display = $tester->getDisplay();
[$appId, $secret] = $this->credentialsFrom($display);
$this->assertSame(1, substr_count($display, $secret));
$this->assertStringContainsString('<login>*'.$appId, $display);
$key = self::keyByAppId($appId);
$this->assertSame('cal.com', $key->getName());
$this->assertSame(['*'], $key->getScope());
$request = new Request('PROPFIND', '/dav/', ['Authorization' => 'Basic '.base64_encode('test_user2*'.$appId.':'.$secret)]);
[$ok, $principal] = static::getContainer()->get(BasicAuth::class)->check($request, new Response());
$this->assertTrue($ok);
$this->assertSame('principals/test_user2', $principal);
}
public function testCreateRequiresAScope(): void
{
self::bootKernel();
$before = self::countKeys();
$tester = $this->runCommand('davis:app-key:create', ['name' => 'nobody']);
$this->assertSame(Command::INVALID, $tester->getStatusCode());
$this->assertStringContainsString('--all-users', $tester->getDisplay());
$this->assertSame($before, self::countKeys());
}
public function testCreateForAllUsersAndANamedAdmin(): void
{
$tester = $this->runCommand('davis:app-key:create', ['name' => 'sync', '--all-users' => true, '--user' => ['test_user']]);
$this->assertSame(Command::SUCCESS, $tester->getStatusCode());
[$appId] = $this->credentialsFrom($tester->getDisplay());
$this->assertSame(['*', 'test_user'], self::keyByAppId($appId)->getScope());
}
public function testCreateWithUsersRestrictsTheScope(): void
{
$tester = $this->runCommand('davis:app-key:create', ['name' => 'webmail', '--user' => ['test_user', 'test_user2']]);
$this->assertSame(Command::SUCCESS, $tester->getStatusCode());
[$appId] = $this->credentialsFrom($tester->getDisplay());
$this->assertSame(['test_user', 'test_user2'], self::keyByAppId($appId)->getScope());
}
public function testCreateRejectsUnknownUsers(): void
{
self::bootKernel();
$before = self::countKeys();
$tester = $this->runCommand('davis:app-key:create', ['name' => 'webmail', '--user' => ['test_user2', 'alice', 'TEST_USER']]);
$this->assertSame(Command::INVALID, $tester->getStatusCode());
$this->assertStringContainsString('alice, TEST_USER', $tester->getDisplay());
$this->assertSame($before, self::countKeys());
}
public function testCreateRejectsABadExpiry(): void
{
self::bootKernel();
$before = self::countKeys();
$tester = $this->runCommand('davis:app-key:create', ['name' => 'nope', '--all-users' => true, '--expires-at' => 'yesterday-ish-garbage']);
$this->assertSame(Command::FAILURE, $tester->getStatusCode());
$this->assertSame($before, self::countKeys());
}
public function testListShowsEveryKey(): void
{
$tester = $this->runCommand('davis:app-key:list');
$this->assertSame(Command::SUCCESS, $tester->getStatusCode());
$display = $tester->getDisplay();
$this->assertStringContainsString('Test app', $display);
$this->assertStringContainsString('app_test', $display);
$this->assertStringContainsString('Revoked app', $display);
$this->assertStringContainsString('revoked', $display);
$this->assertStringNotContainsString('$2y$', $display);
}
public function testRevoke(): void
{
$tester = $this->runCommand('davis:app-key:revoke', ['app-id' => 'app_test']);
$this->assertSame(Command::SUCCESS, $tester->getStatusCode());
$this->assertNotNull(self::keyByAppId('app_test')->getRevokedAt());
$tester = $this->runCommand('davis:app-key:revoke', ['app-id' => 'app_nope']);
$this->assertSame(Command::FAILURE, $tester->getStatusCode());
$before = self::keyByAppId('app_revoked')->getRevokedAt();
$tester = $this->runCommand('davis:app-key:revoke', ['app-id' => 'app_revoked']);
$this->assertSame(Command::SUCCESS, $tester->getStatusCode());
$this->assertEquals($before, self::keyByAppId('app_revoked')->getRevokedAt());
}
public function testPruneDeletesTheKeysRevokedLongEnoughAgo(): void
{
self::bootKernel();
$old = self::persistKey('old', new \DateTimeImmutable('-10 days'));
$recent = self::persistKey('recent', new \DateTimeImmutable('-3 days'));
$active = self::persistKey('active', null);
$expiring = self::persistKey('expiring', new \DateTimeImmutable('+10 days'));
$tester = $this->runCommand('davis:app-key:prune', ['days' => '7']);
$this->assertSame(Command::SUCCESS, $tester->getStatusCode());
$this->assertStringContainsString($old->getAppId().' old revoked', $tester->getDisplay());
$this->assertNull(self::findKeyByAppId($old->getAppId()));
$this->assertSame(0, self::countUsages($old));
$this->assertNotNull(self::keyByAppId($recent->getAppId()));
$this->assertSame(1, self::countUsages($recent));
$this->assertNotNull(self::keyByAppId($active->getAppId()));
$this->assertNotNull(self::keyByAppId($expiring->getAppId()));
}
public function testPruneWithZeroDaysDeletesEveryRevokedKeyButNotAnExpiry(): void
{
self::bootKernel();
$justRevoked = self::persistKey('just revoked', new \DateTimeImmutable('-1 minute'));
$active = self::persistKey('active', null);
$expiring = self::persistKey('expiring', new \DateTimeImmutable('+1 hour'));
$tester = $this->runCommand('davis:app-key:prune', ['days' => '0']);
$this->assertSame(Command::SUCCESS, $tester->getStatusCode());
$this->assertNull(self::findKeyByAppId($justRevoked->getAppId()));
// app_revoked comes from the fixtures, revoked a day ago
$this->assertNull(self::findKeyByAppId('app_revoked'));
$this->assertNotNull(self::keyByAppId($active->getAppId()));
$this->assertNotNull(self::keyByAppId($expiring->getAppId()));
$this->assertNotNull(self::keyByAppId('app_test'));
}
public function testPruneDryRunDeletesNothing(): void
{
self::bootKernel();
$old = self::persistKey('old', new \DateTimeImmutable('-10 days'));
$before = self::countKeys();
$tester = $this->runCommand('davis:app-key:prune', ['days' => '7', '--dry-run' => true]);
$this->assertSame(Command::SUCCESS, $tester->getStatusCode());
$this->assertStringContainsString('Would delete '.$old->getAppId(), $tester->getDisplay());
$this->assertSame($before, self::countKeys());
$this->assertSame(1, self::countUsages($old));
}
public function testPruneWithNothingToDeleteSucceeds(): void
{
self::bootKernel();
$before = self::countKeys();
// the fixtures have one revoked key, a day old
$tester = $this->runCommand('davis:app-key:prune', ['days' => '365']);
$this->assertSame(Command::SUCCESS, $tester->getStatusCode());
$this->assertStringContainsString('nothing to delete', $tester->getDisplay());
$this->assertSame($before, self::countKeys());
}
public function testPruneRejectsABadNumberOfDays(): void
{
self::bootKernel();
$before = self::countKeys();
// The last two used to pass ctype_digit() and then blow up in DateInterval (or saturate to PHP_INT_MAX)
foreach (['-1', 'abc', '1.5', '', '1e3', (string) (AppKeyPruneCommand::MAX_DAYS + 1), '99999999999999999999'] as $days) {
$tester = $this->runCommand('davis:app-key:prune', ['days' => $days]);
$this->assertSame(Command::INVALID, $tester->getStatusCode(), $days);
$this->assertStringContainsString('not a number of days', $tester->getDisplay(), $days);
}
$this->assertSame($before, self::countKeys());
}
public function testDeleteRefusesAnActiveKey(): void
{
self::bootKernel();
$active = self::persistKey('active', null);
$expiring = self::persistKey('expiring', new \DateTimeImmutable('+10 days'));
foreach ([$active, $expiring] as $key) {
$tester = $this->runCommand('davis:app-key:delete', ['app-id' => $key->getAppId()]);
$this->assertSame(Command::FAILURE, $tester->getStatusCode());
$this->assertStringContainsString('davis:app-key:revoke', $tester->getDisplay());
$this->assertNotNull(self::keyByAppId($key->getAppId()));
$this->assertSame(1, self::countUsages($key));
}
}
public function testDeleteRemovesARevokedKeyAndItsUsages(): void
{
self::bootKernel();
$revoked = self::persistKey('revoked', new \DateTimeImmutable('-1 minute'));
$tester = $this->runCommand('davis:app-key:delete', ['app-id' => $revoked->getAppId()]);
$this->assertSame(Command::SUCCESS, $tester->getStatusCode());
$this->assertNull(self::findKeyByAppId($revoked->getAppId()));
$this->assertSame(0, self::countUsages($revoked));
}
public function testDeleteFailsOnAnUnknownKey(): void
{
self::bootKernel();
$before = self::countKeys();
$tester = $this->runCommand('davis:app-key:delete', ['app-id' => 'app_nope']);
$this->assertSame(Command::FAILURE, $tester->getStatusCode());
$this->assertSame($before, self::countKeys());
}
}