SuperSeller3000/tests/Unit/Infrastructure/Console/CreateApiKeyCommandTest.php
Simon Kuehn f915bba966 feat: admin panel, Mistral client, attribute management, API key command
- Fix EasyAdmin 5 routing: #[AdminDashboard] attribute + easyadmin.routes loader
- Fix login: _username/_password field names, CSRF stateless token config,
  sessions directory, Opcache reload after cache:clear
- Add MistralClient behind OllamaClientInterface — switchable via services.yaml alias
- Add Attribute CRUD with EnumType form + ChoiceField display (enum-safe rendering)
- Add Article Type CRUD with AssociationField for attribute assignments
- Add app:api-keys:create console command (bcrypt-hashed, never stored as plaintext)
- Add redis ext to Docker image + symfony/redis-messenger, start workers
- Translate all UI strings to English
- Add tests: MistralClient, ApiKey, CreateApiKeyCommand, StringArrayType,
  ArticleTypeCrudController, AttributeDefinitionCrudController (82 tests total)
- Update design doc: tech stack, AI backend switching guide, ops section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 20:15:13 +00:00

115 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()));
}
}