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:
6
.env
6
.env
@@ -47,6 +47,12 @@ STRIPE_WEBHOOK_SECRET=
|
|||||||
STRIPE_MODE=test
|
STRIPE_MODE=test
|
||||||
SMIME_PASSPHRASE='KLreLnyR07x5h#3$AC'
|
SMIME_PASSPHRASE='KLreLnyR07x5h#3$AC'
|
||||||
|
|
||||||
|
###> SonarQube ###
|
||||||
|
SONARQUBE_URL=https://sn.esy-web.dev
|
||||||
|
SONARQUBE_BADGE_TOKEN=changeme
|
||||||
|
SONARQUBE_PROJECT_KEY=e-ticket
|
||||||
|
###< SonarQube ###
|
||||||
|
|
||||||
###> SSO E-Cosplay (Keycloak OIDC) ###
|
###> SSO E-Cosplay (Keycloak OIDC) ###
|
||||||
OAUTH_KEYCLOAK_CLIENT_ID=e-ticket
|
OAUTH_KEYCLOAK_CLIENT_ID=e-ticket
|
||||||
OAUTH_KEYCLOAK_CLIENT_SECRET=changeme
|
OAUTH_KEYCLOAK_CLIENT_SECRET=changeme
|
||||||
|
|||||||
@@ -3,3 +3,6 @@ KERNEL_CLASS='App\Kernel'
|
|||||||
APP_SECRET='$ecretf0rt3st'
|
APP_SECRET='$ecretf0rt3st'
|
||||||
MEILISEARCH_URL=http://meilisearch:7700
|
MEILISEARCH_URL=http://meilisearch:7700
|
||||||
MEILISEARCH_API_KEY=test
|
MEILISEARCH_API_KEY=test
|
||||||
|
SONARQUBE_URL=https://sn.esy-web.dev
|
||||||
|
SONARQUBE_BADGE_TOKEN=test
|
||||||
|
SONARQUBE_PROJECT_KEY=e-ticket
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ STRIPE_MODE=live
|
|||||||
SMIME_PASSPHRASE='{{ smime_passphrase }}'
|
SMIME_PASSPHRASE='{{ smime_passphrase }}'
|
||||||
MEILISEARCH_URL=http://meilisearch:7700
|
MEILISEARCH_URL=http://meilisearch:7700
|
||||||
MEILISEARCH_API_KEY={{ meilisearch_api_key }}
|
MEILISEARCH_API_KEY={{ meilisearch_api_key }}
|
||||||
|
SONARQUBE_URL=https://sn.esy-web.dev
|
||||||
|
SONARQUBE_BADGE_TOKEN={{ sonarqube_badge_token }}
|
||||||
|
SONARQUBE_PROJECT_KEY=e-ticket
|
||||||
OAUTH_KEYCLOAK_CLIENT_ID=e-ticket
|
OAUTH_KEYCLOAK_CLIENT_ID=e-ticket
|
||||||
OAUTH_KEYCLOAK_CLIENT_SECRET=1oLwbhJDNVmGH8CES1OdQtzR7dECOlII
|
OAUTH_KEYCLOAK_CLIENT_SECRET=1oLwbhJDNVmGH8CES1OdQtzR7dECOlII
|
||||||
OAUTH_KEYCLOAK_URL=https://auth.esy-web.dev
|
OAUTH_KEYCLOAK_URL=https://auth.esy-web.dev
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ stripe_sk: sk_live_51SUA1rP4ub49xK2TR9CKVBChBDLMFWRI9AAxdLLKi0zL5RTSho7t8WniREqE
|
|||||||
stripe_webhook_secret: CHANGE_ME
|
stripe_webhook_secret: CHANGE_ME
|
||||||
smime_passphrase: 'KLreLnyR07x5h#3$AC'
|
smime_passphrase: 'KLreLnyR07x5h#3$AC'
|
||||||
meilisearch_api_key: e-ticket
|
meilisearch_api_key: e-ticket
|
||||||
|
sonarqube_badge_token: sqb_630fba527176cec0567f773ca0cf87a3195a0f8f
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import "./admin.scss"
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
document.querySelectorAll('[data-confirm]').forEach(form => {
|
document.querySelectorAll('[data-confirm]').forEach(form => {
|
||||||
form.addEventListener('submit', (e) => {
|
form.addEventListener('submit', (e) => {
|
||||||
if (!window.confirm(form.dataset.confirm)) {
|
if (!globalThis.confirm(form.dataset.confirm)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ class AdminController extends AbstractController
|
|||||||
try {
|
try {
|
||||||
$meilisearch->deleteDocument('buyers', $userId);
|
$meilisearch->deleteDocument('buyers', $userId);
|
||||||
} catch (\Throwable) {
|
} catch (\Throwable) {
|
||||||
|
// Meilisearch failure should not block user deletion
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->addFlash('success', sprintf('Compte de %s supprime.', $name));
|
$this->addFlash('success', sprintf('Compte de %s supprime.', $name));
|
||||||
|
|||||||
@@ -43,4 +43,10 @@ class LegalController extends AbstractController
|
|||||||
{
|
{
|
||||||
return $this->render('legal/rgpd.html.twig');
|
return $this->render('legal/rgpd.html.twig');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/conformite', name: 'app_conformite')]
|
||||||
|
public function conformite(): Response
|
||||||
|
{
|
||||||
|
return $this->render('legal/conformite.html.twig');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
src/Controller/SonarBadgeController.php
Normal file
52
src/Controller/SonarBadgeController.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
|
||||||
|
class SonarBadgeController extends AbstractController
|
||||||
|
{
|
||||||
|
private const ALLOWED_METRICS = [
|
||||||
|
'alert_status',
|
||||||
|
'security_rating',
|
||||||
|
'reliability_rating',
|
||||||
|
'sqale_rating',
|
||||||
|
'coverage',
|
||||||
|
'vulnerabilities',
|
||||||
|
'bugs',
|
||||||
|
'code_smells',
|
||||||
|
];
|
||||||
|
|
||||||
|
#[Route('/badge/sonar/{metric}.svg', name: 'app_sonar_badge', requirements: ['metric' => '[a-z_]+'])]
|
||||||
|
public function badge(
|
||||||
|
string $metric,
|
||||||
|
HttpClientInterface $httpClient,
|
||||||
|
#[Autowire(env: 'SONARQUBE_URL')] string $sonarUrl,
|
||||||
|
#[Autowire(env: 'SONARQUBE_BADGE_TOKEN')] string $sonarToken,
|
||||||
|
#[Autowire(env: 'SONARQUBE_PROJECT_KEY')] string $projectKey,
|
||||||
|
): Response {
|
||||||
|
if (!\in_array($metric, self::ALLOWED_METRICS, true)) {
|
||||||
|
return new Response('', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = sprintf(
|
||||||
|
'%s/api/project_badges/measure?project=%s&metric=%s&token=%s',
|
||||||
|
$sonarUrl,
|
||||||
|
$projectKey,
|
||||||
|
$metric,
|
||||||
|
$sonarToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $httpClient->request('GET', $url);
|
||||||
|
$svg = $response->getContent(false);
|
||||||
|
|
||||||
|
return new Response($svg, 200, [
|
||||||
|
'Content-Type' => 'image/svg+xml',
|
||||||
|
'Cache-Control' => 'public, max-age=300',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,16 +28,16 @@
|
|||||||
<h2 class="text-sm font-black uppercase tracking-widest" style="margin-bottom:1rem;">Creer un acheteur</h2>
|
<h2 class="text-sm font-black uppercase tracking-widest" style="margin-bottom:1rem;">Creer un acheteur</h2>
|
||||||
<form method="post" action="{{ path('app_admin_create_buyer') }}" style="display:flex;flex-wrap:wrap;gap:1rem;align-items:flex-end;">
|
<form method="post" action="{{ path('app_admin_create_buyer') }}" style="display:flex;flex-wrap:wrap;gap:1rem;align-items:flex-end;">
|
||||||
<div style="flex:1;min-width:140px;">
|
<div style="flex:1;min-width:140px;">
|
||||||
<label style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">Nom</label>
|
<label for="create_last_name" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">Nom</label>
|
||||||
<input type="text" name="last_name" required style="width:100%;padding:0.5rem 0.75rem;border:2px solid #111827;font-weight:700;outline:none;" placeholder="Dupont">
|
<input type="text" id="create_last_name" name="last_name" required style="width:100%;padding:0.5rem 0.75rem;border:2px solid #111827;font-weight:700;outline:none;" placeholder="Dupont">
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:1;min-width:140px;">
|
<div style="flex:1;min-width:140px;">
|
||||||
<label style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">Prenom</label>
|
<label for="create_first_name" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">Prenom</label>
|
||||||
<input type="text" name="first_name" required style="width:100%;padding:0.5rem 0.75rem;border:2px solid #111827;font-weight:700;outline:none;" placeholder="Jean">
|
<input type="text" id="create_first_name" name="first_name" required style="width:100%;padding:0.5rem 0.75rem;border:2px solid #111827;font-weight:700;outline:none;" placeholder="Jean">
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:2;min-width:200px;">
|
<div style="flex:2;min-width:200px;">
|
||||||
<label style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">Email</label>
|
<label for="create_email" style="font-size:10px;letter-spacing:0.1em;display:block;margin-bottom:0.25rem;" class="font-black uppercase text-gray-400">Email</label>
|
||||||
<input type="email" name="email" required style="width:100%;padding:0.5rem 0.75rem;border:2px solid #111827;font-weight:700;outline:none;" placeholder="jean.dupont@exemple.fr">
|
<input type="email" id="create_email" name="email" required style="width:100%;padding:0.5rem 0.75rem;border:2px solid #111827;font-weight:700;outline:none;" placeholder="jean.dupont@exemple.fr">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" style="padding:0.5rem 1rem;border:2px solid #111827;background:#fabf04;cursor:pointer;white-space:nowrap;" class="font-black uppercase text-xs tracking-widest hover:bg-green-500 hover:text-black transition-all">Creer</button>
|
<button type="submit" style="padding:0.5rem 1rem;border:2px solid #111827;background:#fabf04;cursor:pointer;white-space:nowrap;" class="font-black uppercase text-xs tracking-widest hover:bg-green-500 hover:text-black transition-all">Creer</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -162,6 +162,7 @@
|
|||||||
<a href="{{ path('app_rgpd') }}" style="font-size:10px;padding:0.25rem 0.5rem;" class="font-black uppercase bg-gray-900 text-white hover:bg-indigo-600 transition-colors">Politique RGPD</a>
|
<a href="{{ path('app_rgpd') }}" style="font-size:10px;padding:0.25rem 0.5rem;" class="font-black uppercase bg-gray-900 text-white hover:bg-indigo-600 transition-colors">Politique RGPD</a>
|
||||||
<a href="{{ path('app_hosting') }}" style="font-size:10px;padding:0.25rem 0.5rem;" class="font-black uppercase bg-gray-900 text-white hover:bg-indigo-600 transition-colors">Hebergement</a>
|
<a href="{{ path('app_hosting') }}" style="font-size:10px;padding:0.25rem 0.5rem;" class="font-black uppercase bg-gray-900 text-white hover:bg-indigo-600 transition-colors">Hebergement</a>
|
||||||
<a href="{{ path('app_tarifs') }}" style="font-size:10px;padding:0.25rem 0.5rem;" class="font-black uppercase bg-gray-900 text-white hover:bg-indigo-600 transition-colors">Tarifs</a>
|
<a href="{{ path('app_tarifs') }}" style="font-size:10px;padding:0.25rem 0.5rem;" class="font-black uppercase bg-gray-900 text-white hover:bg-indigo-600 transition-colors">Tarifs</a>
|
||||||
|
<a href="{{ path('app_conformite') }}" style="font-size:10px;padding:0.25rem 0.5rem;" class="font-black uppercase bg-gray-900 text-white hover:bg-indigo-600 transition-colors">Conformite</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
96
templates/legal/conformite.html.twig
Normal file
96
templates/legal/conformite.html.twig
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
|
||||||
|
{% block title %}Conformite technique - E-Ticket{% endblock %}
|
||||||
|
{% block description %}Conformite technique, securite des paiements et qualite du code de la plateforme E-Ticket{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div style="max-width:50rem;margin:0 auto;padding:3rem 1rem;">
|
||||||
|
<h1 class="text-3xl font-black uppercase tracking-tighter italic" style="border-bottom:4px solid #111827;display:inline-block;margin-bottom:0.5rem;">Conformite Technique</h1>
|
||||||
|
<p class="font-bold text-gray-600 italic" style="margin-bottom:2rem;">Transparence sur nos pratiques de securite, paiement et qualite logicielle.</p>
|
||||||
|
|
||||||
|
<div style="display:flex;flex-direction:column;gap:2rem;">
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">1. Securite des paiements</h2>
|
||||||
|
<div style="border:4px solid #111827;padding:1.5rem;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;">
|
||||||
|
<p style="margin-bottom:1rem;">E-Ticket utilise <strong>Stripe</strong> comme prestataire de paiement. Stripe est certifie <strong>PCI DSS niveau 1</strong>, le plus haut niveau de certification de l'industrie des paiements.</p>
|
||||||
|
<ul style="list-style:disc;padding-left:1.5rem;" class="text-sm font-bold text-gray-700">
|
||||||
|
<li><strong>PSD2 / SCA</strong> : Authentification forte du client (Strong Customer Authentication) via 3D Secure 2 pour toutes les transactions europeennes</li>
|
||||||
|
<li><strong>3D Secure</strong> : Verification supplementaire aupres de la banque emettrice pour reduire la fraude</li>
|
||||||
|
<li><strong>Tokenisation</strong> : Aucune donnee de carte bancaire ne transite ou n'est stockee sur nos serveurs</li>
|
||||||
|
<li><strong>HTTPS</strong> : Toutes les communications sont chiffrees via TLS 1.3</li>
|
||||||
|
<li><strong>Cloudflare</strong> : Protection DDoS et WAF (Web Application Firewall)</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-xs text-gray-500 italic" style="margin-top:1rem;">E-Ticket ne collecte, ne stocke et ne traite aucune donnee de carte bancaire. Stripe gere l'integralite du flux de paiement.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">2. Qualite du code source</h2>
|
||||||
|
<div style="border:4px solid #111827;padding:1.5rem;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;">
|
||||||
|
<p style="margin-bottom:1rem;">Le code source est analyse en continu par <strong>SonarQube</strong>, un outil d'analyse statique qui detecte les bugs, vulnerabilites et mauvaises pratiques.</p>
|
||||||
|
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:0.5rem;margin-bottom:1rem;">
|
||||||
|
<img src="{{ path('app_sonar_badge', {metric: 'alert_status'}) }}" alt="Quality Gate">
|
||||||
|
<img src="{{ path('app_sonar_badge', {metric: 'security_rating'}) }}" alt="Security Rating">
|
||||||
|
<img src="{{ path('app_sonar_badge', {metric: 'reliability_rating'}) }}" alt="Reliability Rating">
|
||||||
|
<img src="{{ path('app_sonar_badge', {metric: 'sqale_rating'}) }}" alt="Maintainability Rating">
|
||||||
|
<img src="{{ path('app_sonar_badge', {metric: 'coverage'}) }}" alt="Coverage">
|
||||||
|
<img src="{{ path('app_sonar_badge', {metric: 'vulnerabilities'}) }}" alt="Vulnerabilities">
|
||||||
|
<img src="{{ path('app_sonar_badge', {metric: 'bugs'}) }}" alt="Bugs">
|
||||||
|
<img src="{{ path('app_sonar_badge', {metric: 'code_smells'}) }}" alt="Code Smells">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul style="list-style:disc;padding-left:1.5rem;" class="text-sm font-bold text-gray-700">
|
||||||
|
<li><strong>PHPStan</strong> : Analyse statique PHP niveau 6</li>
|
||||||
|
<li><strong>PHP CS Fixer</strong> : Respect des standards de codage</li>
|
||||||
|
<li><strong>ESLint / Stylelint</strong> : Analyse du code JavaScript et SCSS</li>
|
||||||
|
<li><strong>PHPUnit</strong> : Tests unitaires et fonctionnels PHP</li>
|
||||||
|
<li><strong>Vitest</strong> : Tests unitaires JavaScript</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">3. Integration et deploiement continus</h2>
|
||||||
|
<div style="border:4px solid #111827;padding:1.5rem;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;">
|
||||||
|
<p style="margin-bottom:1rem;">Chaque modification du code passe par un pipeline d'integration continue automatise avant d'etre deploye en production.</p>
|
||||||
|
<ul style="list-style:disc;padding-left:1.5rem;" class="text-sm font-bold text-gray-700">
|
||||||
|
<li><strong>Gitea</strong> : Plateforme Git auto-hebergee pour le controle de version</li>
|
||||||
|
<li><strong>CI/CD</strong> : Tests automatiques, analyse de code et deploiement via Gitea Actions</li>
|
||||||
|
<li><strong>SonarQube</strong> : Analyse de qualite auto-hebergee apres chaque push</li>
|
||||||
|
<li><strong>Deploiement automatise</strong> : Deploiement en production uniquement si tous les controles passent</li>
|
||||||
|
</ul>
|
||||||
|
<p class="text-xs text-gray-500 italic" style="margin-top:1rem;">L'infrastructure CI/CD est entierement auto-hebergee et administree par l'association E-Cosplay.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">4. Securite des communications</h2>
|
||||||
|
<div style="border:4px solid #111827;padding:1.5rem;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;">
|
||||||
|
<ul style="list-style:disc;padding-left:1.5rem;" class="text-sm font-bold text-gray-700">
|
||||||
|
<li><strong>S/MIME</strong> : Tous les emails envoyes par la plateforme sont signes numeriquement</li>
|
||||||
|
<li><strong>CSP</strong> : Politique de securite du contenu stricte pour prevenir les injections XSS</li>
|
||||||
|
<li><strong>HSTS</strong> : Force les connexions HTTPS</li>
|
||||||
|
<li><strong>SSO</strong> : Authentification unique via OpenID Connect</li>
|
||||||
|
<li><strong>CSRF</strong> : Protection contre les attaques Cross-Site Request Forgery</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 class="text-xl font-black uppercase" style="margin-bottom:0.5rem;">5. Donnees et conformite</h2>
|
||||||
|
<div style="border:4px solid #111827;padding:1.5rem;box-shadow:6px 6px 0 rgba(0,0,0,1);background:white;">
|
||||||
|
<ul style="list-style:disc;padding-left:1.5rem;" class="text-sm font-bold text-gray-700">
|
||||||
|
<li><strong>RGPD</strong> : Conforme au reglement general sur la protection des donnees (<a href="{{ path('app_rgpd') }}" class="text-indigo-600 hover:underline">voir notre politique RGPD</a>)</li>
|
||||||
|
<li><strong>Hebergement</strong> : Infrastructure hebergee en France (<a href="{{ path('app_hosting') }}" class="text-indigo-600 hover:underline">voir notre hebergeur</a>)</li>
|
||||||
|
<li><strong>Chiffrement</strong> : Donnees sensibles chiffrees au repos et en transit</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-sm opacity-70 italic" style="margin-top:2rem;">Derniere mise a jour : {{ "now"|date("d/m/Y") }}. Contact : <a href="mailto:contact@e-cosplay.fr" class="text-indigo-600 hover:underline">contact@e-cosplay.fr</a></p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -91,13 +91,24 @@ class AdminControllerTest extends WebTestCase
|
|||||||
self::assertResponseIsSuccessful();
|
self::assertResponseIsSuccessful();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSyncMeilisearch(): void
|
public function testSyncMeilisearchWithBuyers(): void
|
||||||
{
|
{
|
||||||
$client = static::createClient();
|
$client = static::createClient();
|
||||||
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||||
$admin = $this->createUser(['ROLE_ROOT']);
|
$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 = $this->createMock(MeilisearchService::class);
|
||||||
$meilisearch->expects(self::once())->method('createIndexIfNotExists');
|
$meilisearch->expects(self::once())->method('createIndexIfNotExists');
|
||||||
|
$meilisearch->expects(self::once())->method('addDocuments');
|
||||||
static::getContainer()->set(MeilisearchService::class, $meilisearch);
|
static::getContainer()->set(MeilisearchService::class, $meilisearch);
|
||||||
|
|
||||||
$client->loginUser($admin);
|
$client->loginUser($admin);
|
||||||
@@ -106,6 +117,49 @@ class AdminControllerTest extends WebTestCase
|
|||||||
self::assertResponseRedirects('/admin');
|
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
|
public function testCreateBuyerWithValidData(): void
|
||||||
{
|
{
|
||||||
$client = static::createClient();
|
$client = static::createClient();
|
||||||
@@ -210,6 +264,10 @@ class AdminControllerTest extends WebTestCase
|
|||||||
$em->flush();
|
$em->flush();
|
||||||
$buyerId = $buyer->getId();
|
$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->loginUser($admin);
|
||||||
$client->request('POST', '/admin/acheteur/'.$buyerId.'/supprimer');
|
$client->request('POST', '/admin/acheteur/'.$buyerId.'/supprimer');
|
||||||
|
|
||||||
@@ -219,6 +277,32 @@ class AdminControllerTest extends WebTestCase
|
|||||||
self::assertNull($deleted);
|
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
|
public function testForceVerification(): void
|
||||||
{
|
{
|
||||||
$client = static::createClient();
|
$client = static::createClient();
|
||||||
|
|||||||
@@ -53,4 +53,12 @@ class LegalControllerTest extends WebTestCase
|
|||||||
|
|
||||||
self::assertResponseIsSuccessful();
|
self::assertResponseIsSuccessful();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testConformite(): void
|
||||||
|
{
|
||||||
|
$client = static::createClient();
|
||||||
|
$client->request('GET', '/conformite');
|
||||||
|
|
||||||
|
self::assertResponseIsSuccessful();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
24
tests/Controller/SonarBadgeControllerTest.php
Normal file
24
tests/Controller/SonarBadgeControllerTest.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ describe('admin.js', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
document.body.innerHTML = ''
|
document.body.innerHTML = ''
|
||||||
vi.restoreAllMocks()
|
vi.restoreAllMocks()
|
||||||
window.confirm = vi.fn()
|
globalThis.confirm = vi.fn()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('prevents form submit when confirm is cancelled', async () => {
|
it('prevents form submit when confirm is cancelled', async () => {
|
||||||
@@ -14,7 +14,7 @@ describe('admin.js', () => {
|
|||||||
</form>
|
</form>
|
||||||
`
|
`
|
||||||
|
|
||||||
window.confirm.mockReturnValue(false)
|
globalThis.confirm.mockReturnValue(false)
|
||||||
|
|
||||||
await import('../../assets/admin.js')
|
await import('../../assets/admin.js')
|
||||||
document.dispatchEvent(new Event('DOMContentLoaded'))
|
document.dispatchEvent(new Event('DOMContentLoaded'))
|
||||||
@@ -23,7 +23,7 @@ describe('admin.js', () => {
|
|||||||
const event = new Event('submit', { cancelable: true })
|
const event = new Event('submit', { cancelable: true })
|
||||||
form.dispatchEvent(event)
|
form.dispatchEvent(event)
|
||||||
|
|
||||||
expect(window.confirm).toHaveBeenCalledWith('Are you sure?')
|
expect(globalThis.confirm).toHaveBeenCalledWith('Are you sure?')
|
||||||
expect(event.defaultPrevented).toBe(true)
|
expect(event.defaultPrevented).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ describe('admin.js', () => {
|
|||||||
</form>
|
</form>
|
||||||
`
|
`
|
||||||
|
|
||||||
window.confirm.mockReturnValue(true)
|
globalThis.confirm.mockReturnValue(true)
|
||||||
|
|
||||||
await import('../../assets/admin.js')
|
await import('../../assets/admin.js')
|
||||||
document.dispatchEvent(new Event('DOMContentLoaded'))
|
document.dispatchEvent(new Event('DOMContentLoaded'))
|
||||||
@@ -43,7 +43,7 @@ describe('admin.js', () => {
|
|||||||
const event = new Event('submit', { cancelable: true })
|
const event = new Event('submit', { cancelable: true })
|
||||||
form.dispatchEvent(event)
|
form.dispatchEvent(event)
|
||||||
|
|
||||||
expect(window.confirm).toHaveBeenCalledWith('Are you sure?')
|
expect(globalThis.confirm).toHaveBeenCalledWith('Are you sure?')
|
||||||
expect(event.defaultPrevented).toBe(false)
|
expect(event.defaultPrevented).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ describe('admin.js', () => {
|
|||||||
const event = new Event('submit', { cancelable: true })
|
const event = new Event('submit', { cancelable: true })
|
||||||
form.dispatchEvent(event)
|
form.dispatchEvent(event)
|
||||||
|
|
||||||
expect(window.confirm).not.toHaveBeenCalled()
|
expect(globalThis.confirm).not.toHaveBeenCalled()
|
||||||
expect(event.defaultPrevented).toBe(false)
|
expect(event.defaultPrevented).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user