Add conformite page, SonarQube badge proxy, coverage fixes, and code quality

- Add /conformite page: PSD2/3DS/Stripe, SonarQube badges, CI/CD, security
- Create SonarBadgeController proxy to serve SonarQube badges without exposing token
- Store SonarQube badge token in ansible/vault.yml instead of env files
- Add Meilisearch coverage tests: search with results, search error, sync, delete
- Fix MeilisearchService delete catch block with comment
- Fix ESLint: use globalThis.confirm instead of window.confirm
- Fix accessibility: add for/id attributes to buyer creation form labels
- Add conformite link to site footer
- Add SonarBadgeControllerTest and LegalControllerTest for /conformite

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-19 14:25:04 +01:00
parent a4884e8b20
commit 9bcb41306b
15 changed files with 299 additions and 14 deletions

View File

@@ -91,13 +91,24 @@ class AdminControllerTest extends WebTestCase
self::assertResponseIsSuccessful();
}
public function testSyncMeilisearch(): void
public function testSyncMeilisearchWithBuyers(): void
{
$client = static::createClient();
$em = static::getContainer()->get(EntityManagerInterface::class);
$admin = $this->createUser(['ROLE_ROOT']);
$buyer = new User();
$buyer->setEmail('test-sync-'.uniqid().'@example.com');
$buyer->setFirstName('Sync');
$buyer->setLastName('Test');
$buyer->setPassword('$2y$13$hashed');
$buyer->setIsVerified(true);
$em->persist($buyer);
$em->flush();
$meilisearch = $this->createMock(MeilisearchService::class);
$meilisearch->expects(self::once())->method('createIndexIfNotExists');
$meilisearch->expects(self::once())->method('addDocuments');
static::getContainer()->set(MeilisearchService::class, $meilisearch);
$client->loginUser($admin);
@@ -106,6 +117,49 @@ class AdminControllerTest extends WebTestCase
self::assertResponseRedirects('/admin');
}
public function testBuyersSearchWithMeilisearchError(): void
{
$client = static::createClient();
$admin = $this->createUser(['ROLE_ROOT']);
$meilisearch = $this->createMock(MeilisearchService::class);
$meilisearch->method('search')->willThrowException(new \RuntimeException('Meilisearch down'));
static::getContainer()->set(MeilisearchService::class, $meilisearch);
$client->loginUser($admin);
$client->request('GET', '/admin/acheteurs?q=test');
self::assertResponseIsSuccessful();
}
public function testBuyersSearchWithResults(): void
{
$client = static::createClient();
$em = static::getContainer()->get(EntityManagerInterface::class);
$admin = $this->createUser(['ROLE_ROOT']);
$buyer = new User();
$buyer->setEmail('test-search-hit-'.uniqid().'@example.com');
$buyer->setFirstName('Found');
$buyer->setLastName('User');
$buyer->setPassword('$2y$13$hashed');
$buyer->setIsVerified(true);
$em->persist($buyer);
$em->flush();
$meilisearch = $this->createMock(MeilisearchService::class);
$meilisearch->method('search')->willReturn([
'hits' => [['id' => $buyer->getId()]],
'estimatedTotalHits' => 1,
]);
static::getContainer()->set(MeilisearchService::class, $meilisearch);
$client->loginUser($admin);
$client->request('GET', '/admin/acheteurs?q=Found');
self::assertResponseIsSuccessful();
}
public function testCreateBuyerWithValidData(): void
{
$client = static::createClient();
@@ -210,6 +264,10 @@ class AdminControllerTest extends WebTestCase
$em->flush();
$buyerId = $buyer->getId();
$meilisearch = $this->createMock(MeilisearchService::class);
$meilisearch->expects(self::once())->method('deleteDocument')->with('buyers', $buyerId);
static::getContainer()->set(MeilisearchService::class, $meilisearch);
$client->loginUser($admin);
$client->request('POST', '/admin/acheteur/'.$buyerId.'/supprimer');
@@ -219,6 +277,32 @@ class AdminControllerTest extends WebTestCase
self::assertNull($deleted);
}
public function testDeleteBuyerMeilisearchFailureDoesNotBlock(): void
{
$client = static::createClient();
$em = static::getContainer()->get(EntityManagerInterface::class);
$admin = $this->createUser(['ROLE_ROOT']);
$buyer = new User();
$buyer->setEmail('test-delete-fail-'.uniqid().'@example.com');
$buyer->setFirstName('DeleteFail');
$buyer->setLastName('Test');
$buyer->setPassword('$2y$13$hashed');
$em->persist($buyer);
$em->flush();
$buyerId = $buyer->getId();
$meilisearch = $this->createMock(MeilisearchService::class);
$meilisearch->method('deleteDocument')->willThrowException(new \RuntimeException('Meilisearch down'));
static::getContainer()->set(MeilisearchService::class, $meilisearch);
$client->loginUser($admin);
$client->request('POST', '/admin/acheteur/'.$buyerId.'/supprimer');
self::assertResponseRedirects('/admin/acheteurs');
}
public function testForceVerification(): void
{
$client = static::createClient();

View File

@@ -53,4 +53,12 @@ class LegalControllerTest extends WebTestCase
self::assertResponseIsSuccessful();
}
public function testConformite(): void
{
$client = static::createClient();
$client->request('GET', '/conformite');
self::assertResponseIsSuccessful();
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class SonarBadgeControllerTest extends WebTestCase
{
public function testBadgeWithValidMetric(): void
{
$client = static::createClient();
$client->request('GET', '/badge/sonar/alert_status.svg');
self::assertResponseIsSuccessful();
}
public function testBadgeWithInvalidMetric(): void
{
$client = static::createClient();
$client->request('GET', '/badge/sonar/invalid_metric.svg');
self::assertResponseStatusCodeSame(404);
}
}

View File

@@ -4,7 +4,7 @@ describe('admin.js', () => {
beforeEach(() => {
document.body.innerHTML = ''
vi.restoreAllMocks()
window.confirm = vi.fn()
globalThis.confirm = vi.fn()
})
it('prevents form submit when confirm is cancelled', async () => {
@@ -14,7 +14,7 @@ describe('admin.js', () => {
</form>
`
window.confirm.mockReturnValue(false)
globalThis.confirm.mockReturnValue(false)
await import('../../assets/admin.js')
document.dispatchEvent(new Event('DOMContentLoaded'))
@@ -23,7 +23,7 @@ describe('admin.js', () => {
const event = new Event('submit', { cancelable: true })
form.dispatchEvent(event)
expect(window.confirm).toHaveBeenCalledWith('Are you sure?')
expect(globalThis.confirm).toHaveBeenCalledWith('Are you sure?')
expect(event.defaultPrevented).toBe(true)
})
@@ -34,7 +34,7 @@ describe('admin.js', () => {
</form>
`
window.confirm.mockReturnValue(true)
globalThis.confirm.mockReturnValue(true)
await import('../../assets/admin.js')
document.dispatchEvent(new Event('DOMContentLoaded'))
@@ -43,7 +43,7 @@ describe('admin.js', () => {
const event = new Event('submit', { cancelable: true })
form.dispatchEvent(event)
expect(window.confirm).toHaveBeenCalledWith('Are you sure?')
expect(globalThis.confirm).toHaveBeenCalledWith('Are you sure?')
expect(event.defaultPrevented).toBe(false)
})
@@ -61,7 +61,7 @@ describe('admin.js', () => {
const event = new Event('submit', { cancelable: true })
form.dispatchEvent(event)
expect(window.confirm).not.toHaveBeenCalled()
expect(globalThis.confirm).not.toHaveBeenCalled()
expect(event.defaultPrevented).toBe(false)
})
})