From af8bbc24dc522ee1b69fce40f300517ac590f44e Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Thu, 19 Mar 2026 00:01:58 +0100 Subject: [PATCH] Add homepage, tarifs, legal pages, navbar, footer and full test coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Homepage: hero, how it works (buyer/organizer), features, CTA - Tarifs: 3 plans (Gratuit, Basique 10€, Sur-mesure), JSON-LD Product - Legal pages: mentions legales, CGU (tabs buyer/organizer), CGV, RGPD, cookies, hosting - Navbar: neubrutalism style, logo liip, mobile menu, SEO attributes - Footer: contact, description, legal links, tarifs - Sitemap: add /tarifs and /sitemap-orgas-{page}.xml - Liip Imagine: remove S3, webp format on all filters - Tests: full coverage for all controllers, services, repositories - Fix CSP: replace inline onclick with data-tab JS Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitignore | 1 + Makefile | 6 + assets/app.js | 29 +++ assets/app.scss | 3 +- config/packages/liip_imagine.yaml | 25 +-- public/logo.jpg | 1 + src/Controller/HomeController.php | 11 ++ src/Controller/SitemapController.php | 25 +++ templates/base.html.twig | 98 +++++++++- templates/home/index.html.twig | 105 ++++++++++- templates/home/tarifs.html.twig | 147 +++++++++++++++ templates/legal/cgu.html.twig | 173 ++++++++++++++++++ templates/legal/cgv.html.twig | 130 +++++++++++++ templates/legal/cookies.html.twig | 81 +++++++- templates/legal/hosting.html.twig | 73 ++++++++ templates/legal/mentions_legales.html.twig | 88 +++++++++ templates/legal/rgpd.html.twig | 152 ++++++++++++++- tests/Controller/CspReportControllerTest.php | 25 +++ .../EmailTrackingControllerTest.php | 32 +++- tests/Controller/HomeControllerTest.php | 8 + tests/Controller/LegalControllerTest.php | 56 ++++++ .../Controller/RegistrationControllerTest.php | 21 +++ tests/Controller/SecurityControllerTest.php | 59 ++++++ tests/Controller/SitemapControllerTest.php | 10 + .../MessengerFailureSubscriberTest.php | 48 ++++- .../Repository/MessengerLogRepositoryTest.php | 40 ++++ tests/Repository/UserRepositoryTest.php | 52 ++++++ tests/Service/MailerServiceTest.php | 77 +++++++- tests/Service/MeilisearchServiceTest.php | 80 +++++++- tests/Service/UnsubscribeManagerTest.php | 14 ++ 30 files changed, 1631 insertions(+), 39 deletions(-) create mode 100644 public/logo.jpg create mode 100644 templates/home/tarifs.html.twig create mode 100644 tests/Controller/LegalControllerTest.php create mode 100644 tests/Repository/MessengerLogRepositoryTest.php create mode 100644 tests/Repository/UserRepositoryTest.php diff --git a/.gitignore b/.gitignore index d5e4ff6..32be47f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ node_modules/ .idea/ cert/ +/public/media/ ###> friendsofphp/php-cs-fixer ### /.php-cs-fixer.php diff --git a/Makefile b/Makefile index 83eac10..059b17f 100644 --- a/Makefile +++ b/Makefile @@ -55,6 +55,12 @@ clear_prod: ## Clear le cache Symfony et le pool opcache en prod via Docker docker compose -f docker-compose-prod.yml exec php php bin/console cache:clear --env=prod docker compose -f docker-compose-prod.yml exec php php bin/console cache:pool:clear --all --env=prod +purge_liip_dev: ## Purge le cache Liip Imagine en dev via Docker + docker compose -f docker-compose-dev.yml exec php php bin/console liip:imagine:cache:remove + +purge_liip_prod: ## Purge le cache Liip Imagine en prod via Docker + docker compose -f docker-compose-prod.yml exec php php bin/console liip:imagine:cache:remove --env=prod + ## —— Maintenance —————————————————————————————————— maintenance_on: ## Active le mode maintenance touch public/.update diff --git a/assets/app.js b/assets/app.js index 52a0661..f48b6ae 100644 --- a/assets/app.js +++ b/assets/app.js @@ -1 +1,30 @@ import "./app.scss" + +document.addEventListener('DOMContentLoaded', () => { + const btn = document.getElementById('mobile-menu-btn') + const menu = document.getElementById('mobile-menu') + const iconOpen = document.getElementById('menu-icon-open') + const iconClose = document.getElementById('menu-icon-close') + + if (btn && menu) { + btn.addEventListener('click', () => { + const isOpen = !menu.classList.contains('hidden') + menu.classList.toggle('hidden') + iconOpen.classList.toggle('hidden') + iconClose.classList.toggle('hidden') + btn.setAttribute('aria-expanded', String(!isOpen)) + }) + } + + document.querySelectorAll('[data-tab]').forEach(button => { + button.addEventListener('click', () => { + const targetId = button.dataset.tab + document.querySelectorAll('[data-tab]').forEach(b => { + const isActive = b.dataset.tab === targetId + b.style.backgroundColor = isActive ? '#111827' : 'white' + b.style.color = isActive ? 'white' : '#111827' + document.getElementById(b.dataset.tab).style.display = isActive ? 'block' : 'none' + }) + }) + }) +}) diff --git a/assets/app.scss b/assets/app.scss index 7dff76f..e7d9b4c 100644 --- a/assets/app.scss +++ b/assets/app.scss @@ -1,3 +1,2 @@ -@use "tailwindcss"; +@import "tailwindcss"; @import 'https://fonts.googleapis.com/css2?family=Intel+One+Mono:ital,wght@0,300..700;1,300..700&display=swap'; - diff --git a/config/packages/liip_imagine.yaml b/config/packages/liip_imagine.yaml index 1b2b6a0..ff7abdd 100644 --- a/config/packages/liip_imagine.yaml +++ b/config/packages/liip_imagine.yaml @@ -3,39 +3,32 @@ liip_imagine: twig: mode: lazy - loaders: - flysystem_loader: - flysystem: - filesystem_service: default.storage - - data_loader: flysystem_loader - - resolvers: - flysystem_resolver: - flysystem: - filesystem_service: default.storage - root_url: '%env(S3_ENDPOINT)%/%env(S3_BUCKET)%' - cache_prefix: cache - - cache: flysystem_resolver - webp: generate: true quality: 80 filter_sets: + navbar_logo: + quality: 85 + format: webp + filters: + thumbnail: { size: [200, 72], mode: inset } + thumbnail: quality: 80 + format: webp filters: thumbnail: { size: [300, 300], mode: inset } background: { size: [300, 300], position: center, color: '#ffffff' } medium: quality: 85 + format: webp filters: thumbnail: { size: [600, 600], mode: inset } large: quality: 90 + format: webp filters: thumbnail: { size: [1200, 1200], mode: inset } diff --git a/public/logo.jpg b/public/logo.jpg new file mode 100644 index 0000000..5354318 --- /dev/null +++ b/public/logo.jpg @@ -0,0 +1 @@ +fake-image \ No newline at end of file diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index 86d376b..62dd6a2 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -17,4 +17,15 @@ class HomeController extends AbstractController ], ]); } + + #[Route('/tarifs', name: 'app_tarifs')] + public function tarifs(): Response + { + return $this->render('home/tarifs.html.twig', [ + 'breadcrumbs' => [ + ['name' => 'Accueil', 'url' => '/'], + ['name' => 'Tarifs', 'url' => '/tarifs'], + ], + ]); + } } diff --git a/src/Controller/SitemapController.php b/src/Controller/SitemapController.php index 4cb931e..1028bf0 100644 --- a/src/Controller/SitemapController.php +++ b/src/Controller/SitemapController.php @@ -21,12 +21,20 @@ class SitemapController extends AbstractController ['loc' => $this->generateUrl('app_sitemap_main', [], UrlGeneratorInterface::ABSOLUTE_URL)], ]; + $orgaPages = max(1, (int) ceil(0 / self::MAX_URLS_PER_SITEMAP)); + for ($i = 1; $i <= $eventPages; ++$i) { $sitemaps[] = [ 'loc' => $this->generateUrl('app_sitemap_events', ['page' => $i], UrlGeneratorInterface::ABSOLUTE_URL), ]; } + for ($i = 1; $i <= $orgaPages; ++$i) { + $sitemaps[] = [ + 'loc' => $this->generateUrl('app_sitemap_orgas', ['page' => $i], UrlGeneratorInterface::ABSOLUTE_URL), + ]; + } + return new Response( $this->renderView('sitemap/index.xml.twig', ['sitemaps' => $sitemaps]), 200, @@ -48,6 +56,11 @@ class SitemapController extends AbstractController 'changefreq' => 'weekly', 'priority' => '0.5', ], + [ + 'loc' => $this->generateUrl('app_tarifs', [], UrlGeneratorInterface::ABSOLUTE_URL), + 'changefreq' => 'monthly', + 'priority' => '0.7', + ], ]; return new Response( @@ -68,4 +81,16 @@ class SitemapController extends AbstractController ['Content-Type' => self::CONTENT_TYPE_XML], ); } + + #[Route('/sitemap-orgas-{page}.xml', name: 'app_sitemap_orgas', requirements: ['page' => '\d+'], methods: ['GET'])] + public function orgas(int $page = 1): Response + { + $urls = []; + + return new Response( + $this->renderView('sitemap/urlset.xml.twig', ['urls' => $urls]), + 200, + ['Content-Type' => self::CONTENT_TYPE_XML], + ); + } } diff --git a/templates/base.html.twig b/templates/base.html.twig index c37c4f3..1fd4d27 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -68,7 +68,101 @@ {{ vite_asset('app.js') }} {% endblock %} - - {% block body %}{% endblock %} + +
+ + + +
+ +
+ {% block body %}{% endblock %} +
+ + diff --git a/templates/home/index.html.twig b/templates/home/index.html.twig index 1457891..13f1013 100644 --- a/templates/home/index.html.twig +++ b/templates/home/index.html.twig @@ -1,7 +1,110 @@ {% extends 'base.html.twig' %} -{% block title %}Accueil - E-Ticket{% endblock %} +{% block title %}E-Ticket - Billetterie en ligne pour associations{% endblock %} +{% block description %}E-Ticket, plateforme de billetterie en ligne pour associations : vente de tickets, reservation de tables, brocantes et vote en ligne.{% endblock %} {% block body %} +
+
+

