116 lines
3.9 KiB
PHP
116 lines
3.9 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace App\Tests\Unit\Infrastructure\Console;
|
||
|
|
|
||
|
|
use App\Domain\Auth\ApiKey;
|
||
|
|
use App\Domain\Auth\Repository\ApiKeyRepositoryInterface;
|
||
|
|
use App\Domain\Auth\Repository\UserRepositoryInterface;
|
||
|
|
use App\Domain\Auth\User;
|
||
|
|
use App\Infrastructure\Console\CreateApiKeyCommand;
|
||
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
||
|
|
use PHPUnit\Framework\TestCase;
|
||
|
|
use Symfony\Component\Console\Tester\CommandTester;
|
||
|
|
|
||
|
|
final class CreateApiKeyCommandTest extends TestCase
|
||
|
|
{
|
||
|
|
private UserRepositoryInterface&MockObject $users;
|
||
|
|
private ApiKeyRepositoryInterface&MockObject $apiKeys;
|
||
|
|
private CommandTester $tester;
|
||
|
|
|
||
|
|
protected function setUp(): void
|
||
|
|
{
|
||
|
|
$this->users = $this->createMock(UserRepositoryInterface::class);
|
||
|
|
$this->apiKeys = $this->createMock(ApiKeyRepositoryInterface::class);
|
||
|
|
|
||
|
|
$this->tester = new CommandTester(new CreateApiKeyCommand($this->users, $this->apiKeys));
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testFailsWhenEmailIsEmpty(): void
|
||
|
|
{
|
||
|
|
$this->tester->setInputs(['', '']);
|
||
|
|
|
||
|
|
$this->tester->execute([]);
|
||
|
|
|
||
|
|
self::assertSame(1, $this->tester->getStatusCode());
|
||
|
|
self::assertStringContainsString('Email is required', $this->tester->getDisplay());
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testFailsWhenUserNotFound(): void
|
||
|
|
{
|
||
|
|
$this->users->method('findByEmail')->willReturn(null);
|
||
|
|
|
||
|
|
$this->tester->setInputs(['unknown@example.com', '']);
|
||
|
|
|
||
|
|
$this->tester->execute([]);
|
||
|
|
|
||
|
|
self::assertSame(1, $this->tester->getStatusCode());
|
||
|
|
self::assertStringContainsString('No user found', $this->tester->getDisplay());
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testFailsWhenLabelIsEmpty(): void
|
||
|
|
{
|
||
|
|
$this->users->method('findByEmail')->willReturn(new User('test@example.com', 'hash'));
|
||
|
|
|
||
|
|
$this->tester->setInputs(['test@example.com', '']);
|
||
|
|
|
||
|
|
$this->tester->execute([]);
|
||
|
|
|
||
|
|
self::assertSame(1, $this->tester->getStatusCode());
|
||
|
|
self::assertStringContainsString('Label is required', $this->tester->getDisplay());
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testCreatesKeyAndPrintsIt(): void
|
||
|
|
{
|
||
|
|
$user = new User('test@example.com', 'hash');
|
||
|
|
$this->users->method('findByEmail')->willReturn($user);
|
||
|
|
|
||
|
|
$savedKey = null;
|
||
|
|
$this->apiKeys->expects($this->once())
|
||
|
|
->method('save')
|
||
|
|
->willReturnCallback(function (ApiKey $key) use (&$savedKey): void {
|
||
|
|
$savedKey = $key;
|
||
|
|
});
|
||
|
|
|
||
|
|
$this->tester->setInputs(['test@example.com', 'dev laptop']);
|
||
|
|
$this->tester->execute([]);
|
||
|
|
|
||
|
|
self::assertSame(0, $this->tester->getStatusCode());
|
||
|
|
|
||
|
|
$display = $this->tester->getDisplay();
|
||
|
|
self::assertStringContainsString('API key created', $display);
|
||
|
|
self::assertStringContainsString('dev laptop', $display);
|
||
|
|
|
||
|
|
self::assertNotNull($savedKey);
|
||
|
|
self::assertSame('dev laptop', $savedKey->getLabel());
|
||
|
|
self::assertSame(8, strlen($savedKey->getKeyPrefix()));
|
||
|
|
self::assertStringContainsString($savedKey->getKeyPrefix(), $display);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testStoredHashVerifiesAgainstPrintedKey(): void
|
||
|
|
{
|
||
|
|
$user = new User('test@example.com', 'hash');
|
||
|
|
$this->users->method('findByEmail')->willReturn($user);
|
||
|
|
|
||
|
|
$savedKey = null;
|
||
|
|
$this->apiKeys->method('save')
|
||
|
|
->willReturnCallback(function (ApiKey $key) use (&$savedKey): void {
|
||
|
|
$savedKey = $key;
|
||
|
|
});
|
||
|
|
|
||
|
|
$this->tester->setInputs(['test@example.com', 'ci-runner']);
|
||
|
|
$this->tester->execute([]);
|
||
|
|
|
||
|
|
self::assertNotNull($savedKey);
|
||
|
|
|
||
|
|
// SymfonyStyle table uses spaces, not pipes: " API Key <hex48> "
|
||
|
|
preg_match('/API Key\s+([a-f0-9]{48})/', $this->tester->getDisplay(), $matches);
|
||
|
|
self::assertNotEmpty($matches, 'Raw key not found in output');
|
||
|
|
|
||
|
|
$rawKey = $matches[1];
|
||
|
|
self::assertSame(substr($rawKey, 0, 8), $savedKey->getKeyPrefix());
|
||
|
|
self::assertTrue(password_verify($rawKey, $savedKey->getKeyHash()));
|
||
|
|
}
|
||
|
|
}
|