SuperSeller3000/tests/Unit/Infrastructure/AI/MistralClientTest.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

123 lines
4.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Unit\Infrastructure\AI;
use App\Infrastructure\AI\MistralClient;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
final class MistralClientTest extends TestCase
{
private function makeClient(MockResponse $response): MistralClient
{
return new MistralClient(
new MockHttpClient($response),
'test-api-key',
'https://api.mistral.ai',
);
}
public function testGenerateReturnsParsedContent(): void
{
$body = json_encode([
'choices' => [['message' => ['content' => 'Dell Latitude 5520']]],
]);
$client = $this->makeClient(new MockResponse((string) $body));
self::assertSame('Dell Latitude 5520', $client->generate('mistral-large-latest', 'What is this?'));
}
public function testGenerateSendsAuthHeader(): void
{
$body = json_encode([
'choices' => [['message' => ['content' => 'ok']]],
]);
$response = new MockResponse((string) $body);
$client = $this->makeClient($response);
$client->generate('mistral-large-latest', 'test');
self::assertStringContainsString(
'Bearer test-api-key',
implode(', ', $response->getRequestOptions()['headers'] ?? []),
);
}
public function testGenerateWithImageReturnsParsedContent(): void
{
$tmpFile = tempnam(sys_get_temp_dir(), 'test_') . '.jpg';
file_put_contents($tmpFile, 'fake-image-data');
$body = json_encode([
'choices' => [['message' => ['content' => 'MODEL: ThinkPad X1']]],
]);
$client = $this->makeClient(new MockResponse((string) $body));
$result = $client->generateWithImage('pixtral-12b-2409', 'Read the nameplate', $tmpFile);
unlink($tmpFile);
self::assertSame('MODEL: ThinkPad X1', $result);
}
public function testGenerateWithImageEncodesImageAsBase64(): void
{
$tmpFile = tempnam(sys_get_temp_dir(), 'test_') . '.png';
$imageContent = 'fake-png-bytes';
file_put_contents($tmpFile, $imageContent);
$body = json_encode([
'choices' => [['message' => ['content' => 'ok']]],
]);
$response = new MockResponse((string) $body);
$client = $this->makeClient($response);
$client->generateWithImage('pixtral-12b-2409', 'describe', $tmpFile);
unlink($tmpFile);
$requestBody = json_decode($response->getRequestOptions()['body'], true);
$imageUrl = $requestBody['messages'][0]['content'][1]['image_url']['url'];
self::assertStringStartsWith('data:image/png;base64,', $imageUrl);
self::assertStringContainsString(base64_encode($imageContent), $imageUrl);
}
/**
* @dataProvider mimeTypeProvider
*/
public function testGuessMimeTypeFromExtension(string $filename, string $expectedMime): void
{
$tmpFile = tempnam(sys_get_temp_dir(), 'test_') . '.' . pathinfo($filename, PATHINFO_EXTENSION);
file_put_contents($tmpFile, 'x');
$body = json_encode(['choices' => [['message' => ['content' => 'ok']]]]);
$response = new MockResponse((string) $body);
$client = $this->makeClient($response);
$client->generateWithImage('pixtral-12b-2409', 'x', $tmpFile);
unlink($tmpFile);
$requestBody = json_decode($response->getRequestOptions()['body'], true);
$imageUrl = $requestBody['messages'][0]['content'][1]['image_url']['url'];
self::assertStringStartsWith('data:' . $expectedMime . ';base64,', $imageUrl);
}
/** @return array<string, array{string, string}> */
public static function mimeTypeProvider(): array
{
return [
'jpeg' => ['photo.jpg', 'image/jpeg'],
'jpeg uppercase' => ['photo.JPG', 'image/jpeg'],
'jpeg ext' => ['photo.jpeg', 'image/jpeg'],
'png' => ['photo.png', 'image/png'],
'webp' => ['photo.webp', 'image/webp'],
'unknown falls back to jpeg' => ['photo.bmp', 'image/jpeg'],
];
}
}