+ La billetterie pensee pour les associations +

+

+ Vendez vos billets, gerez vos evenements et simplifiez votre organisation. Simple, securise, transparent. +

+
+ Creer mon evenement + Trouver un evenement +
+
+
+ +
+
+

Comment ca marche ?

+
+ +
+

Pour les acheteurs

+
+
+

1

+

Choisissez

+

Parcourez les evenements et selectionnez vos billets.

+
+
+

2

+

Payez en ligne

+

Paiement securise par carte bancaire via Stripe.

+
+
+

3

+

Recevez votre billet

+

Billet electronique avec QR Code envoye par email signe.

+
+
+
+ +
+

Pour les organisateurs

+
+
+

1

+

Creez votre evenement

+

Configurez vos billets, tarifs et nombre de places en quelques minutes.

+
+
+

2

+

Vendez en ligne

+

Partagez le lien de votre evenement. Les paiements sont automatiques.

+
+
+

3

+

Scannez a l'entree

+

Validez les billets QR Code depuis n'importe quel smartphone.

+
+
+
+
+ +
+
+
+

Pourquoi E-Ticket ?

+
+
+
+

🔒

+

Securise

+

Paiement Stripe, HTTPS, emails signes S/MIME, protection Cloudflare.

+
+
+

💰

+

Transparent

+

Commission claire a 3%, negociable. Aucun frais cache.

+
+
+

+

Simple

+

Creez un evenement en 5 minutes. Aucune competence technique requise.

+
+
+

🎫

+

Fait pour les assos

+

Billets, brocantes, tables, votes. Tout ce dont votre association a besoin.

+
+
+
+
+ +
+

Pret a lancer votre evenement ?

+

Rejoignez E-Ticket et commencez a vendre vos billets des aujourd'hui.

+
+ Creer un compte organisateur + Voir les tarifs +
+
+ {% endblock %} diff --git a/templates/home/tarifs.html.twig b/templates/home/tarifs.html.twig new file mode 100644 index 0000000..77c5592 --- /dev/null +++ b/templates/home/tarifs.html.twig @@ -0,0 +1,147 @@ +{% extends 'base.html.twig' %} + +{% block title %}Tarifs - E-Ticket{% endblock %} +{% block description %}Tarifs et commissions de la plateforme E-Ticket pour les organisateurs d'evenements{% endblock %} + +{% block stylesheets %} + +{% endblock %} + +{% block body %} +
+

Tarifs

+

Transparence totale sur nos commissions et abonnements.

+ +

Commission par transaction

+
+ +
+

Commission plateforme

+

3%

+

du montant de chaque billet vendu

+

Negociable selon votre profil, contactez-nous.

+

+ frais Stripe (1.5% + 0.25€ par transaction, non negociables)

+
+ +
+

Billets gratuits

+

0€

+

aucune commission sur les billets gratuits

+
+ +
+ +

Abonnements organisateur

+
+ +
+
+

Gratuit

+
+

0€/mois

+

Sur demande, apres approbation par la plateforme

+
    +
  • ✓ 5 evenements simultanement
  • +
  • ✓ Billets illimites
  • +
  • ✓ QR Code securise
  • +
  • ✓ Paiement Stripe
  • +
  • ✓ Email de confirmation
  • +
+ Faire une demande +
+ +
+
+

Basique

+
+

10€/mois

+
    +
  • ✓ Evenements illimites
  • +
  • ✓ Billets illimites
  • +
  • ✓ Commission reduite a 1.5%
  • +
  • ✓ Page organisateur personnalisee
  • +
+
+ +
+
+

Sur-mesure

+
+

Sur devis

+

Tarif personnalise selon vos besoins

+
    +
  • ✓ Evenements illimites
  • +
  • ✓ Billets illimites
  • +
  • ✓ Commission a partir de 0.5%
  • +
  • ✓ Page organisateur personnalisee
  • +
  • ✓ Reservation de tables
  • +
  • ✓ Gestion de brocantes
  • +
  • ✓ Billets personnalisables
  • +
  • ✓ Accompagnement dedie
  • +
+ Nous contacter +
+ +
+ +
+

Inclus dans toutes les formules

+
    +
  • Paiement securise via Stripe
  • +
  • Billets electroniques avec QR Code
  • +
  • Emails signes S/MIME
  • +
  • Scan des billets a l'entree
  • +
  • Conformite RGPD
  • +
  • HTTPS et protection Cloudflare
  • +
+
+ +

Les tarifs sont exprimes en euros HT. Contact : contact@e-cosplay.fr

+
+{% endblock %} diff --git a/templates/legal/cgu.html.twig b/templates/legal/cgu.html.twig index 631e9ca..486f009 100644 --- a/templates/legal/cgu.html.twig +++ b/templates/legal/cgu.html.twig @@ -1,7 +1,180 @@ {% extends 'base.html.twig' %} {% block title %}CGU - E-Ticket{% endblock %} +{% block description %}Conditions Generales d'Utilisation de la plateforme E-Ticket{% endblock %} {% block body %} +
+

Conditions Generales d'Utilisation

+
+ + +
+ +
+
+ +
+

1. Objet

+

Les presentes Conditions Generales d'Utilisation (CGU) regissent l'utilisation de la plateforme E-Ticket (ticket.e-cosplay.fr) par les acheteurs de billets. En achetant un billet, vous acceptez sans reserve les presentes CGU.

+
+ +
+

2. Inscription

+

L'achat de billets necessite la creation d'un compte utilisateur. Vous vous engagez a fournir des informations exactes et a jour. Vous etes responsable de la confidentialite de vos identifiants de connexion.

+
+ +
+

3. Achat de billets

+

L'achat d'un billet constitue un contrat directement entre l'acheteur et l'organisateur de l'evenement. La Plateforme E-Ticket agit uniquement en tant qu'intermediaire technique facilitant la mise en relation et le paiement.

+

Le prix des billets est fixe par l'organisateur. Une commission de service peut etre appliquee par la Plateforme, dont le montant est indique avant la validation du paiement.

+
+ +
+

4. Paiement

+

Les paiements sont securises par Stripe. La Plateforme ne stocke aucune donnee bancaire. Le paiement est debite immediatement a la validation de la commande.

+
+ +
+

5. Billets et QR Code

+

Apres validation du paiement, un billet electronique comportant un QR Code unique est envoye par email. Ce billet est personnel et ne peut etre revendu. Toute duplication ou falsification est interdite et pourra donner lieu a des poursuites.

