- 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>
115 lines
3.9 KiB
PHP
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()));
|
|
}
|
|
}
|