+
+ +
+

6. Annulation et remboursement

+

L'organisateur est seul responsable de la politique d'annulation et de remboursement de ses evenements. En cas d'annulation d'un evenement :

+
    +
  • L'acheteur doit contacter directement l'organisateur pour toute demande de remboursement
  • +
  • La Plateforme E-Ticket n'est pas responsable des remboursements
  • +
  • La commission de service de la Plateforme n'est pas remboursable
  • +
+

Conformement a l'article L221-28 du Code de la consommation, le droit de retractation ne s'applique pas aux prestations de services de loisirs devant etre fournis a une date determinee.

+
+ +
+

7. Responsabilite de la Plateforme

+

La Plateforme E-Ticket decline toute responsabilite concernant :

+
    +
  • L'organisation, le deroulement ou l'annulation des evenements
  • +
  • La qualite ou la securite des evenements
  • +
  • Les litiges entre acheteurs et organisateurs
  • +
  • Les dommages directs ou indirects lies a la participation a un evenement
  • +
+
+ +
+

8. Donnees personnelles

+

Vos donnees sont traitees conformement a notre Politique de confidentialite. Vos donnees (nom, email) sont transmises a l'organisateur de l'evenement pour la gestion de votre billet.

+
+ +
+

9. Comportement

+

L'acheteur s'engage a :

+
    +
  • Ne pas utiliser la Plateforme a des fins frauduleuses
  • +
  • Ne pas revendre les billets achetes
  • +
  • Ne pas perturber le fonctionnement de la Plateforme
  • +
  • Respecter le reglement interieur de l'evenement
  • +
+
+ +
+

10. Droit applicable

+

Les presentes CGU sont soumises au droit francais. Tout litige sera soumis aux tribunaux competents de Laon.

+
+
+
+ + + +

Derniere mise a jour : {{ "now"|date("d/m/Y") }}

+
{% endblock %} diff --git a/templates/legal/cgv.html.twig b/templates/legal/cgv.html.twig index eb71e9c..6e7b09a 100644 --- a/templates/legal/cgv.html.twig +++ b/templates/legal/cgv.html.twig @@ -1,7 +1,137 @@ {% extends 'base.html.twig' %} {% block title %}CGV - E-Ticket{% endblock %} +{% block description %}Conditions Generales de Vente de la plateforme E-Ticket{% endblock %} {% block body %} +
+

Conditions Generales de Vente

+
+ +
+

1. Objet

+

Les presentes Conditions Generales de Vente (CGV) regissent les transactions effectuees sur la plateforme E-Ticket (ticket.e-cosplay.fr), editee par l'association E-Cosplay (SIREN 943121517). Elles s'appliquent a toute vente de billets realisee via la Plateforme.

+
+ +
+

2. Role de la Plateforme

+

La Plateforme E-Ticket agit en qualite d'intermediaire technique entre les organisateurs d'evenements et les acheteurs de billets. La Plateforme :

+
    +
  • Met a disposition un outil de creation et de vente de billets
  • +
  • Assure le traitement securise des paiements via Stripe
  • +
  • Genere et envoie les billets electroniques (QR Code)
  • +
  • N'est pas partie au contrat de vente conclu entre l'organisateur et l'acheteur
  • +
+
+ +
+

3. Prix et commission

+

Les prix des billets sont fixes librement par chaque organisateur et affiches en euros (EUR) toutes taxes comprises.

+

Une commission de service est appliquee par la Plateforme sur chaque transaction. Le montant de cette commission est clairement indique a l'acheteur avant la validation du paiement et a l'organisateur lors de la creation de l'evenement.

+

La commission comprend :

+
    +
  • Les frais de fonctionnement de la Plateforme
  • +
  • Les frais de traitement du paiement (Stripe)
  • +
  • La generation et l'envoi du billet electronique
  • +
+
+ +
+

4. Processus d'achat

+
    +
  • L'acheteur selectionne un evenement et le nombre de billets souhaites
  • +
  • Le recapitulatif de la commande (prix, commission, total) est affiche avant validation
  • +
  • Le paiement est effectue en ligne via Stripe (carte bancaire)
  • +
  • Un email de confirmation contenant le(s) billet(s) avec QR Code est envoye apres validation du paiement
  • +
  • La commande est consideree comme definitive des la confirmation du paiement
  • +
+
+ +
+

5. Billets electroniques

+

Chaque billet est unique et comporte un QR Code securise. Le billet est :

+
    +
  • Personnel et nominatif
  • +
  • Non cessible et non revendable
  • +
  • Valable uniquement pour l'evenement et la date indiques
  • +
  • Signe electroniquement (S/MIME) pour garantir son authenticite
  • +
+

Toute falsification, duplication ou revente de billets est interdite et passible de poursuites judiciaires.

+
+ +
+

6. Annulation, remboursement et droit de retractation

+

Conformement a l'article L221-28 12° du Code de la consommation, le droit de retractation ne s'applique pas aux prestations de services de loisirs devant etre fournies a une date determinee.

+ +

En cas d'annulation par l'organisateur :

+
    +
  • L'organisateur est seul responsable du remboursement des acheteurs
  • +
  • Le remboursement doit intervenir dans un delai de 30 jours suivant l'annulation
  • +
  • La commission de la Plateforme n'est pas remboursable, sauf accord contraire
  • +
+ +

En cas de modification par l'organisateur (changement de date, lieu, programme) :

+
    +
  • L'organisateur doit informer les acheteurs dans les meilleurs delais
  • +
  • L'acheteur peut demander un remboursement a l'organisateur si la modification est substantielle
  • +
+
+ +
+

7. Reversement aux organisateurs

+

Les fonds collectes sont reverses aux organisateurs via Stripe Connect :

+
    +
  • Apres la tenue de l'evenement, ou selon le calendrier convenu
  • +
  • Apres deduction de la commission de la Plateforme et des frais Stripe
  • +
  • La Plateforme se reserve le droit de retenir les fonds en cas de litige, fraude ou reclamation
  • +
+
+ +
+

8. CGV specifiques des organisateurs

+

Chaque organisateur peut definir ses propres conditions de vente specifiques a ses evenements (politique de remboursement, conditions d'acces, reglement interieur, etc.).

+

Les conditions specifiques de l'organisateur ne doivent en aucun cas etre en conflit avec les presentes CGV de la Plateforme. En cas de contradiction, les presentes CGV prevalent.

+

Les conditions specifiques de l'organisateur sont affichees sur la page de l'evenement et doivent etre acceptees par l'acheteur avant l'achat. L'organisateur est seul responsable du contenu et de la legalite de ses conditions specifiques.

+
+ +
+

9. Responsabilite

+

La Plateforme E-Ticket, en tant qu'intermediaire technique :

+
    +
  • N'est pas responsable de l'organisation, du deroulement ou de l'annulation des evenements
  • +
  • N'est pas responsable des litiges entre organisateurs et acheteurs
  • +
  • N'est pas responsable des dommages directs ou indirects lies a un evenement
  • +
  • S'engage a assurer la disponibilite et la securite de la Plateforme dans la mesure du possible
  • +
+

L'organisateur est seul responsable de ses evenements et de ses obligations envers les acheteurs.

+
+ +
+

10. Propriete intellectuelle

+

L'ensemble des elements de la Plateforme (code, design, logo, textes) est la propriete exclusive de l'association E-Cosplay. Les contenus publies par les organisateurs restent leur propriete.

+
+ +
+

11. Donnees personnelles

+

Les donnees personnelles collectees dans le cadre des ventes sont traitees conformement a notre Politique de confidentialite et au RGPD.

+
+ +
+

12. Mediation

+

En cas de litige, l'acheteur peut recourir gratuitement au service de mediation suivant :

+
    +
  • Plateforme de reglement en ligne des litiges de la Commission Europeenne : ec.europa.eu/consumers/odr
  • +
+

Avant toute mediation, l'acheteur est invite a contacter la Plateforme a l'adresse contact@e-cosplay.fr.

+
+ +
+

13. Droit applicable

+

Les presentes CGV sont soumises au droit francais. Tout litige sera soumis aux tribunaux competents de Laon.

+
+ +

Derniere mise a jour : {{ "now"|date("d/m/Y") }}

+
+
{% endblock %} diff --git a/templates/legal/cookies.html.twig b/templates/legal/cookies.html.twig index e7a0a99..0fb6b9e 100644 --- a/templates/legal/cookies.html.twig +++ b/templates/legal/cookies.html.twig @@ -1,7 +1,86 @@ {% extends 'base.html.twig' %} -{% block title %}Cookies - E-Ticket{% endblock %} +{% block title %}Politique de Cookies - E-Ticket{% endblock %} +{% block description %}Politique de cookies de la plateforme E-Ticket{% endblock %} {% block body %} +
+

Politique de Cookies

+
+ +
+

1. Qu'est-ce qu'un cookie ?

+

Un cookie est un petit fichier texte depose sur votre terminal (ordinateur, tablette, smartphone) lors de la visite d'un site web. Il permet au site de memoriser des informations relatives a votre navigation (preferences, session, etc.).

+
+ +
+

2. Cookies utilises sur la Plateforme

+

La plateforme E-Ticket utilise exclusivement des cookies strictement necessaires au fonctionnement du site :

+
    +
  • Cookie de session : permet de maintenir votre connexion et votre navigation sur la Plateforme. Il est supprime a la fermeture du navigateur.
  • +
  • Cookie de securite (CSRF) : protege contre les attaques de type Cross-Site Request Forgery lors de la soumission de formulaires.
  • +
  • Cookie de preference : memorise vos choix (langue, theme) pour ameliorer votre experience utilisateur.
  • +
+
+ +
+

3. Cookies tiers

+

La Plateforme peut integrer des services tiers qui deposent leurs propres cookies :

+
    +
  • Stripe : pour le traitement securise des paiements. Ces cookies sont necessaires au fonctionnement du module de paiement.
  • +
  • Cloudflare : pour la securite et la performance du site (protection DDoS, CDN). Ces cookies sont strictement techniques.
  • +
+

La Plateforme n'utilise aucun cookie publicitaire, de tracking ou d'analyse comportementale (pas de Google Analytics, Facebook Pixel, etc.).

+
+ +
+

4. Duree de conservation

+
    +
  • Cookies de session : supprimes a la fermeture du navigateur
  • +
  • Cookies de securite (Cloudflare) : duree maximale de 24 heures
  • +
  • Cookies Stripe : selon la politique de Stripe, generalement le temps de la transaction
  • +
+
+ +
+

5. Gestion des cookies

+

Etant donne que la Plateforme utilise uniquement des cookies strictement necessaires, aucun consentement prealable n'est requis conformement a l'article 82 de la loi Informatique et Libertes et aux recommandations de la CNIL.

+

Vous pouvez toutefois configurer votre navigateur pour refuser les cookies. Veuillez noter que la desactivation des cookies necessaires peut empecher le bon fonctionnement de la Plateforme (connexion, paiement, etc.).

+

Pour configurer les cookies dans votre navigateur :

+
    +
  • Chrome : Parametres > Confidentialite et securite > Cookies
  • +
  • Firefox : Parametres > Vie privee et securite > Cookies
  • +
  • Safari : Preferences > Confidentialite > Cookies
  • +
  • Edge : Parametres > Cookies et autorisations de site
  • +
+
+ +
+

6. Base legale

+

Le depot de cookies strictement necessaires repose sur l'interet legitime de l'editeur a assurer le fonctionnement et la securite de la Plateforme, conformement a :

+
    +
  • Article 82 de la loi n°78-17 du 6 janvier 1978 (Informatique et Libertes)
  • +
  • Directive 2002/58/CE (directive ePrivacy)
  • +
  • Recommandations de la CNIL sur les cookies et traceurs
  • +
+
+ +
+

7. Delegue a la Protection des Donnees

+

Pour toute question relative aux cookies ou a vos donnees personnelles :

+ +
+ +
+

8. Droit applicable

+

Tout litige en relation avec l'utilisation des cookies sur la Plateforme est soumis au droit francais. Il est fait attribution exclusive de juridiction aux tribunaux competents de Laon.

+
+ +

Derniere mise a jour : {{ "now"|date("d/m/Y") }}

+
+
{% endblock %} diff --git a/templates/legal/hosting.html.twig b/templates/legal/hosting.html.twig index 6f73184..c38606b 100644 --- a/templates/legal/hosting.html.twig +++ b/templates/legal/hosting.html.twig @@ -1,7 +1,80 @@ {% extends 'base.html.twig' %} {% block title %}Hebergement - E-Ticket{% endblock %} +{% block description %}Informations sur l'hebergement de la plateforme E-Ticket{% endblock %} {% block body %} +
+

Hebergement

+
+ +
+

1. Hebergeur principal

+

Le site ticket.e-cosplay.fr est heberge par :

+
    +
  • Google Cloud Platform (GCP)
  • +
  • Google Ireland Limited
  • +
  • Gordon House, Barrow Street, Dublin 4, Irlande
  • +
  • Region : europe-west1 (Belgique)
  • +
  • Site : cloud.google.com
  • +
+
+ +
+

2. CDN et securite

+
    +
  • Cloudflare, Inc.
  • +
  • 101 Townsend St, San Francisco, CA 94107, Etats-Unis
  • +
  • Services : CDN, protection DDoS, WAF, gestion DNS, certificats TLS
  • +
  • Site : cloudflare.com
  • +
+
+ +
+

3. Stockage des fichiers

+
    +
  • Stockage objet compatible S3
  • +
  • Heberge sur infrastructure ESY-WEB (s3.esy-web.dev)
  • +
  • Localisation : Europe
  • +
  • Utilisation : stockage des images, documents et fichiers uploades
  • +
+
+ +
+

4. Service d'envoi d'emails

+
    +
  • Amazon Simple Email Service (SES)
  • +
  • Amazon Web Services EMEA SARL
  • +
  • 38 Avenue John F. Kennedy, L-1855 Luxembourg
  • +
  • Region : eu-west-3 (Paris)
  • +
  • Utilisation : envoi des emails transactionnels (billets, confirmations, notifications)
  • +
+
+ +
+

5. Service de paiement

+
    +
  • Stripe Payments Europe, Ltd.
  • +
  • 1 Grand Canal Street Lower, Grand Canal Dock, Dublin 2, Irlande
  • +
  • Utilisation : traitement des paiements en ligne, gestion des remboursements
  • +
  • Certification : PCI DSS Level 1
  • +
  • Site : stripe.com
  • +
+
+ +
+

6. Localisation des donnees

+

L'ensemble des donnees de la Plateforme (base de donnees, fichiers, sauvegardes) est heberge dans l'Union Europeenne.

+

Les sous-traitants americains (Cloudflare, Stripe) operent sous le cadre du Data Privacy Framework (DPF) et/ou des Clauses Contractuelles Types (CCT) pour les transferts de donnees hors UE.

+
+ +
+

7. Contact

+

Pour toute question relative a l'hebergement : contact@e-cosplay.fr

+
+ +

Derniere mise a jour : {{ "now"|date("d/m/Y") }}

+
+
{% endblock %} diff --git a/templates/legal/mentions_legales.html.twig b/templates/legal/mentions_legales.html.twig index 1709597..923b573 100644 --- a/templates/legal/mentions_legales.html.twig +++ b/templates/legal/mentions_legales.html.twig @@ -1,7 +1,95 @@ {% extends 'base.html.twig' %} {% block title %}Mentions legales - E-Ticket{% endblock %} +{% block description %}Mentions legales de la plateforme E-Ticket{% endblock %} {% block body %} +
+

Mentions Legales

+
+ +
+

1. Editeur du site

+

Le site ticket.e-cosplay.fr (ci-apres "la Plateforme") est edite par :

+
    +
  • Association E-Cosplay
  • +
  • RNA : W022006988
  • +
  • SIREN : 943121517
  • +
  • Siege social : 42 rue de Saint-Quentin, 02800 Beautor, France
  • +
  • Email : contact@e-cosplay.fr
  • +
  • Telephone : 06 79 34 88 02
  • +
  • Directeur de la publication : Serreau Jovann
  • +
+
+ +
+

2. Hebergement

+

Le site est heberge par :

+
    +
  • Google Cloud Platform
  • +
  • Google Ireland Limited, Gordon House, Barrow Street, Dublin 4, Irlande
  • +
+

Le nom de domaine est gere via Cloudflare, Inc., 101 Townsend St, San Francisco, CA 94107, Etats-Unis.

+
+ +
+

3. Nature de la Plateforme

+

La Plateforme E-Ticket est un intermediaire technique qui met a disposition des organisateurs d'evenements (associations, collectifs, particuliers) un outil de creation et de vente de billets en ligne.

+

La Plateforme n'organise aucun evenement et n'est pas partie prenante aux transactions effectuees entre les organisateurs et les acheteurs de billets.

+
+ +
+

4. Responsabilite

+

L'association E-Cosplay, en tant qu'editeur de la Plateforme, decline toute responsabilite concernant :

+
    +
  • L'organisation, le deroulement, l'annulation ou la modification des evenements publies sur la Plateforme
  • +
  • La qualite, la conformite ou la securite des evenements proposes par les organisateurs
  • +
  • Les litiges commerciaux entre les organisateurs et les acheteurs (remboursements, reclamations, etc.)
  • +
  • Les informations publiees par les organisateurs sur leurs evenements (descriptions, dates, tarifs, etc.)
  • +
  • Les dommages directs ou indirects resultant de l'utilisation de la Plateforme ou de la participation a un evenement
  • +
+

Chaque organisateur est seul responsable de son evenement, de la vente de ses billets, du respect de la reglementation applicable (securite, assurance, droits d'auteur, etc.) et de ses obligations envers les acheteurs, notamment en matiere de remboursement.

+
+ +
+

5. Paiement

+

Les paiements en ligne sont securises par Stripe (Stripe Payments Europe, Ltd., 1 Grand Canal Street Lower, Dublin 2, Irlande). La Plateforme ne stocke aucune donnee bancaire.

+

Les fonds collectes sont reverses directement aux organisateurs via Stripe Connect. L'association E-Cosplay peut percevoir une commission sur chaque transaction, dont le montant est indique dans les CGV.

+
+ +
+

6. Propriete intellectuelle

+

L'ensemble du contenu de la Plateforme (textes, graphismes, logos, icones, code source) est la propriete exclusive de l'association E-Cosplay, sauf mention contraire. Toute reproduction, meme partielle, est interdite sans autorisation ecrite prealable.

+

Les contenus publies par les organisateurs (textes, images, logos) restent leur propriete. En publiant sur la Plateforme, ils accordent a E-Cosplay une licence non exclusive d'utilisation a des fins d'affichage et de promotion sur la Plateforme.

+
+ +
+

7. Donnees personnelles

+

Conformement au Reglement General sur la Protection des Donnees (RGPD), l'association E-Cosplay s'engage a proteger la confidentialite des donnees personnelles collectees. Pour toute information ou exercice de vos droits Informatique et Libertes sur les traitements de donnees personnelles, vous pouvez contacter notre Delegue a la Protection des Donnees (DPO).

+ +

Pour plus d'informations, consultez notre Politique de confidentialite.

+
+ +
+

8. Cookies

+

La Plateforme utilise des cookies strictement necessaires a son fonctionnement. Pour plus de details, consultez notre Politique de cookies.

+
+ +
+

9. Droit applicable et litiges

+

Les presentes mentions legales sont regies par le droit francais. En cas de litige, les tribunaux competents de Laon (Aisne) seront seuls competents.

+
+ +
+

10. Contact

+

Pour toute question relative aux presentes mentions legales, vous pouvez nous contacter a l'adresse : contact@e-cosplay.fr.

+
+ +

Derniere mise a jour : {{ "now"|date("d/m/Y") }}

+
+
{% endblock %} diff --git a/templates/legal/rgpd.html.twig b/templates/legal/rgpd.html.twig index 1ebbd49..5f25753 100644 --- a/templates/legal/rgpd.html.twig +++ b/templates/legal/rgpd.html.twig @@ -1,7 +1,157 @@ {% extends 'base.html.twig' %} -{% block title %}RGPD - E-Ticket{% endblock %} +{% block title %}Politique RGPD - E-Ticket{% endblock %} +{% block description %}Politique de confidentialite et protection des donnees personnelles de la plateforme E-Ticket{% endblock %} {% block body %} +
+

Politique de Confidentialite

+
+ +
+

1. Responsable du traitement

+

Le responsable du traitement des donnees personnelles est :

+
    +
  • Association E-Cosplay
  • +
  • SIREN : 943121517 / RNA : W022006988
  • +
  • 42 rue de Saint-Quentin, 02800 Beautor, France
  • +
  • Email : contact@e-cosplay.fr
  • +
+
+ +
+

2. Delegue a la Protection des Donnees (DPO)

+

Conformement au RGPD, un Delegue a la Protection des Donnees a ete designe :

+ +
+ +
+

3. Donnees collectees

+

La Plateforme collecte les donnees suivantes :

+ +

Lors de la creation de compte :

+
    +
  • Nom et prenom
  • +
  • Adresse email
  • +
  • Mot de passe (stocke sous forme hashee)
  • +
+ +

Lors de l'achat de billets :

+
    +
  • Nom et prenom de l'acheteur
  • +
  • Adresse email
  • +
  • Donnees de paiement (traitees exclusivement par Stripe, non stockees sur nos serveurs)
  • +
+ +

Donnees techniques :

+
    +
  • Adresse IP (anonymisee via Cloudflare)
  • +
  • Cookies strictement necessaires (voir Politique de cookies)
  • +
+
+ +
+

4. Finalites du traitement

+
    +
  • Gestion des comptes utilisateurs
  • +
  • Traitement des commandes et emission des billets
  • +
  • Envoi des billets et confirmations par email
  • +
  • Communication liee aux evenements achetes (modifications, annulations)
  • +
  • Securite de la Plateforme et prevention des fraudes
  • +
  • Respect des obligations legales et reglementaires
  • +
+
+ +
+

5. Bases legales

+
    +
  • Execution du contrat : traitement des commandes, emission des billets, gestion du compte
  • +
  • Obligation legale : conservation des donnees de facturation
  • +
  • Interet legitime : securite de la Plateforme, prevention des fraudes
  • +
  • Consentement : envoi de communications commerciales (newsletter)
  • +
+
+ +
+

6. Destinataires des donnees

+

Vos donnees personnelles sont accessibles par :

+
    +
  • L'association E-Cosplay : administration de la Plateforme
  • +
  • Les organisateurs d'evenements : uniquement les donnees necessaires a la gestion de leurs evenements (nom, email de l'acheteur)
  • +
  • Stripe : traitement des paiements (Stripe Payments Europe, Ltd., Dublin, Irlande)
  • +
  • Amazon Web Services (SES) : envoi des emails transactionnels (region eu-west-3, Irlande)
  • +
  • Google Cloud Platform : hebergement des donnees (region Europe)
  • +
  • Cloudflare : securite et CDN
  • +
+

Aucune donnee n'est vendue ou cedee a des tiers a des fins commerciales ou publicitaires.

+
+ +
+

7. Transferts hors UE

+

Certains sous-traitants (Stripe, Cloudflare) peuvent transferer des donnees en dehors de l'Union Europeenne. Ces transferts sont encadres par :

+
    +
  • Les Clauses Contractuelles Types (CCT) de la Commission Europeenne
  • +
  • Le Data Privacy Framework (DPF) UE-US pour les entreprises certifiees
  • +
+
+ +
+

8. Duree de conservation

+
    +
  • Donnees de compte : conservees pendant la duree d'existence du compte, puis 3 ans apres la derniere activite
  • +
  • Donnees de transaction : 10 ans (obligation legale comptable)
  • +
  • Donnees de connexion (logs) : 12 mois
  • +
  • Donnees de prospection : 3 ans apres le dernier contact
  • +
+
+ +
+

9. Securite des donnees

+

L'association E-Cosplay met en oeuvre des mesures techniques et organisationnelles appropriees pour garantir la securite des donnees :

+
    +
  • Chiffrement des communications (TLS/HTTPS)
  • +
  • Hashage des mots de passe (bcrypt)
  • +
  • Signature S/MIME des emails
  • +
  • Protection DDoS et WAF via Cloudflare
  • +
  • Acces restreint aux donnees (principe du moindre privilege)
  • +
  • Sauvegardes regulieres et chiffrees
  • +
+
+ +
+

10. Vos droits

+

Conformement au RGPD, vous disposez des droits suivants :

+
    +
  • Droit d'acces : obtenir une copie de vos donnees personnelles
  • +
  • Droit de rectification : corriger des donnees inexactes ou incompletes
  • +
  • Droit a l'effacement : demander la suppression de vos donnees (sous reserve des obligations legales)
  • +
  • Droit a la limitation : limiter le traitement dans certains cas
  • +
  • Droit a la portabilite : recevoir vos donnees dans un format structure et lisible
  • +
  • Droit d'opposition : vous opposer au traitement de vos donnees pour des motifs legitimes
  • +
  • Droit de retrait du consentement : retirer votre consentement a tout moment (newsletter, etc.)
  • +
+

Pour exercer vos droits, contactez le DPO a l'adresse contact@e-cosplay.fr en precisant votre identite. Une reponse vous sera apportee dans un delai maximum de 30 jours.

+
+ +
+

11. Reclamation

+

Si vous estimez que le traitement de vos donnees personnelles constitue une violation du RGPD, vous avez le droit d'introduire une reclamation aupres de la Commission Nationale de l'Informatique et des Libertes (CNIL) :

+
    +
  • CNIL - 3 Place de Fontenoy, TSA 80715, 75334 Paris Cedex 07
  • +
  • Site : www.cnil.fr
  • +
+
+ +
+

12. Droit applicable

+

Tout litige en relation avec le traitement des donnees personnelles est soumis au droit francais. Il est fait attribution exclusive de juridiction aux tribunaux competents de Laon.

+
+ +

Derniere mise a jour : {{ "now"|date("d/m/Y") }}

+
+
{% endblock %} diff --git a/tests/Controller/CspReportControllerTest.php b/tests/Controller/CspReportControllerTest.php index 29cda9e..4d8e5fd 100644 --- a/tests/Controller/CspReportControllerTest.php +++ b/tests/Controller/CspReportControllerTest.php @@ -3,6 +3,7 @@ namespace App\Tests\Controller; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Symfony\Component\Mailer\MailerInterface; class CspReportControllerTest extends WebTestCase { @@ -53,4 +54,28 @@ class CspReportControllerTest extends WebTestCase self::assertResponseStatusCodeSame(204); } + + public function testRealViolationHandlesMailerFailure(): void + { + $client = static::createClient(); + + $mailer = $this->createMock(MailerInterface::class); + $mailer->method('send')->willThrowException(new \RuntimeException('SMTP down')); + static::getContainer()->set(MailerInterface::class, $mailer); + + $payload = json_encode([ + 'csp-report' => [ + 'source-file' => 'https://evil.com/script.js', + 'blocked-uri' => 'https://evil.com', + 'document-uri' => 'https://e-cosplay.fr/page', + 'violated-directive' => 'script-src', + ], + ]); + + $client->request('POST', '/my-csp-report', [], [], [ + 'CONTENT_TYPE' => 'application/json', + ], $payload); + + self::assertResponseStatusCodeSame(204); + } } diff --git a/tests/Controller/EmailTrackingControllerTest.php b/tests/Controller/EmailTrackingControllerTest.php index 8dd7c54..867974c 100644 --- a/tests/Controller/EmailTrackingControllerTest.php +++ b/tests/Controller/EmailTrackingControllerTest.php @@ -2,26 +2,48 @@ namespace App\Tests\Controller; +use App\Entity\EmailTracking; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class EmailTrackingControllerTest extends WebTestCase { - public function testTrackReturnsImageResponse(): void + private function ensureLogoExists(): void { - $client = static::createClient(); $projectDir = static::getContainer()->getParameter('kernel.project_dir'); $logoPath = $projectDir.'/public/logo.jpg'; if (!file_exists($logoPath)) { file_put_contents($logoPath, 'fake-image'); } + } + + public function testTrackReturnsImageResponse(): void + { + $client = static::createClient(); + $this->ensureLogoExists(); $client->request('GET', '/track/nonexistent-id/logo.jpg'); self::assertResponseIsSuccessful(); + } - if ('fake-image' === file_get_contents($logoPath)) { - unlink($logoPath); - } + public function testTrackMarksAsOpened(): void + { + $client = static::createClient(); + $this->ensureLogoExists(); + + $em = static::getContainer()->get(EntityManagerInterface::class); + $tracking = new EmailTracking('test-track-'.uniqid(), 'user@example.com', 'Test Subject'); + $em->persist($tracking); + $em->flush(); + + $client->request('GET', '/track/'.$tracking->getMessageId().'/logo.jpg'); + + self::assertResponseIsSuccessful(); + + $em->refresh($tracking); + self::assertSame('opened', $tracking->getState()); + self::assertNotNull($tracking->getOpenedAt()); } } diff --git a/tests/Controller/HomeControllerTest.php b/tests/Controller/HomeControllerTest.php index eb3cacd..81f958a 100644 --- a/tests/Controller/HomeControllerTest.php +++ b/tests/Controller/HomeControllerTest.php @@ -13,4 +13,12 @@ class HomeControllerTest extends WebTestCase self::assertResponseIsSuccessful(); } + + public function testTarifsReturnsSuccess(): void + { + $client = static::createClient(); + $client->request('GET', '/tarifs'); + + self::assertResponseIsSuccessful(); + } } diff --git a/tests/Controller/LegalControllerTest.php b/tests/Controller/LegalControllerTest.php new file mode 100644 index 0000000..ba37f14 --- /dev/null +++ b/tests/Controller/LegalControllerTest.php @@ -0,0 +1,56 @@ +request('GET', '/mentions-legales'); + + self::assertResponseIsSuccessful(); + } + + public function testCgu(): void + { + $client = static::createClient(); + $client->request('GET', '/cgu'); + + self::assertResponseIsSuccessful(); + } + + public function testCgv(): void + { + $client = static::createClient(); + $client->request('GET', '/cgv'); + + self::assertResponseIsSuccessful(); + } + + public function testHosting(): void + { + $client = static::createClient(); + $client->request('GET', '/hebergement'); + + self::assertResponseIsSuccessful(); + } + + public function testCookies(): void + { + $client = static::createClient(); + $client->request('GET', '/cookies'); + + self::assertResponseIsSuccessful(); + } + + public function testRgpd(): void + { + $client = static::createClient(); + $client->request('GET', '/rgpd'); + + self::assertResponseIsSuccessful(); + } +} diff --git a/tests/Controller/RegistrationControllerTest.php b/tests/Controller/RegistrationControllerTest.php index 352cd34..38b032a 100644 --- a/tests/Controller/RegistrationControllerTest.php +++ b/tests/Controller/RegistrationControllerTest.php @@ -2,10 +2,31 @@ namespace App\Tests\Controller; +use App\Entity\User; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class RegistrationControllerTest extends WebTestCase { + public function testRegistrationRedirectsWhenAuthenticated(): void + { + $client = static::createClient(); + $em = static::getContainer()->get(EntityManagerInterface::class); + + $user = new User(); + $user->setEmail('test-reg-auth-'.uniqid().'@example.com'); + $user->setFirstName('Test'); + $user->setLastName('User'); + $user->setPassword('$2y$13$hashed'); + $em->persist($user); + $em->flush(); + + $client->loginUser($user); + $client->request('GET', '/inscription'); + + self::assertResponseRedirects(); + } + public function testRegistrationPageReturnsSuccess(): void { $client = static::createClient(); diff --git a/tests/Controller/SecurityControllerTest.php b/tests/Controller/SecurityControllerTest.php index 09dfd05..2ae1d04 100644 --- a/tests/Controller/SecurityControllerTest.php +++ b/tests/Controller/SecurityControllerTest.php @@ -2,6 +2,8 @@ namespace App\Tests\Controller; +use App\Entity\User; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class SecurityControllerTest extends WebTestCase @@ -14,6 +16,47 @@ class SecurityControllerTest extends WebTestCase self::assertResponseIsSuccessful(); } + public function testLoginRedirectsWhenAuthenticated(): void + { + $client = static::createClient(); + $user = $this->createUser(); + + $client->loginUser($user); + $client->request('GET', '/connexion'); + + self::assertResponseRedirects(); + } + + public function testChangePasswordRedirectsWhenNotAuthenticated(): void + { + $client = static::createClient(); + $client->request('GET', '/mot-de-passe'); + + self::assertResponseRedirects(); + } + + public function testChangePasswordReturnsSuccessWhenAuthenticated(): void + { + $client = static::createClient(); + $user = $this->createUser(); + + $client->loginUser($user); + $client->request('GET', '/mot-de-passe'); + + self::assertResponseIsSuccessful(); + } + + public function testWellKnownChangePasswordWhenAuthenticated(): void + { + $client = static::createClient(); + $user = $this->createUser(); + + $client->loginUser($user); + $client->request('GET', '/.well-known/change-password'); + + self::assertResponseIsSuccessful(); + } + public function testLogoutThrowsLogicException(): void { $this->expectException(\LogicException::class); @@ -21,4 +64,20 @@ class SecurityControllerTest extends WebTestCase $controller = new \App\Controller\SecurityController(); $controller->logout(); } + + private function createUser(): User + { + $em = static::getContainer()->get(EntityManagerInterface::class); + + $user = new User(); + $user->setEmail('test-security-'.uniqid().'@example.com'); + $user->setFirstName('Test'); + $user->setLastName('User'); + $user->setPassword('$2y$13$hashed'); + + $em->persist($user); + $em->flush(); + + return $user; + } } diff --git a/tests/Controller/SitemapControllerTest.php b/tests/Controller/SitemapControllerTest.php index c68dffc..701eff8 100644 --- a/tests/Controller/SitemapControllerTest.php +++ b/tests/Controller/SitemapControllerTest.php @@ -22,6 +22,7 @@ class SitemapControllerTest extends WebTestCase self::assertResponseIsSuccessful(); self::assertStringContainsString('text/xml', $client->getResponse()->headers->get('Content-Type')); + self::assertStringContainsString('/tarifs', $client->getResponse()->getContent()); } public function testSitemapEventsReturnsXml(): void @@ -32,4 +33,13 @@ class SitemapControllerTest extends WebTestCase self::assertResponseIsSuccessful(); self::assertStringContainsString('text/xml', $client->getResponse()->headers->get('Content-Type')); } + + public function testSitemapOrgasReturnsXml(): void + { + $client = static::createClient(); + $client->request('GET', '/sitemap-orgas-1.xml'); + + self::assertResponseIsSuccessful(); + self::assertStringContainsString('text/xml', $client->getResponse()->headers->get('Content-Type')); + } } diff --git a/tests/EventSubscriber/MessengerFailureSubscriberTest.php b/tests/EventSubscriber/MessengerFailureSubscriberTest.php index 2704bea..e7da596 100644 --- a/tests/EventSubscriber/MessengerFailureSubscriberTest.php +++ b/tests/EventSubscriber/MessengerFailureSubscriberTest.php @@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Mailer\MailerInterface; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; +use Symfony\Component\Messenger\Stamp\RedeliveryStamp; class MessengerFailureSubscriberTest extends TestCase { @@ -39,6 +40,51 @@ class MessengerFailureSubscriberTest extends TestCase $subscriber->onMessageFailed($event); } + public function testOnMessageFailedWithRedeliveryStamp(): void + { + $em = $this->createMock(EntityManagerInterface::class); + $mailer = $this->createMock(MailerInterface::class); + + $em->expects(self::once())->method('persist'); + $em->expects(self::once())->method('flush'); + $mailer->expects(self::once())->method('send'); + + $subscriber = new MessengerFailureSubscriber($em, $mailer); + + $message = new \stdClass(); + $envelope = new Envelope($message, [new RedeliveryStamp(3)]); + $exception = new \RuntimeException('Retry failure'); + + $event = new WorkerMessageFailedEvent($envelope, 'async', $exception); + + $subscriber->onMessageFailed($event); + } + + public function testOnMessageFailedWithNonSerializableMessage(): void + { + $em = $this->createMock(EntityManagerInterface::class); + $mailer = $this->createMock(MailerInterface::class); + + $em->expects(self::once())->method('persist'); + $em->expects(self::once())->method('flush'); + $mailer->expects(self::once())->method('send'); + + $subscriber = new MessengerFailureSubscriber($em, $mailer); + + $message = new class () { + public function __serialize(): array + { + throw new \RuntimeException('not serializable'); + } + }; + $envelope = new Envelope($message); + $exception = new \RuntimeException('Test failure'); + + $event = new WorkerMessageFailedEvent($envelope, 'async', $exception); + + $subscriber->onMessageFailed($event); + } + public function testOnMessageFailedHandlesMailerException(): void { $em = $this->createMock(EntityManagerInterface::class); @@ -55,7 +101,7 @@ class MessengerFailureSubscriberTest extends TestCase $exception = new \RuntimeException('Test failure'); $event = new WorkerMessageFailedEvent($envelope, 'async', $exception); - $subscriber->onMessageFailed($event); + @$subscriber->onMessageFailed($event); self::assertTrue(true); } diff --git a/tests/Repository/MessengerLogRepositoryTest.php b/tests/Repository/MessengerLogRepositoryTest.php new file mode 100644 index 0000000..c9c460f --- /dev/null +++ b/tests/Repository/MessengerLogRepositoryTest.php @@ -0,0 +1,40 @@ +get(MessengerLogRepository::class); + + self::assertInstanceOf(MessengerLogRepository::class, $repository); + } + + public function testPersistAndFind(): void + { + self::bootKernel(); + $em = static::getContainer()->get('doctrine.orm.entity_manager'); + + $log = new MessengerLog( + messageClass: 'App\Message\TestMessage', + messageBody: 'serialized', + errorMessage: 'Test error', + stackTrace: 'trace', + transportName: 'async', + retryCount: 1, + ); + + $em->persist($log); + $em->flush(); + + $found = $em->getRepository(MessengerLog::class)->find($log->getId()); + self::assertNotNull($found); + self::assertSame('App\Message\TestMessage', $found->getMessageClass()); + } +} diff --git a/tests/Repository/UserRepositoryTest.php b/tests/Repository/UserRepositoryTest.php new file mode 100644 index 0000000..47f9912 --- /dev/null +++ b/tests/Repository/UserRepositoryTest.php @@ -0,0 +1,52 @@ +repository = static::getContainer()->get(UserRepository::class); + } + + public function testUpgradePasswordUpdatesUser(): void + { + $em = static::getContainer()->get('doctrine.orm.entity_manager'); + + $user = new User(); + $user->setEmail('test-upgrade-'.uniqid().'@example.com'); + $user->setFirstName('Test'); + $user->setLastName('User'); + $user->setPassword('old-hash'); + $em->persist($user); + $em->flush(); + + $this->repository->upgradePassword($user, 'new-hash'); + + $em->refresh($user); + self::assertSame('new-hash', $user->getPassword()); + } + + public function testUpgradePasswordThrowsForUnsupportedUser(): void + { + $this->expectException(UnsupportedUserException::class); + + $fakeUser = new class () implements PasswordAuthenticatedUserInterface { + public function getPassword(): ?string + { + return null; + } + }; + + $this->repository->upgradePassword($fakeUser, 'hash'); + } +} diff --git a/tests/Service/MailerServiceTest.php b/tests/Service/MailerServiceTest.php index 5f4145d..a0274d8 100644 --- a/tests/Service/MailerServiceTest.php +++ b/tests/Service/MailerServiceTest.php @@ -16,7 +16,7 @@ class MailerServiceTest extends TestCase private UnsubscribeManager $unsubscribeManager; private EntityManagerInterface $em; private UrlGeneratorInterface $urlGenerator; - private MailerService $service; + private string $projectDir; protected function setUp(): void { @@ -24,10 +24,27 @@ class MailerServiceTest extends TestCase $this->unsubscribeManager = $this->createMock(UnsubscribeManager::class); $this->em = $this->createMock(EntityManagerInterface::class); $this->urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $this->projectDir = sys_get_temp_dir().'/mailer_test_'.uniqid(); + mkdir($this->projectDir.'/public', 0o777, true); + mkdir($this->projectDir.'/config/cert', 0o777, true); + } - $this->service = new MailerService( + protected function tearDown(): void + { + @unlink($this->projectDir.'/public/key.asc'); + @unlink($this->projectDir.'/config/cert/certificate.pem'); + @unlink($this->projectDir.'/config/cert/private-key.pem'); + @rmdir($this->projectDir.'/config/cert'); + @rmdir($this->projectDir.'/config'); + @rmdir($this->projectDir.'/public'); + @rmdir($this->projectDir); + } + + private function createService(): MailerService + { + return new MailerService( $this->bus, - sys_get_temp_dir(), + $this->projectDir, 'passphrase', $this->urlGenerator, $this->unsubscribeManager, @@ -40,7 +57,7 @@ class MailerServiceTest extends TestCase $this->unsubscribeManager->method('isUnsubscribed')->willReturn(true); $this->bus->expects(self::never())->method('dispatch'); - $this->service->sendEmail('user@example.com', 'Subject', '

Body

'); + $this->createService()->sendEmail('user@example.com', 'Subject', '

Body

'); } public function testSendEmailDoesNotSkipWhitelistedAddress(): void @@ -51,7 +68,7 @@ class MailerServiceTest extends TestCase $this->em->expects(self::once())->method('flush'); $this->bus->expects(self::once())->method('dispatch')->willReturn(new Envelope(new \stdClass())); - $this->service->sendEmail('contact@e-cosplay.fr', 'Subject', '

Body

'); + $this->createService()->sendEmail('contact@e-cosplay.fr', 'Subject', '

Body

'); } public function testSendEmailDispatchesForNonUnsubscribedUser(): void @@ -63,15 +80,61 @@ class MailerServiceTest extends TestCase $this->em->expects(self::once())->method('flush'); $this->bus->expects(self::once())->method('dispatch')->willReturn(new Envelope(new \stdClass())); - $this->service->sendEmail('user@example.com', 'Test', '

Content

'); + $this->createService()->sendEmail('user@example.com', 'Test', '

Content

'); } public function testSendEmailWithoutUnsubscribeHeaders(): void { $this->urlGenerator->method('generate')->willReturn('https://example.com/url'); $this->em->expects(self::once())->method('persist'); + $this->em->expects(self::once())->method('flush'); $this->bus->expects(self::once())->method('dispatch')->willReturn(new Envelope(new \stdClass())); - $this->service->sendEmail('user@example.com', 'Test', '

Content

', withUnsubscribe: false); + $this->createService()->sendEmail('user@example.com', 'Test', '

Content

', withUnsubscribe: false); + } + + public function testSendEmailWithReplyTo(): void + { + $this->unsubscribeManager->method('isUnsubscribed')->willReturn(false); + $this->unsubscribeManager->method('generateToken')->willReturn('token'); + $this->urlGenerator->method('generate')->willReturn('https://example.com/url'); + $this->em->expects(self::once())->method('persist'); + $this->em->expects(self::once())->method('flush'); + $this->bus->expects(self::once())->method('dispatch')->willReturn(new Envelope(new \stdClass())); + + $this->createService()->sendEmail('user@example.com', 'Test', '

Content

', replyTo: 'reply@example.com'); + } + + public function testSendEmailWithAttachments(): void + { + $tmpFile = $this->projectDir.'/public/test.txt'; + file_put_contents($tmpFile, 'test content'); + + $this->unsubscribeManager->method('isUnsubscribed')->willReturn(false); + $this->unsubscribeManager->method('generateToken')->willReturn('token'); + $this->urlGenerator->method('generate')->willReturn('https://example.com/url'); + $this->em->expects(self::once())->method('persist'); + $this->em->expects(self::once())->method('flush'); + $this->bus->expects(self::once())->method('dispatch')->willReturn(new Envelope(new \stdClass())); + + $this->createService()->sendEmail('user@example.com', 'Test', '

Content

', attachments: [ + ['path' => $tmpFile, 'name' => 'test.txt'], + ]); + + @unlink($tmpFile); + } + + public function testSendAttachesPublicKey(): void + { + file_put_contents($this->projectDir.'/public/key.asc', 'fake-pgp-key'); + + $this->unsubscribeManager->method('isUnsubscribed')->willReturn(false); + $this->unsubscribeManager->method('generateToken')->willReturn('token'); + $this->urlGenerator->method('generate')->willReturn('https://example.com/url'); + $this->em->expects(self::once())->method('persist'); + $this->em->expects(self::once())->method('flush'); + $this->bus->expects(self::once())->method('dispatch')->willReturn(new Envelope(new \stdClass())); + + $this->createService()->sendEmail('user@example.com', 'Test', '

Content

'); } } diff --git a/tests/Service/MeilisearchServiceTest.php b/tests/Service/MeilisearchServiceTest.php index 5845d80..7e17ea9 100644 --- a/tests/Service/MeilisearchServiceTest.php +++ b/tests/Service/MeilisearchServiceTest.php @@ -44,6 +44,27 @@ class MeilisearchServiceTest extends TestCase self::assertFalse($this->service->indexExists('events')); } + public function testCreateIndexIfNotExistsCreatesWhenMissing(): void + { + $this->httpClient->method('request')->willThrowException(new \RuntimeException('not found')); + $this->bus->expects(self::once()) + ->method('dispatch') + ->with(self::callback(fn (MeilisearchMessage $m) => 'createIndex' === $m->action)) + ->willReturn(new Envelope(new \stdClass())); + + $this->service->createIndexIfNotExists('events'); + } + + public function testCreateIndexIfNotExistsSkipsWhenExists(): void + { + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode')->willReturn(200); + $this->httpClient->method('request')->willReturn($response); + $this->bus->expects(self::never())->method('dispatch'); + + $this->service->createIndexIfNotExists('events'); + } + public function testCreateIndexDispatchesMessage(): void { $this->bus->expects(self::once()) @@ -75,20 +96,73 @@ class MeilisearchServiceTest extends TestCase $this->service->addDocuments('events', $docs); } + public function testUpdateDocumentsDispatchesMessage(): void + { + $docs = [['id' => 1, 'title' => 'Updated']]; + $this->bus->expects(self::once()) + ->method('dispatch') + ->with(self::callback(fn (MeilisearchMessage $m) => 'updateDocuments' === $m->action && $m->payload['documents'] === $docs)) + ->willReturn(new Envelope(new \stdClass())); + + $this->service->updateDocuments('events', $docs); + } + + public function testDeleteDocumentDispatchesMessage(): void + { + $this->bus->expects(self::once()) + ->method('dispatch') + ->with(self::callback(fn (MeilisearchMessage $m) => 'deleteDocument' === $m->action && 42 === $m->payload['documentId'])) + ->willReturn(new Envelope(new \stdClass())); + + $this->service->deleteDocument('events', 42); + } + + public function testDeleteDocumentsDispatchesMessage(): void + { + $ids = [1, 2, 3]; + $this->bus->expects(self::once()) + ->method('dispatch') + ->with(self::callback(fn (MeilisearchMessage $m) => 'deleteDocuments' === $m->action && $m->payload['ids'] === $ids)) + ->willReturn(new Envelope(new \stdClass())); + + $this->service->deleteDocuments('events', $ids); + } + + public function testUpdateSettingsDispatchesMessage(): void + { + $settings = ['filterableAttributes' => ['status']]; + $this->bus->expects(self::once()) + ->method('dispatch') + ->with(self::callback(fn (MeilisearchMessage $m) => 'updateSettings' === $m->action && $m->payload['settings'] === $settings)) + ->willReturn(new Envelope(new \stdClass())); + + $this->service->updateSettings('events', $settings); + } + public function testSearchMakesPostRequest(): void { $response = $this->createMock(ResponseInterface::class); $response->method('getStatusCode')->willReturn(200); $response->method('toArray')->willReturn(['hits' => []]); - $this->httpClient->method('request') - ->with('POST', self::stringContains('/indexes/events/search'), self::anything()) - ->willReturn($response); + $this->httpClient->method('request')->willReturn($response); $result = $this->service->search('events', 'test'); self::assertArrayHasKey('hits', $result); } + public function testGetDocumentReturnsArray(): void + { + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode')->willReturn(200); + $response->method('toArray')->willReturn(['id' => 1, 'title' => 'Event']); + $this->httpClient->method('request')->willReturn($response); + + $result = $this->service->getDocument('events', 1); + + self::assertSame(1, $result['id']); + } + public function testRequestReturnsEmptyArrayOn204(): void { $response = $this->createMock(ResponseInterface::class); diff --git a/tests/Service/UnsubscribeManagerTest.php b/tests/Service/UnsubscribeManagerTest.php index faa3267..7e2dfeb 100644 --- a/tests/Service/UnsubscribeManagerTest.php +++ b/tests/Service/UnsubscribeManagerTest.php @@ -79,4 +79,18 @@ class UnsubscribeManagerTest extends TestCase $data = json_decode(file_get_contents($this->tempDir.'/var/unsubscribed.json'), true); self::assertCount(1, $data); } + + public function testUnsubscribeCreatesDirWhenMissing(): void + { + $dir = sys_get_temp_dir().'/unsubscribe_nodir_'.uniqid(); + $manager = new UnsubscribeManager($dir, 'secret'); + + $manager->unsubscribe('user@example.com'); + + self::assertTrue($manager->isUnsubscribed('user@example.com')); + + @unlink($dir.'/var/unsubscribed.json'); + @rmdir($dir.'/var'); + @rmdir($dir); + } }