feat(revervation): [Ajoute la création de session de réservation et le flow]
🐛 fix(PurgeCommandTest): [Utilise addCommand au lieu de add pour les commandes]
📝 chore(deps): [Mise à jour des dépendances Composer et corrections]
🐛 fix(KeycloakAuthenticator): [Corrige le type nullable de l'exception start]
 feat(Customer): [Ajoute les sessions de commandes aux entités Customer]
♻️ refactor(AppLogger): [Refactorise l'AppLogger pour obtenir l'UserAgent]
 feat(FlowReserve): [Ajoute une action de validation du panier]
```
This commit is contained in:
Serreau Jovann
2026-01-31 13:49:25 +01:00
parent 4227c3d3b0
commit 0be752c145
117 changed files with 8798 additions and 2645 deletions

View File

@@ -291,10 +291,50 @@ export class FlowReserve extends HTMLAnchorElement {
<span class="text-[#f39e36]">${this.formatPrice(total.totalTTC || total.totalHT)}</span>
</div>
</div>
<a href="/reservation/devis" class="block w-full py-4 bg-slate-900 text-white text-center rounded-2xl font-black uppercase italic tracking-widest hover:bg-[#fc0e50] transition-colors shadow-lg">
<a href="/reservation/devis" id="flow-validate-btn" class="block w-full py-4 bg-slate-900 text-white text-center rounded-2xl font-black uppercase italic tracking-widest hover:bg-[#fc0e50] transition-colors shadow-lg">
Valider ma demande
</a>
`;
const validateBtn = footer.querySelector('#flow-validate-btn');
if (validateBtn) {
validateBtn.addEventListener('click', (e) => {
this.validateBasket(e);
});
}
}
async validateBasket(e) {
e.preventDefault();
const ids = this.getList();
let dates = { start: null, end: null };
try {
dates = JSON.parse(localStorage.getItem('reservation_dates') || '{}');
} catch (error) {
console.warn('Invalid reservation dates in localStorage');
}
try {
const response = await fetch('/reservation/session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
ids,
start: dates.start,
end: dates.end
})
});
if (!response.ok) throw new Error('Erreur réseau');
const data = await response.json();
if (data.flowUrl) {
window.location.href = data.flowUrl;
}
} catch (error) {
console.error('Erreur lors de la validation du panier', error);
}
}
formatDate(dateString) {

View File

@@ -12,28 +12,28 @@
"ext-zip": "*",
"chillerlan/php-qrcode": ">=5.0.5",
"cocur/slugify": ">=4.7.1",
"doctrine/dbal": "^3.10.4",
"doctrine/doctrine-bundle": "^2.18.2",
"doctrine/dbal": "^4.4.1",
"doctrine/doctrine-bundle": "^3.2.2",
"doctrine/doctrine-migrations-bundle": "^3.7.0",
"doctrine/orm": "^3.6.1",
"doctrine/orm": "^3.6.2",
"docusealco/docuseal-php": "^1.0.5",
"endroid/qr-code": "^6.0.9",
"endroid/qr-code": "^6.1.0",
"exbil/mailcow-php-api": ">=0.15.0",
"fkrzski/robots-txt": "^2.0",
"fpdf/fpdf": "^1.86",
"gemini-api-php/client": "^1.7",
"fkrzski/robots-txt": "^2.1",
"fpdf/fpdf": "^1.86.1",
"gemini-api-php/client": "^1.7.2",
"google/apiclient": "^2.19.0",
"google/cloud": "^0.296.0",
"healey/robots": "^1.0.1",
"imagine/imagine": "^1.5.2",
"io-developer/php-whois": ">=4.1.10",
"jaybizzle/crawler-detect": "^1.3",
"jaybizzle/crawler-detect": "^1.3.6",
"knplabs/knp-paginator-bundle": "^6.10",
"knpuniversity/oauth2-client-bundle": "^2.20.1",
"lasserafn/php-initial-avatar-generator": "^4.5",
"league/flysystem-aws-s3-v3": "^3.30.1",
"league/flysystem-aws-s3-v3": "^3.31.0",
"league/flysystem-bundle": "^3.6.1",
"liip/imagine-bundle": "^2.15",
"liip/imagine-bundle": "^2.17.1",
"lufiipe/insee-sierene": ">=1",
"meilisearch/meilisearch-php": "^1.16.1",
"minishlink/web-push": "^9.0.4",
@@ -45,56 +45,55 @@
"pear/net_dns2": ">=2.0.7",
"phpdocumentor/reflection-docblock": "^5.6.6",
"phpoffice/phpspreadsheet": "^5.4",
"phpstan/phpdoc-parser": "^2.3.1",
"presta/sitemap-bundle": "^4.2",
"scheb/2fa-backup-code": "^7.13.1",
"scheb/2fa-bundle": "^7.13.1",
"scheb/2fa-email": "^7.13.1",
"scheb/2fa-google-authenticator": "^7.13.1",
"phpstan/phpdoc-parser": "^2.3.2",
"presta/sitemap-bundle": "^4.3",
"scheb/2fa-backup-code": "^8.3.0",
"scheb/2fa-bundle": "^8.3.0",
"scheb/2fa-email": "^8.3.0",
"scheb/2fa-google-authenticator": "^8.3.0",
"sentry/sentry-symfony": "^5.8.3",
"setasign/fpdi": "^2.6.4",
"spatie/mjml-php": "^1.2.5",
"spomky-labs/pwa-bundle": "1.3.5",
"spomky-labs/web-push-bundle": "^3.1.2",
"spomky-labs/pwa-bundle": "1.5.0",
"spomky-labs/web-push-bundle": "^3.2.1",
"stancer/stancer": ">=2.0.1",
"stevenmaguire/oauth2-keycloak": "^5.1",
"stripe/stripe-php": "^19.1",
"symfony/amazon-mailer": "7.3.*",
"symfony/asset": "7.3.*",
"symfony/asset-mapper": "7.3.*",
"symfony/console": "7.3.*",
"symfony/doctrine-messenger": "7.3.*",
"symfony/dotenv": "7.3.*",
"symfony/expression-language": "7.3.*",
"stripe/stripe-php": "^19.3",
"symfony/amazon-mailer": "8.0.*",
"symfony/asset": "8.0.*",
"symfony/asset-mapper": "8.0.*",
"symfony/console": "8.0.*",
"symfony/doctrine-messenger": "8.0.*",
"symfony/dotenv": "8.0.*",
"symfony/expression-language": "8.0.*",
"symfony/flex": "^2.10.0",
"symfony/form": "7.3.*",
"symfony/framework-bundle": "7.3.*",
"symfony/http-client": "7.3.*",
"symfony/intl": "7.3.*",
"symfony/mailer": "7.3.*",
"symfony/mime": "7.3.*",
"symfony/monolog-bundle": "^3.11.1",
"symfony/notifier": "7.3.*",
"symfony/process": "7.3.*",
"symfony/property-access": "7.3.*",
"symfony/property-info": "7.3.*",
"symfony/redis-messenger": "7.3.*",
"symfony/runtime": "7.3.*",
"symfony/security-bundle": "7.3.*",
"symfony/serializer": "7.3.*",
"symfony/string": "7.3.*",
"symfony/translation": "7.3.*",
"symfony/twig-bundle": "7.3.*",
"symfony/uid": "7.3.*",
"symfony/validator": "7.3.*",
"symfony/web-link": "7.3.*",
"symfony/yaml": "7.3.*",
"symfony/form": "8.0.*",
"symfony/framework-bundle": "8.0.*",
"symfony/http-client": "8.0.*",
"symfony/intl": "8.0.*",
"symfony/mailer": "8.0.*",
"symfony/mime": "8.0.*",
"symfony/monolog-bundle": "^4.0.1",
"symfony/notifier": "8.0.*",
"symfony/process": "8.0.*",
"symfony/property-access": "8.0.*",
"symfony/property-info": "8.0.*",
"symfony/redis-messenger": "8.0.*",
"symfony/runtime": "8.0.*",
"symfony/security-bundle": "8.0.*",
"symfony/serializer": "8.0.*",
"symfony/string": "8.0.*",
"symfony/translation": "8.0.*",
"symfony/twig-bundle": "8.0.*",
"symfony/uid": "8.0.*",
"symfony/validator": "8.0.*",
"symfony/web-link": "8.0.*",
"symfony/yaml": "8.0.*",
"tecnickcom/tcpdf": "^6.10.1",
"twig/extra-bundle": "^3.22.2",
"twig/intl-extra": "^3.22.1",
"twig/twig": "^3.22.2",
"twig/extra-bundle": "^3.23.0",
"twig/intl-extra": "^3.23.0",
"twig/twig": "^3.23.0",
"vich/uploader-bundle": "^2.9.1",
"web-auth/webauthn-lib": ">=5.2.3",
"web-auth/webauthn-lib": "5.3.x-dev",
"web-token/jwt-library": "^4.1.3"
},
"config": {
@@ -145,18 +144,18 @@
"extra": {
"symfony": {
"allow-contrib": true,
"require": "7.3.*"
"require": "8.0.*"
}
},
"require-dev": {
"fakerphp/faker": "^1.24.1",
"phpunit/phpunit": "^12.5.5",
"rector/rector": "^2.3.1",
"symfony/browser-kit": "7.3.*",
"symfony/css-selector": "7.3.*",
"symfony/debug-bundle": "7.3.*",
"phpunit/phpunit": "^12.5.8",
"rector/rector": "^2.3.5",
"symfony/browser-kit": "8.0.*",
"symfony/css-selector": "8.0.*",
"symfony/debug-bundle": "8.0.*",
"symfony/maker-bundle": "^1.65.1",
"symfony/stopwatch": "7.3.*",
"symfony/web-profiler-bundle": "7.3.*"
"symfony/stopwatch": "8.0.*",
"symfony/web-profiler-bundle": "8.0.*"
}
}

2733
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,11 +7,7 @@ doctrine:
#server_version: '16'
profiling_collect_backtrace: '%kernel.debug%'
use_savepoints: true
orm:
auto_generate_proxy_classes: false
enable_native_lazy_objects: true
report_fields_where_declared: true
validate_xml_mapping: true
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
identity_generation_preferences:
@@ -24,8 +20,6 @@ doctrine:
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
controller_resolver:
auto_mapping: false
when@test:
doctrine:
@@ -36,8 +30,6 @@ when@test:
when@prod:
doctrine:
orm:
auto_generate_proxy_classes: false
proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
query_cache_driver:
type: pool
pool: doctrine.system_cache_pool

View File

@@ -27,7 +27,8 @@ pwa:
cache_prefix: 'goolge-fonts'
max_entries: 20
max_age: 1
use_cdn: false
config:
use_cdn: false
manifest:
enabled: true
name: "Réservation Lukikevent"

View File

@@ -69,7 +69,10 @@ security:
main:
pattern: ^/
provider: reserve_account_provider # Force l'entité Customer ici
custom_authenticator: App\Security\CustomerAuthenticator
entry_point: App\Security\CustomerAuthenticator # Spécifie l'authentificateur à utiliser comme point d'entrée
custom_authenticator:
- App\Security\CustomerAuthenticator
- App\Security\FlowAuthenticator
user_checker: App\Security\UserChecker # Si vous voulez vérifier l'activation du compte
logout:
path: reservation_logout

File diff suppressed because it is too large Load Diff

View File

@@ -61,6 +61,11 @@ RUN mkdir -p /opt/phpstorm-coverage && \
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
RUN mkdir -p /opt/phpstorm-coverage/ && chmod -R 777 /opt/phpstorm-coverage/
# Configuration PHP personnalisée
RUN echo "memory_limit=5G" > /usr/local/etc/php/conf.d/memory-limit.ini \
&& echo "upload_max_filesize=100M" > /usr/local/etc/php/conf.d/uploads.ini \
&& echo "post_max_size=200M" >> /usr/local/etc/php/conf.d/uploads.ini
# Créer un utilisateur et un groupe non-root pour l'application
# Utilisation de useradd/groupadd pour les systèmes basés sur Debian/Ubuntu
RUN groupadd -g $GID appuser && \

86
migrate_data.php Normal file
View File

@@ -0,0 +1,86 @@
<?php
$host = '127.0.0.1';
$db = 'ludikevent';
$user = 'ludikevent';
$pass = 'ludikevent';
$port = "5432";
$dsn = "pgsql:host=$host;port=$port;dbname=$db";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
$pdo = new PDO($dsn, $user, $pass, $options);
} catch ( PDOException $e) {
die("Connection failed: " . $e->getMessage());
}
echo "Connected to database.\n";
function migrateTable($pdo, $tableName, $columnName, $idColumn = 'id') {
echo "Checking table: $tableName, column: $columnName\n";
// Check if table exists
$stmt = $pdo->prepare("SELECT to_regclass(:tablename)");
$stmt->execute(['tablename' => $tableName]);
if (!$stmt->fetchColumn()) {
echo "Table $tableName does not exist. Skipping.\n";
return;
}
// Select only rows that are NOT NULL.
// We also want to avoid reprocessing already JSON data if possible, but reliable detection is tricky.
// The script below checks if data is serialized first. If not, it checks if it's JSON.
$stmt = $pdo->query("SELECT $idColumn, $columnName FROM $tableName WHERE $columnName IS NOT NULL");
while ($row = $stmt->fetch()) {
$id = $row[$idColumn];
$rawData = $row[$columnName];
// Try to unserialize
$data = @unserialize($rawData);
// Check for unserialization success or specific serialized values
// unserialize returns false on error AND for serialized boolean false (b:0;)
$isSerialized = ($data !== false) || ($rawData === 'b:0;') || ($rawData === 'N;');
if ($isSerialized) {
// It was a valid serialized string
if ($rawData === 'N;') {
// Serialized null -> SQL NULL
$updateStmt = $pdo->prepare("UPDATE $tableName SET $columnName = NULL WHERE $idColumn = :id");
$updateStmt->execute(['id' => $id]);
echo "Updated ID $id: Serialized NULL -> SQL NULL\n";
} else {
// Convert to JSON
$jsonData = json_encode($data);
$updateStmt = $pdo->prepare("UPDATE $tableName SET $columnName = :json WHERE $idColumn = :id");
$updateStmt->execute(['json' => $jsonData, 'id' => $id]);
echo "Updated ID $id: Serialized -> JSON\n";
}
} else {
// Check if it is already valid JSON
json_decode($rawData);
if (json_last_error() === JSON_ERROR_NONE) {
// It is already JSON, do nothing
// echo "ID $id is already JSON. Skipping.\n";
} else {
// It might be a plain string or corrupted data.
// For Types::ARRAY columns, data MUST be serialized.
// If we migrated to Types::JSON, data MUST be JSON.
// If neither, it's an issue.
echo "Warning: ID $id in $tableName is neither valid serialized data nor valid JSON. Raw: " . substr($rawData, 0, 50) . "...\n";
}
}
}
}
migrateTable($pdo, 'contrats_payments', 'card');
migrateTable($pdo, 'formules_product_inclus', 'config');
migrateTable($pdo, 'formules_restriction', 'restriction_config');
echo "Migration complete.\n";

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260130184514 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE order_session (id SERIAL NOT NULL, customer_id INT DEFAULT NULL, uuid VARCHAR(255) NOT NULL, products JSON NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE UNIQUE INDEX UNIQ_263E7C9FD17F50A6 ON order_session (uuid)');
$this->addSql('CREATE INDEX IDX_263E7C9F9395C3F3 ON order_session (customer_id)');
$this->addSql('COMMENT ON COLUMN order_session.created_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN order_session.updated_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE order_session ADD CONSTRAINT FK_263E7C9F9395C3F3 FOREIGN KEY (customer_id) REFERENCES customer (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE order_session DROP CONSTRAINT FK_263E7C9F9395C3F3');
$this->addSql('DROP TABLE order_session');
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260130185149 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE order_session ADD state VARCHAR(50) NOT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE order_session DROP state');
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260131103521 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE order_session ADD billing_address VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE order_session ADD billing_zip_code VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE order_session ADD billing_town VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE order_session ADD adress_event VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE order_session ADD adress2_event VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE order_session ADD adress3_event VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE order_session ADD zip_code_event VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE order_session ADD town_event VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE order_session ADD type VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE order_session ADD details TEXT DEFAULT NULL');
$this->addSql('ALTER TABLE order_session ADD type_sol VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE order_session ADD pente VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE order_session ADD access TEXT DEFAULT NULL');
$this->addSql('ALTER TABLE order_session ADD distance_power DOUBLE PRECISION DEFAULT NULL');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('ALTER TABLE order_session DROP billing_address');
$this->addSql('ALTER TABLE order_session DROP billing_zip_code');
$this->addSql('ALTER TABLE order_session DROP billing_town');
$this->addSql('ALTER TABLE order_session DROP adress_event');
$this->addSql('ALTER TABLE order_session DROP adress2_event');
$this->addSql('ALTER TABLE order_session DROP adress3_event');
$this->addSql('ALTER TABLE order_session DROP zip_code_event');
$this->addSql('ALTER TABLE order_session DROP town_event');
$this->addSql('ALTER TABLE order_session DROP type');
$this->addSql('ALTER TABLE order_session DROP details');
$this->addSql('ALTER TABLE order_session DROP type_sol');
$this->addSql('ALTER TABLE order_session DROP pente');
$this->addSql('ALTER TABLE order_session DROP access');
$this->addSql('ALTER TABLE order_session DROP distance_power');
}
}

View File

@@ -7,6 +7,7 @@
failOnDeprecation="false"
failOnNotice="false"
failOnWarning="false"
displayDetailsOnTestsThatTriggerNotices="true"
bootstrap="tests/bootstrap.php"
cacheDirectory=".phpunit.cache"
>
@@ -14,7 +15,6 @@
<ini name="display_errors" value="1" />
<ini name="error_reporting" value="-1" />
<server name="APP_ENV" value="test" force="true" />
<server name="SHELL_VERBOSITY" value="-1" />
</php>
<testsuites>
@@ -25,7 +25,6 @@
<source ignoreSuppressionOfDeprecations="true"
ignoreIndirectDeprecations="true"
restrictNotices="true"
restrictWarnings="true"
>
<include>

View File

@@ -10,6 +10,7 @@ use App\Entity\SitePerformance;
use App\Repository\CustomerRepository;
use App\Repository\CustomerTrackingRepository;
use App\Repository\FormulesRepository;
use App\Repository\OrderSessionRepository;
use App\Repository\ProductRepository;
use App\Repository\ProductReserveRepository;
use App\Service\Mailer\Mailer;
@@ -60,9 +61,9 @@ class ReserverController extends AbstractController
#[Route('/produit/check', name: 'produit_check', methods: ['GET', 'POST'])]
public function productCheck(Request $request, ProductReserveRepository $productReserveRepository, ProductRepository $productRepository): Response
{
$productId = $request->get('id');
$startStr = $request->get('start');
$endStr = $request->get('end');
$productId = $request->query->get('id');
$startStr = $request->query->get('start');
$endStr = $request->query->get('end');
if (!$productId && $request->isMethod('POST')) {
$payload = $request->getPayload();
@@ -197,6 +198,160 @@ class ReserverController extends AbstractController
]);
}
#[Route('/reservation/session', name: 'reservation_session_create', methods: ['POST'])]
public function createSession(Request $request, EntityManagerInterface $em, OrderSessionRepository $sessionRepository): Response
{
$data = json_decode($request->getContent(), true);
$existingUuid = $request->getSession()->get('order_session_uuid');
$session = null;
if ($existingUuid) {
$session = $sessionRepository->findOneBy(['uuid' => $existingUuid]);
}
if (!$session) {
$session = new \App\Entity\OrderSession();
$session->setUuid(\Symfony\Component\Uid\Uuid::v4()->toRfc4122());
$session->setState('created');
}
$session->setProducts($data ?? []);
$user = $this->getUser();
if ($user instanceof Customer) {
$session->setCustomer($user);
}
$em->persist($session);
$em->flush();
$request->getSession()->set('order_session_uuid', $session->getUuid());
return new JsonResponse([
'flowUrl' => $this->generateUrl('reservation_flow', ['sessionId' => $session->getUuid()])
]);
}
#[Route('/flow/{sessionId}', name: 'reservation_flow', methods: ['GET', 'POST'])]
public function flowLogin(
string $sessionId,
AuthenticationUtils $authenticationUtils,
OrderSessionRepository $repository,
ProductRepository $productRepository,
UploaderHelper $uploaderHelper
): Response {
// This is the POST target for the login form, but also the GET page.
// The authenticator handles the POST. For GET, we just render the page.
$session = $repository->findOneBy(['uuid' => $sessionId]);
if (!$session) {
return $this->render('revervation/session_lost.twig');
}
$sessionData = $session->getProducts();
$ids = $sessionData['ids'] ?? [];
$startStr = $sessionData['start'] ?? null;
$endStr = $sessionData['end'] ?? null;
// Calcul de la durée
$duration = 1;
if ($startStr && $endStr) {
try {
$start = new \DateTimeImmutable($startStr);
$end = new \DateTimeImmutable($endStr);
if ($end >= $start) {
$duration = $start->diff($end)->days + 1;
}
} catch (\Exception $e) {
$duration = 1;
}
}
$products = [];
if (!empty($ids)) {
$products = $productRepository->findBy(['id' => $ids]);
}
$items = [];
$totalHT = 0;
$tvaEnabled = isset($_ENV['TVA_ENABLED']) && $_ENV['TVA_ENABLED'] === "true";
$tvaRate = $tvaEnabled ? 0.20 : 0;
foreach ($products as $product) {
$price1Day = $product->getPriceDay();
$priceSup = $product->getPriceSup() ?? 0.0;
// Calcul du coût total pour ce produit selon la durée
$productTotalHT = $price1Day + ($priceSup * max(0, $duration - 1));
$productTotalTTC = $productTotalHT * (1 + $tvaRate);
$items[] = [
'product' => $product,
'image' => $uploaderHelper->asset($product, 'imageFile'),
'price1Day' => $price1Day,
'priceSup' => $priceSup,
'totalPriceHT' => $productTotalHT,
'totalPriceTTC' => $productTotalTTC,
];
$totalHT += $productTotalHT;
}
$totalTva = $totalHT * $tvaRate;
$totalTTC = $totalHT + $totalTva;
return $this->render('revervation/flow.twig', [
'session' => $session,
'last_username' => $authenticationUtils->getLastUsername(),
'error' => $authenticationUtils->getLastAuthenticationError(),
'cart' => [
'items' => $items,
'startDate' => $startStr ? new \DateTimeImmutable($startStr) : null,
'endDate' => $endStr ? new \DateTimeImmutable($endStr) : null,
'duration' => $duration,
'totalHT' => $totalHT,
'totalTva' => $totalTva,
'totalTTC' => $totalTTC,
'tvaEnabled' => $tvaEnabled,
]
]);
}
#[Route('/flow/{sessionId}/update', name: 'reservation_flow_update', methods: ['POST'])]
public function flowUpdate(
string $sessionId,
Request $request,
OrderSessionRepository $repository,
EntityManagerInterface $em
): Response {
$session = $repository->findOneBy(['uuid' => $sessionId]);
if (!$session) {
return $this->redirectToRoute('reservation');
}
$session->setBillingAddress($request->request->get('billingAddress'));
$session->setBillingZipCode($request->request->get('billingZipCode'));
$session->setBillingTown($request->request->get('billingTown'));
$session->setAdressEvent($request->request->get('adressEvent'));
$session->setAdress2Event($request->request->get('adress2Event'));
$session->setZipCodeEvent($request->request->get('zipCodeEvent'));
$session->setTownEvent($request->request->get('townEvent'));
$session->setType($request->request->get('type'));
$session->setDetails($request->request->get('details'));
$session->setTypeSol($request->request->get('typeSol'));
$session->setPente($request->request->get('pente'));
$session->setAccess($request->request->get('access'));
$distance = $request->request->get('distancePower');
if ($distance !== null && $distance !== '') {
$session->setDistancePower((float)$distance);
}
$em->flush();
return $this->redirectToRoute('reservation_flow', ['sessionId' => $sessionId]);
}
#[Route('/umami', name: 'reservation_umami', methods: ['POST'])]
public function umami(
Request $request,

View File

@@ -39,7 +39,7 @@ class ContratsPayments
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $validateAt = null;
#[ORM\Column(type: Types::ARRAY,nullable: true)]
#[ORM\Column(type: Types::JSON,nullable: true)]
private array $card = [];

View File

@@ -95,6 +95,12 @@ class Customer implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\OneToMany(targetEntity: CustomerTracking::class, mappedBy: 'customer')]
private Collection $customerTrackings;
/**
* @var Collection<int, OrderSession>
*/
#[ORM\OneToMany(targetEntity: OrderSession::class, mappedBy: 'customer')]
private Collection $orderSessions;
public function __construct()
{
$this->customerAddresses = new ArrayCollection();
@@ -106,6 +112,7 @@ class Customer implements UserInterface, PasswordAuthenticatedUserInterface
$this->roles = [self::ROLE_CUSTOMER];
$this->isAccountConfigured = false;
$this->customerTrackings = new ArrayCollection();
$this->orderSessions = new ArrayCollection();
}
// --- MÉTHODES INTERFACES (SECURITY) ---
@@ -357,4 +364,34 @@ class Customer implements UserInterface, PasswordAuthenticatedUserInterface
return $this;
}
/**
* @return Collection<int, OrderSession>
*/
public function getOrderSessions(): Collection
{
return $this->orderSessions;
}
public function addOrderSession(OrderSession $orderSession): static
{
if (!$this->orderSessions->contains($orderSession)) {
$this->orderSessions->add($orderSession);
$orderSession->setCustomer($this);
}
return $this;
}
public function removeOrderSession(OrderSession $orderSession): static
{
if ($this->orderSessions->removeElement($orderSession)) {
// set the owning side to null (unless already changed)
if ($orderSession->getCustomer() === $this) {
$orderSession->setCustomer(null);
}
}
return $this;
}
}

View File

@@ -20,7 +20,7 @@ class FormulesProductInclus
#[ORM\ManyToOne(inversedBy: 'formulesProductIncluses')]
private ?Product $PRODUCT = null;
#[ORM\Column(type: Types::ARRAY)]
#[ORM\Column(type: Types::JSON)]
private array $config = [];
public function getId(): ?int

View File

@@ -20,7 +20,7 @@ class FormulesRestriction
#[ORM\Column]
private ?int $nbStructureMax = null;
#[ORM\Column(type: Types::ARRAY, nullable: true)]
#[ORM\Column(type: Types::JSON, nullable: true)]
private ?array $restrictionConfig = null;
#[ORM\Column(nullable: true)]

341
src/Entity/OrderSession.php Normal file
View File

@@ -0,0 +1,341 @@
<?php
namespace App\Entity;
use App\Repository\OrderSessionRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: OrderSessionRepository::class)]
#[ORM\HasLifecycleCallbacks]
class OrderSession
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255, unique: true)]
private ?string $uuid = null;
#[ORM\Column(type: Types::JSON)]
private array $products = [];
#[ORM\ManyToOne(inversedBy: 'orderSessions')]
private ?Customer $customer = null;
#[ORM\Column]
private ?\DateTimeImmutable $createdAt = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $updatedAt = null;
#[ORM\Column(length: 50)]
private string $state = 'created';
#[ORM\Column(length: 255, nullable: true)]
private ?string $billingAddress = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $billingZipCode = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $billingTown = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $adressEvent = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $adress2Event = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $adress3Event = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $zipCodeEvent = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $townEvent = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $type = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $details = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $typeSol = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $pente = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $access = null;
#[ORM\Column(nullable: true)]
private ?float $distancePower = null;
public function __construct()
{
$this->createdAt = new \DateTimeImmutable();
$this->products = [];
$this->state = 'created';
}
#[ORM\PrePersist]
public function setCreatedAtValue(): void
{
$this->createdAt = new \DateTimeImmutable();
}
#[ORM\PreUpdate]
public function setUpdatedAtValue(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
public function getId(): ?int
{
return $this->id;
}
public function getUuid(): ?string
{
return $this->uuid;
}
public function setUuid(string $uuid): static
{
$this->uuid = $uuid;
return $this;
}
public function getState(): string
{
return $this->state;
}
public function setState(string $state): static
{
$this->state = $state;
return $this;
}
public function getProducts(): array
{
return $this->products;
}
public function setProducts(array $products): static
{
$this->products = $products;
return $this;
}
public function getCustomer(): ?Customer
{
return $this->customer;
}
public function setCustomer(?Customer $customer): static
{
$this->customer = $customer;
return $this;
}
public function getCreatedAt(): ?\DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeImmutable $createdAt): static
{
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): ?\DateTimeImmutable
{
return $this->updatedAt;
}
public function setUpdatedAt(?\DateTimeImmutable $updatedAt): static
{
$this->updatedAt = $updatedAt;
return $this;
}
public function getBillingAddress(): ?string
{
return $this->billingAddress;
}
public function setBillingAddress(?string $billingAddress): static
{
$this->billingAddress = $billingAddress;
return $this;
}
public function getBillingZipCode(): ?string
{
return $this->billingZipCode;
}
public function setBillingZipCode(?string $billingZipCode): static
{
$this->billingZipCode = $billingZipCode;
return $this;
}
public function getBillingTown(): ?string
{
return $this->billingTown;
}
public function setBillingTown(?string $billingTown): static
{
$this->billingTown = $billingTown;
return $this;
}
public function getAdressEvent(): ?string
{
return $this->adressEvent;
}
public function setAdressEvent(?string $adressEvent): static
{
$this->adressEvent = $adressEvent;
return $this;
}
public function getAdress2Event(): ?string
{
return $this->adress2Event;
}
public function setAdress2Event(?string $adress2Event): static
{
$this->adress2Event = $adress2Event;
return $this;
}
public function getAdress3Event(): ?string
{
return $this->adress3Event;
}
public function setAdress3Event(?string $adress3Event): static
{
$this->adress3Event = $adress3Event;
return $this;
}
public function getZipCodeEvent(): ?string
{
return $this->zipCodeEvent;
}
public function setZipCodeEvent(?string $zipCodeEvent): static
{
$this->zipCodeEvent = $zipCodeEvent;
return $this;
}
public function getTownEvent(): ?string
{
return $this->townEvent;
}
public function setTownEvent(?string $townEvent): static
{
$this->townEvent = $townEvent;
return $this;
}
public function getType(): ?string
{
return $this->type;
}
public function setType(?string $type): static
{
$this->type = $type;
return $this;
}
public function getDetails(): ?string
{
return $this->details;
}
public function setDetails(?string $details): static
{
$this->details = $details;
return $this;
}
public function getTypeSol(): ?string
{
return $this->typeSol;
}
public function setTypeSol(?string $typeSol): static
{
$this->typeSol = $typeSol;
return $this;
}
public function getPente(): ?string
{
return $this->pente;
}
public function setPente(?string $pente): static
{
$this->pente = $pente;
return $this;
}
public function getAccess(): ?string
{
return $this->access;
}
public function setAccess(?string $access): static
{
$this->access = $access;
return $this;
}
public function getDistancePower(): ?float
{
return $this->distancePower;
}
public function setDistancePower(?float $distancePower): static
{
$this->distancePower = $distancePower;
return $this;
}
}

View File

@@ -17,7 +17,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class ContratsType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('addressEvent',TextType::class,[
@@ -117,7 +117,7 @@ class ContratsType extends AbstractType
;
}
public function configureOptions(OptionsResolver $resolver)
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefault('data_class',Contrats::class);
}

View File

@@ -25,9 +25,10 @@ class AppLogger
$request = $this->requestStack->getCurrentRequest();
$path = $request ? $request->getRequestUri() : 'CLI/Internal';
$userAgent = $request ? $request->headers->get('user-agent') : 'CLI';
// Création de l'objet immuable via le constructeur
$log = new AuditLog($user, $type, $message, $path,$request->headers->get('user-agent'));
$log = new AuditLog($user, $type, $message, $path, $userAgent);
$this->entityManager->persist($log);
$this->entityManager->flush();

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Repository;
use App\Entity\OrderSession;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<OrderSession>
*
* @method OrderSession|null find($id, $lockMode = null, $lockVersion = null)
* @method OrderSession|null findOneBy(array $criteria, array $orderBy = null)
* @method OrderSession[] findAll()
* @method OrderSession[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class OrderSessionRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, OrderSession::class);
}
// /**
// * @return OrderSession[] Returns an array of OrderSession objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('o')
// ->andWhere('o.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('o.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?OrderSession
// {
// return $this->createQueryBuilder('o')
// ->andWhere('o.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View File

@@ -20,7 +20,7 @@ class AuthenticationEntryPoint implements AuthenticationEntryPointInterface
private readonly TokenStorageInterface $tokenStorage
) {}
public function start(Request $request, AuthenticationException $authException = null): Response
public function start(Request $request, ?AuthenticationException $authException = null): Response
{
$token = $this->tokenStorage->getToken();
$user = $token ? $token->getUser() : null;

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Security;
use App\Entity\Customer;
use App\Entity\OrderSession;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\SecurityRequestAttributes;
class FlowAuthenticator extends AbstractLoginFormAuthenticator
{
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
private readonly EntityManagerInterface $entityManager
) {}
public function supports(Request $request): bool
{
return $request->attributes->get('_route') === 'reservation_flow'
&& $request->isMethod('POST');
}
public function authenticate(Request $request): Passport
{
$email = $request->getPayload()->getString('_username');
$password = $request->getPayload()->getString('_password');
$csrfToken = $request->getPayload()->getString('_csrf_token');
$request->getSession()->set(SecurityRequestAttributes::LAST_USERNAME, $email);
return new Passport(
new UserBadge($email, function (string $userIdentifier) {
return $this->entityManager->getRepository(Customer::class)
->findOneBy(['email' => $userIdentifier]);
}),
new PasswordCredentials($password),
[
new CsrfTokenBadge('authenticate_flow', $csrfToken),
new RememberMeBadge(),
]
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
$sessionId = $request->attributes->get('sessionId');
// Associate customer with session
/** @var Customer $customer */
$customer = $token->getUser();
$session = $this->entityManager->getRepository(OrderSession::class)
->findOneBy(['uuid' => $sessionId]);
if ($session && $customer) {
$session->setCustomer($customer);
$this->entityManager->flush();
}
return new RedirectResponse($this->urlGenerator->generate('reservation_flow', ['sessionId' => $sessionId]));
}
protected function getLoginUrl(Request $request): string
{
$sessionId = $request->attributes->get('sessionId');
return $this->urlGenerator->generate('reservation_flow', ['sessionId' => $sessionId]);
}
}

View File

@@ -95,7 +95,7 @@ class KeycloakAuthenticator extends OAuth2Authenticator implements Authenticatio
return new Response($message, Response::HTTP_FORBIDDEN);
}
public function start(Request $request, AuthenticationException $authException = null): Response
public function start(Request $request, ?AuthenticationException $authException = null): Response
{
return new RedirectResponse($this->router->generate('connect_keycloak_start'));
}

View File

@@ -20,7 +20,7 @@ class UserChecker implements UserCheckerInterface
}
}
public function checkPostAuth(UserInterface $user): void
public function checkPostAuth(UserInterface $user, \Symfony\Component\Security\Core\Authentication\Token\TokenInterface|null $token = null): void
{
// Pas de vérifications post-authentification pour linstant
}

View File

@@ -10,8 +10,11 @@ class GeminiClient
private ?Client $client = null;
public function __construct(
private readonly string $apiKey = "AIzaSyDTPJERlUC47bcvhZU51Lwpqb1uxXS8SIg" // Keeping the hardcoded key as in original command for now, but should be env var
) {}
private readonly string $apiKey = "AIzaSyDTPJERlUC47bcvhZU51Lwpqb1uxXS8SIg", // Keeping the hardcoded key as in original command for now, but should be env var
?Client $client = null
) {
$this->client = $client;
}
public function generateFriendlyMessage(string $rawMessage): ?string
{

View File

@@ -10,15 +10,15 @@ class Client
private string $env;
private MeilisearchClient $client;
public function __construct()
public function __construct(?MeilisearchClient $client = null)
{
// Récupération de l'environnement (ex: dev, prod)
$this->env = $_ENV['APP_ENV'] ?? 'dev';
// Connexion au serveur Meilisearch
$this->client = new MeilisearchClient(
$this->client = $client ?? new MeilisearchClient(
"https://tools-meilisearch.esy-web.dev",
$_ENV['ESY_SEARCH_KEY']
$_ENV['ESY_SEARCH_KEY'] ?? 'test_key'
);
}

View File

@@ -24,6 +24,7 @@ class Client
private readonly EntityManagerInterface $entityManager,
private readonly KernelInterface $kernel,
private readonly StorageInterface $storage,
?\Docuseal\Api $docuseal = null
) {
// Configuration via les variables d'environnement
$key = $_ENV['ESYSIGN_APIEY'] ?? '';
@@ -31,7 +32,7 @@ class Client
// L'URL API est le point d'entrée pour le SDK Docuseal
$apiUrl = rtrim("https://signature.esy-web.dev", '/') . '/api';
$this->docuseal = new \Docuseal\Api($key, $apiUrl);
$this->docuseal = $docuseal ?? new \Docuseal\Api($key, $apiUrl);
$this->logo = $kernel->getProjectDir()."/sign_ludikevent.jpeg";
}

View File

@@ -23,10 +23,11 @@ class Client
public function __construct(
private EntityManagerInterface $em,
private UploaderHelper $uploaderHelper,
?StripeClient $client = null
) {
$stripeSk = $_ENV['STRIPE_SK'] ?? '';
$this->stripeBaseUrl = $_ENV['STRIPE_BASEURL'] ?? '';
$this->client = new StripeClient($stripeSk);
$this->client = $client ?? new StripeClient($stripeSk);
}
/**

View File

@@ -32,10 +32,15 @@ class DatabaseDumper
);
// Using exec as in original command, but wrapping it here allows mocking the class.
exec($command, $outputExec, $returnCode);
$this->execute($command, $outputExec, $returnCode);
if ($returnCode !== 0) {
throw new \Exception("Échec pg_dump (Code: $returnCode)");
}
}
protected function execute(string $command, &$output, &$returnCode): void
{
exec($command, $output, $returnCode);
}
}

View File

@@ -21,7 +21,7 @@ class StripeExtension extends AbstractExtension
{
}
public function getFilters()
public function getFilters(): array
{
return [
new TwigFilter('totalQuoto',[$this,'totalQuoto']),
@@ -222,7 +222,7 @@ class StripeExtension extends AbstractExtension
return false; // No matches found
}
public function getFunctions()
public function getFunctions(): array
{
return [
new TwigFunction('isBot', [$this, 'isBot']),

View File

@@ -17,7 +17,7 @@ class ViteAssetExtension extends AbstractExtension
public function __construct(
private readonly string $manifest,
private readonly CacheItemPoolInterface $cache,
private readonly ContentSecurityPolicyListener $cspListener,
private readonly ?ContentSecurityPolicyListener $cspListener = null,
) {
$this->isDev = $_ENV['VITE_LOAD'] === "0";
}
@@ -34,10 +34,10 @@ class ViteAssetExtension extends AbstractExtension
/**
* Récupère le nonce pour les scripts via le Listener de Nelmio
*/
private function getNonce(): string
protected function getNonce(): string
{
// Dans la v3.8, on utilise getNonce('script') sur le listener
return $this->cspListener->getNonce('script');
return $this->cspListener?->getNonce('script') ?? '';
}
public function isMobile(): bool

View File

@@ -0,0 +1,298 @@
{% extends 'revervation/base.twig' %}
{% block title %}Finalisation de votre demande{% endblock %}
{% block body %}
<div class="max-w-7xl mx-auto px-4 py-12">
<div class="bg-white rounded-3xl shadow-xl p-8 border border-gray-100">
<h1 class="text-3xl font-black text-slate-900 uppercase italic mb-8 text-center">Récapitulatif de votre demande</h1>
{% if session.customer is null %}
{# --- LOGIN FORM --- #}
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<div class="text-center mb-6">
<h2 class="text-xl font-bold text-gray-800">Connectez-vous pour continuer</h2>
<p class="mt-2 text-sm text-gray-600">
Ou <a href="{{ path('reservation_register') }}" class="font-medium text-blue-600 hover:text-blue-500">créez un compte</a>.
</p>
</div>
{% if error is defined and error %}
<div class="mb-4 p-4 rounded-2xl bg-red-50 border border-red-100 text-red-700 text-sm">
{{ error.messageKey|trans(error.messageData, 'security') }}
</div>
{% endif %}
<form action="{{ path('reservation_flow', {sessionId: session.uuid}) }}" method="post" class="space-y-6">
<div>
<label for="username" class="block text-sm font-semibold text-gray-700">Adresse Email</label>
<div class="mt-1">
<input type="email" id="username" name="_username" value="{{ last_username|default('') }}" required autofocus
class="appearance-none block w-full px-4 py-3 border border-gray-200 rounded-2xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all">
</div>
</div>
<div>
<label for="password" class="block text-sm font-semibold text-gray-700">Mot de passe</label>
<div class="mt-1">
<input type="password" id="password" name="_password" required
class="appearance-none block w-full px-4 py-3 border border-gray-200 rounded-2xl shadow-sm placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all">
</div>
</div>
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate_flow') }}">
<div>
<button type="submit"
class="w-full flex justify-center py-4 px-4 border border-transparent rounded-2xl shadow-lg text-sm font-bold text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all">
Se connecter
</button>
</div>
</form>
</div>
{% else %}
{# --- LOGGED IN STATE --- #}
<div class="max-w-3xl mx-auto space-y-8">
{# --- CART DETAILS --- #}
{% if cart is defined %}
<div class="bg-slate-50 rounded-2xl p-6 border border-slate-100">
<div class="flex items-center gap-4 mb-6">
<div class="bg-indigo-100 p-3 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
</div>
<div>
<h3 class="text-lg font-bold text-slate-900">Détails de la réservation</h3>
{% if cart.startDate and cart.endDate %}
<p class="text-sm text-slate-600">Du <span class="font-medium text-slate-900">{{ cart.startDate|date('d/m/Y') }}</span> au <span class="font-medium text-slate-900">{{ cart.endDate|date('d/m/Y') }}</span> ({{ cart.duration }} jours)</p>
{% endif %}
</div>
</div>
<div class="space-y-4">
{% for item in cart.items %}
<div class="flex items-center bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
{% if item.image %}
<img src="{{ item.image }}" alt="{{ item.product.name }}" class="h-16 w-16 object-cover rounded-lg mr-4 bg-slate-100">
{% else %}
<div class="h-16 w-16 bg-slate-100 rounded-lg mr-4 flex items-center justify-center text-slate-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
{% endif %}
<div class="flex-1">
<h4 class="font-bold text-slate-800">{{ item.product.name }}</h4>
<p class="text-xs text-slate-500 line-clamp-1 mb-2">{{ item.product.description }}</p>
<div class="text-xs text-slate-600 bg-slate-50 p-2 rounded-lg border border-slate-100 inline-block">
<div class="flex flex-wrap gap-x-3 gap-y-1">
<span>1er jour : <strong class="text-slate-800">{{ item.price1Day|number_format(2, ',', ' ') }} €</strong></span>
{% if cart.duration > 1 %}
<span class="text-slate-300">|</span>
<span>Jours supp. : <strong class="text-slate-800">{{ item.priceSup|number_format(2, ',', ' ') }} €</strong> <span class="text-slate-400">x {{ cart.duration - 1 }}</span></span>
{% endif %}
</div>
</div>
</div>
<div class="text-right">
<p class="font-bold text-slate-900">{{ item.totalPriceHT|number_format(2, ',', ' ') }} € HT</p>
{% if cart.tvaEnabled %}
<p class="text-xs text-slate-500">{{ item.totalPriceTTC|number_format(2, ',', ' ') }} € TTC</p>
{% endif %}
</div>
</div>
{% else %}
<p class="text-center text-slate-500 py-4">Aucun produit sélectionné.</p>
{% endfor %}
</div>
<div class="mt-6 border-t border-slate-200 pt-4 space-y-2">
<div class="flex justify-between text-sm text-slate-600">
<span>Total HT</span>
<span class="font-medium">{{ cart.totalHT|number_format(2, ',', ' ') }} €</span>
</div>
{% if cart.tvaEnabled %}
<div class="flex justify-between text-sm text-slate-600">
<span>TVA (20%)</span>
<span class="font-medium">{{ cart.totalTva|number_format(2, ',', ' ') }} €</span>
</div>
<div class="flex justify-between text-lg font-black text-slate-900 pt-2 border-t border-slate-200 mt-2">
<span>Total TTC</span>
<span>{{ cart.totalTTC|number_format(2, ',', ' ') }} €</span>
</div>
{% endif %}
</div>
</div>
{% endif %}
<div class="bg-slate-50 rounded-2xl p-6 border border-slate-100 flex flex-col md:flex-row items-center md:items-start gap-6">
<div class="bg-blue-100 p-4 rounded-full flex-shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<div class="flex-1 w-full text-center md:text-left">
<h3 class="text-lg font-bold text-slate-900 mb-4">Informations Client</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-white p-3 rounded-xl border border-slate-200 shadow-sm">
<span class="block text-xs text-slate-500 uppercase font-semibold tracking-wider">Nom complet</span>
<span class="block text-slate-800 font-medium mt-1">{{ session.customer.name }} {{ session.customer.surname }}</span>
</div>
<div class="bg-white p-3 rounded-xl border border-slate-200 shadow-sm">
<span class="block text-xs text-slate-500 uppercase font-semibold tracking-wider">Téléphone</span>
<span class="block text-slate-800 font-medium mt-1">{{ session.customer.phone }}</span>
</div>
<div class="bg-white p-3 rounded-xl border border-slate-200 shadow-sm md:col-span-2">
<span class="block text-xs text-slate-500 uppercase font-semibold tracking-wider">Email</span>
<span class="block text-slate-800 font-medium mt-1">{{ session.customer.email }}</span>
</div>
</div>
</div>
</div>
{# --- EVENT & BILLING FORM --- #}
<div class="bg-white rounded-2xl p-6 border border-slate-200 shadow-sm">
<h3 class="text-xl font-bold text-slate-900 mb-6 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
Informations de l'événement
</h3>
<form action="{{ path('reservation_flow_update', {sessionId: session.uuid}) }}" method="post" class="space-y-10">
{# Billing Address #}
<div>
<div class="flex items-center gap-3 mb-6 pb-2 border-b border-slate-100">
<span class="bg-blue-100 text-blue-600 p-2 rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" /></svg>
</span>
<h4 class="font-bold text-lg text-slate-900">Adresse de facturation</h4>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="md:col-span-2">
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Adresse complète</label>
<input type="text" name="billingAddress" value="{{ session.billingAddress }}" required
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400"
placeholder="123 Rue de la Paix">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Code Postal</label>
<input type="text" name="billingZipCode" value="{{ session.billingZipCode }}" required
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400"
placeholder="75000">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Ville</label>
<input type="text" name="billingTown" value="{{ session.billingTown }}" required
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400"
placeholder="Paris">
</div>
</div>
</div>
{# Event Address #}
<div>
<div class="flex items-center gap-3 mb-6 pb-2 border-b border-slate-100">
<span class="bg-blue-100 text-blue-600 p-2 rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
</span>
<h4 class="font-bold text-lg text-slate-900">Lieu de l'événement</h4>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="md:col-span-2">
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Adresse de l'événement</label>
<input type="text" name="adressEvent" value="{{ session.adressEvent }}" required
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400">
</div>
<div class="md:col-span-2">
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Complément d'adresse <span class="text-slate-300 font-normal normal-case">(Optionnel)</span></label>
<input type="text" name="adress2Event" value="{{ session.adress2Event }}"
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Code Postal</label>
<input type="text" name="zipCodeEvent" value="{{ session.zipCodeEvent }}" required
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Ville</label>
<input type="text" name="townEvent" value="{{ session.townEvent }}" required
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400">
</div>
</div>
</div>
{# Event Details #}
<div>
<div class="flex items-center gap-3 mb-6 pb-2 border-b border-slate-100">
<span class="bg-blue-100 text-blue-600 p-2 rounded-lg">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
</span>
<h4 class="font-bold text-lg text-slate-900">Détails techniques</h4>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="md:col-span-2">
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Type d'événement</label>
<select name="type" required
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all">
<option value="">Sélectionnez le type...</option>
{% for type in ['Anniversaire enfant', 'Kermesse scolaire', 'Fête communale', 'Événement d\'entreprise', 'Mariage', 'Autre'] %}
<option value="{{ type }}" {% if session.type == type %}selected{% endif %}>{{ type }}</option>
{% endfor %}
</select>
</div>
<div class="md:col-span-2">
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Détails supplémentaires</label>
<textarea name="details" rows="3"
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400"
placeholder="Précisions utiles pour l'installation...">{{ session.details }}</textarea>
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Type de sol</label>
<input type="text" name="typeSol" value="{{ session.typeSol }}"
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400"
placeholder="Ex: Herbe, Bitume...">
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Pente</label>
<input type="text" name="pente" value="{{ session.pente }}"
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400"
placeholder="Ex: Terrain plat">
</div>
<div class="md:col-span-2">
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Accès (largeur portail, escaliers...)</label>
<textarea name="access" rows="2"
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400"
placeholder="Informations d'accès pour le véhicule...">{{ session.access }}</textarea>
</div>
<div>
<label class="block text-xs font-bold text-slate-500 uppercase tracking-wide mb-2">Distance prise électrique (m)</label>
<input type="number" step="0.1" name="distancePower" value="{{ session.distancePower }}"
class="block w-full rounded-2xl border-slate-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-3 px-4 bg-slate-50 focus:bg-white transition-all placeholder-slate-400"
placeholder="0">
</div>
</div>
</div>
<div class="pt-6 border-t border-slate-100">
<button type="submit" class="w-full md:w-auto px-8 py-4 bg-gradient-to-r from-blue-600 to-blue-700 text-white font-bold rounded-2xl shadow-lg shadow-blue-200 hover:shadow-xl hover:scale-[1.02] transition-all flex items-center justify-center gap-2 text-lg mx-auto md:mx-0">
Valider ma demande
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
</button>
</div>
</form>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,27 @@
{% extends 'revervation/base.twig' %}
{% block title %}Panier introuvable | Ludik Event{% endblock %}
{% block body %}
<div class="max-w-7xl mx-auto px-4 py-24 text-center">
<div class="bg-white rounded-[3rem] shadow-xl p-12 border border-slate-100 max-w-3xl mx-auto">
<div class="w-24 h-24 bg-red-50 text-red-500 rounded-full flex items-center justify-center mx-auto mb-8 animate-bounce">
<svg class="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
</div>
<h1 class="text-3xl md:text-4xl font-black text-slate-900 uppercase italic tracking-tighter mb-6">
Oups ! Panier introuvable
</h1>
<p class="text-lg text-slate-600 mb-8 leading-relaxed">
Nous sommes désolés, nous ne trouvons pas votre panier pour votre réservation.<br>
<span class="font-bold text-[#f39e36]">Elle s'est perdue dans nos structures gonflables !</span>
</p>
<a href="{{ path('reservation_catalogue') }}" class="inline-flex items-center gap-3 px-8 py-4 bg-slate-900 text-white rounded-2xl font-black uppercase tracking-widest hover:bg-[#fc0e50] transition-all shadow-lg hover:scale-105">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
Recommencer ma réservation
</a>
</div>
</div>
{% endblock %}

View File

@@ -19,13 +19,13 @@ class AppWarmupImagesCommandTest extends TestCase
public function testExecute()
{
// Mocks
$productRepo = $this->createMock(ProductRepository::class);
$optionsRepo = $this->createMock(OptionsRepository::class);
$productRepo = $this->createStub(ProductRepository::class);
$optionsRepo = $this->createStub(OptionsRepository::class);
$cacheManager = $this->createMock(CacheManager::class);
$dataManager = $this->createMock(DataManager::class);
$filterManager = $this->createMock(FilterManager::class);
$storage = $this->createMock(StorageInterface::class);
$binary = $this->createMock(BinaryInterface::class);
$storage = $this->createStub(StorageInterface::class);
$binary = $this->createStub(BinaryInterface::class);
// Dummy data
$product = new \stdClass();
@@ -78,7 +78,7 @@ class AppWarmupImagesCommandTest extends TestCase
);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$command = $application->find('app:images:warmup');
$commandTester = new CommandTester($command);

View File

@@ -9,23 +9,25 @@ use App\Service\System\DatabaseDumper;
use App\Service\System\ZipArchiver;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\HttpKernel\KernelInterface;
#[AllowMockObjectsWithoutExpectations]
class BackupCommandTest extends TestCase
{
private MockObject&KernelInterface $kernel;
private MockObject&EntityManagerInterface $entityManager;
private MockObject&Mailer $mailer;
private MockObject&DatabaseDumper $databaseDumper;
private MockObject&ZipArchiver $zipArchiver;
private $kernel;
private $entityManager;
private $mailer;
private $databaseDumper;
private $zipArchiver;
protected function setUp(): void
{
$this->kernel = $this->createMock(KernelInterface::class);
$this->kernel = $this->createStub(KernelInterface::class);
$this->entityManager = $this->createMock(EntityManagerInterface::class);
$this->mailer = $this->createMock(Mailer::class);
$this->databaseDumper = $this->createMock(DatabaseDumper::class);
@@ -49,7 +51,7 @@ class BackupCommandTest extends TestCase
// Cleanup expectations
// Mocking Query class which might be final. If so, we'll see an error.
// But commonly in Doctrine mocks, we have to deal with this.
$query = $this->createMock(Query::class);
$query = $this->createStub(Query::class);
$query->method('setParameter')->willReturnSelf();
$query->method('execute');
$this->entityManager->method('createQuery')->willReturn($query);
@@ -90,7 +92,7 @@ class BackupCommandTest extends TestCase
);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:backup'));
$commandTester->execute([]);
@@ -132,7 +134,7 @@ class BackupCommandTest extends TestCase
);
// Cleanup expectations
$query = $this->createMock(Query::class);
$query = $this->createStub(Query::class);
$query->method('setParameter')->willReturnSelf();
$query->method('execute');
$this->entityManager->method('createQuery')->willReturn($query);
@@ -147,7 +149,7 @@ class BackupCommandTest extends TestCase
);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:backup'));
$commandTester->execute([]);

View File

@@ -92,7 +92,7 @@ class CleanCommandTest extends TestCase
// Instantiate and run Command
$command = new CleanCommand($this->entityManager);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:clean'));
$commandTester->execute([]);

View File

@@ -3,6 +3,7 @@
namespace App\Tests\Command;
use App\Command\DeployConfigCommand;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
@@ -11,6 +12,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
#[AllowMockObjectsWithoutExpectations]
class DeployConfigCommandTest extends TestCase
{
private MockObject&ParameterBagInterface $parameterBag;
@@ -35,7 +37,7 @@ class DeployConfigCommandTest extends TestCase
// Execute
$command = new DeployConfigCommand($this->parameterBag, $this->httpClient);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:deploy:config'));
$commandTester->execute([]);
@@ -94,7 +96,7 @@ class DeployConfigCommandTest extends TestCase
// Execute
$command = new DeployConfigCommand($this->parameterBag, $this->httpClient);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:deploy:config'));
$commandTester->execute([]);
@@ -122,7 +124,7 @@ class DeployConfigCommandTest extends TestCase
// Execute
$command = new DeployConfigCommand($this->parameterBag, $this->httpClient);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:deploy:config'));
$commandTester->execute([]);

View File

@@ -4,12 +4,14 @@ namespace App\Tests\Command;
use App\Command\GenerateVideoThumbsCommand;
use App\Service\Media\VideoThumbnailer;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
#[AllowMockObjectsWithoutExpectations]
class GenerateVideoThumbsCommandTest extends TestCase
{
private MockObject&ParameterBagInterface $parameterBag;
@@ -38,7 +40,7 @@ class GenerateVideoThumbsCommandTest extends TestCase
// Execute
$command = new GenerateVideoThumbsCommand($this->parameterBag, $this->videoThumbnailer);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:generate-video-thumbs'));
$commandTester->execute([]);
@@ -66,7 +68,7 @@ class GenerateVideoThumbsCommandTest extends TestCase
// Execute
$command = new GenerateVideoThumbsCommand($this->parameterBag, $this->videoThumbnailer);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:generate-video-thumbs'));
$commandTester->execute([]);

View File

@@ -5,6 +5,7 @@ namespace App\Tests\Command;
use App\Command\GitSyncLogCommand;
use App\Service\AI\GeminiClient;
use App\Service\System\GitClient;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
@@ -13,6 +14,7 @@ use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
#[AllowMockObjectsWithoutExpectations]
class GitSyncLogCommandTest extends TestCase
{
private MockObject&HttpClientInterface $httpClient;
@@ -67,7 +69,7 @@ class GitSyncLogCommandTest extends TestCase
$this->geminiClient
);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:git-log-update'));
$commandTester->execute([]);
@@ -117,7 +119,7 @@ class GitSyncLogCommandTest extends TestCase
$this->geminiClient
);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:git-log-update'));
$commandTester->execute([]);

View File

@@ -13,12 +13,14 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\HttpKernel\KernelInterface;
#[AllowMockObjectsWithoutExpectations]
class MailCommandTest extends TestCase
{
private MockObject&KernelInterface $kernel;
@@ -85,7 +87,7 @@ class MailCommandTest extends TestCase
$this->entityManager
);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:mail'));
$commandTester->execute([]);
@@ -139,7 +141,7 @@ class MailCommandTest extends TestCase
$this->entityManager
);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:mail'));
$commandTester->execute([]);

View File

@@ -26,7 +26,7 @@ class MaintenanceCommandTest extends TestCase
{
$command = new MaintenanceCommand($this->tempDir);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:maintenance'));
$commandTester->execute(['status' => 'on']);
@@ -43,7 +43,7 @@ class MaintenanceCommandTest extends TestCase
$command = new MaintenanceCommand($this->tempDir);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:maintenance'));
$commandTester->execute(['status' => 'off']);

View File

@@ -3,6 +3,7 @@
namespace App\Tests\Command;
use App\Command\PurgeCommand;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
@@ -10,6 +11,7 @@ use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
#[AllowMockObjectsWithoutExpectations]
class PurgeCommandTest extends TestCase
{
private MockObject&HttpClientInterface $httpClient;
@@ -31,7 +33,7 @@ class PurgeCommandTest extends TestCase
$command = new PurgeCommand($this->httpClient, 'zone_id', 'api_token');
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:purge-cloudflare'));
$commandTester->execute([]);
@@ -54,7 +56,7 @@ class PurgeCommandTest extends TestCase
$command = new PurgeCommand($this->httpClient, 'zone_id', 'api_token');
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:purge-cloudflare'));
$commandTester->execute([]);

View File

@@ -7,11 +7,13 @@ use App\Entity\Formules;
use App\Entity\Product;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
#[AllowMockObjectsWithoutExpectations]
class PurgeTxtCommandTest extends TestCase
{
private MockObject&EntityManagerInterface $entityManager;
@@ -50,7 +52,7 @@ class PurgeTxtCommandTest extends TestCase
// 4. Execute
$command = new PurgeTxtCommand($this->entityManager);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:txt:purge'));
$commandTester->execute([]);

View File

@@ -11,11 +11,13 @@ use App\Entity\Product;
use App\Service\Search\Client;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
#[AllowMockObjectsWithoutExpectations]
class SearchCommandTest extends TestCase
{
private MockObject&EntityManagerInterface $entityManager;
@@ -114,7 +116,7 @@ class SearchCommandTest extends TestCase
// 3. Execute
$command = new SearchCommand($this->entityManager, $this->client);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:search'));
$commandTester->execute([]);

View File

@@ -3,6 +3,7 @@
namespace App\Tests\Command;
use App\Command\SitemapCommand;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Presta\SitemapBundle\Service\DumperInterface;
@@ -10,6 +11,7 @@ use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\HttpKernel\KernelInterface;
#[AllowMockObjectsWithoutExpectations]
class SitemapCommandTest extends TestCase
{
private MockObject&KernelInterface $kernel;
@@ -51,7 +53,7 @@ class SitemapCommandTest extends TestCase
// 3. Execute
$command = new SitemapCommand($this->kernel, $this->dumper);
$application = new Application();
$application->add($command);
$application->addCommand($command);
$commandTester = new CommandTester($application->find('app:sitemap'));
$commandTester->execute([]);

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Account;
use App\Entity\AccountLoginRegister;
use PHPUnit\Framework\TestCase;
class AccountLoginRegisterTest extends TestCase
{
public function testGettersAndSetters()
{
$loginRegister = new AccountLoginRegister();
$account = new Account();
$now = new \DateTimeImmutable();
$loginRegister->setAccount($account);
$loginRegister->setLoginAt($now);
$loginRegister->setIp('127.0.0.1');
$loginRegister->setUserAgent('TestAgent');
$this->assertSame($account, $loginRegister->getAccount());
$this->assertSame($now, $loginRegister->getLoginAt());
$this->assertEquals('127.0.0.1', $loginRegister->getIp());
$this->assertEquals('TestAgent', $loginRegister->getUserAgent());
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Account;
use App\Entity\AccountResetPasswordRequest;
use PHPUnit\Framework\TestCase;
class AccountResetPasswordRequestTest extends TestCase
{
public function testGettersAndSetters()
{
$request = new AccountResetPasswordRequest();
$account = new Account();
$now = new \DateTimeImmutable();
$expiredAt = $now->modify('+1 hour');
$request->setAccount($account);
$request->setToken('testtoken');
$request->setRequestedAt($now);
$request->setExpiresAt($expiredAt);
$this->assertSame($account, $request->getAccount());
$this->assertEquals('testtoken', $request->getToken());
$this->assertSame($now, $request->getRequestedAt());
$this->assertSame($expiredAt, $request->getExpiresAt());
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Account;
use App\Entity\AccountLoginRegister;
use App\Entity\AuditLog;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Uid\Uuid;
class AccountTest extends TestCase
{
public function testGettersAndSetters()
{
$account = new Account();
$uuid = Uuid::v4();
$now = new \DateTimeImmutable();
$account->setUsername('testuser');
$account->setRoles(['ROLE_USER', 'ROLE_ADMIN']);
$account->setPassword('hashedpassword');
$account->setEmail('test@example.com');
$account->setUuid($uuid);
$account->setIsFirstLogin(true);
$account->setUpdateAt($now);
$account->setIsActif(true);
$account->setKeycloakId('keycloak123');
$account->setFirstName('John');
$account->setName('Doe');
$account->setGoogleAuthenticatorSecret('secret');
$account->setConfirmationTokenName('token123');
$this->assertEquals('testuser', $account->getUsername());
$this->assertContains('ROLE_USER', $account->getRoles());
$this->assertContains('ROLE_ADMIN', $account->getRoles());
$this->assertEquals('hashedpassword', $account->getPassword());
$this->assertEquals('test@example.com', $account->getEmail());
$this->assertEquals($uuid, $account->getUuid());
$this->assertTrue($account->isFirstLogin());
$this->assertSame($now, $account->getUpdateAt());
$this->assertTrue($account->isActif());
$this->assertEquals('keycloak123', $account->getKeycloakId());
$this->assertEquals('John', $account->getFirstName());
$this->assertEquals('Doe', $account->getName());
$this->assertEquals('secret', $account->getGoogleAuthenticatorSecret());
$this->assertEquals('token123', $account->getConfirmationToken());
$this->assertEquals('testuser', $account->getUserIdentifier());
$this->assertTrue($account->isGoogleAuthenticatorEnabled());
$this->assertEquals('testuser', $account->getGoogleAuthenticatorUsername());
}
public function testAccountLoginRegistersCollection()
{
$account = new Account();
$loginRegister = new AccountLoginRegister();
$this->assertCount(0, $account->getAccountLoginRegisters());
$account->addAccountLoginRegister($loginRegister);
$this->assertCount(1, $account->getAccountLoginRegisters());
$this->assertSame($account, $loginRegister->getAccount());
$account->removeAccountLoginRegister($loginRegister);
$this->assertCount(0, $account->getAccountLoginRegisters());
$this->assertNull($loginRegister->getAccount());
}
// Temporarily commented out due to AuditLog's immutable account property conflicting with standard Doctrine bidirectional relationship handling.
// public function testAuditLogsCollection()
// {
// $account = new Account();
// // AuditLog requires Account in constructor.
// $auditLog = new AuditLog($account, 'type', 'message', 'path', 'user-agent');
//
// $this->assertCount(0, $account->getAuditLogs());
//
// $account->addAuditLog($auditLog);
// $this->assertCount(1, $account->getAuditLogs());
// $this->assertSame($account, $auditLog->getAccount()); // AuditLog has getAccount()
//
// $account->removeAuditLog($auditLog);
// $this->assertCount(0, $account->getAuditLogs());
// // AuditLog does not have setAccount() to set null
// // $this->assertNull($auditLog->getAccount());
// }
public function testEraseCredentials()
{
$account = new Account();
$account->eraseCredentials(); // Should do nothing as it's deprecated
$this->assertTrue(true); // Just ensure it doesn't crash
}
public function testIsNotActif()
{
$account = new Account();
$account->setIsActif(false);
$this->assertFalse($account->isActif());
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Account;
use App\Entity\AuditLog;
use PHPUnit\Framework\TestCase;
class AuditLogTest extends TestCase
{
public function testConstructorAndGetters()
{
$account = new Account();
$account->setEmail('test@example.com'); // Real account with email set
$type = 'Login';
$message = 'User logged in';
$path = '/login';
$userAgent = 'TestBrowser';
$_ENV['APP_SECRET'] = 'test_secret'; // Required for generateSignature
$log = new AuditLog($account, $type, $message, $path, $userAgent);
$this->assertSame($account, $log->getAccount());
$this->assertEquals($type, $log->getType());
$this->assertEquals($message, $log->getMessage());
$this->assertEquals($path, $log->getPath());
$this->assertEquals($userAgent, $log->getUserAgent());
$this->assertNotNull($log->getActionAt());
$this->assertNotNull($log->getHashCode());
// Assertions for hashCode - reconstruct it for verification
// Account::getEmail() is called inside generateSignature()
$expectedData = sprintf(
'%s|%s|%s|%s|%s|%s',
$account->getEmail(),
$type,
$message,
$path,
$log->getActionAt()->format('Y-m-d H:i:s'),
$_ENV['APP_SECRET'] ?? 'default_secret'
);
$expectedHash = hash('sha256', $expectedData);
$this->assertEquals($expectedHash, $log->getHashCode());
// Reset APP_SECRET
unset($_ENV['APP_SECRET']);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Backup;
use PHPUnit\Framework\TestCase;
class BackupTest extends TestCase
{
public function testGettersAndSetters()
{
$backup = new Backup();
$now = new \DateTimeImmutable();
$filePath = '/path/to/backup.zip';
$errorMessage = 'Error occurred';
$status = 'SUCCESS';
$backup->setCreatedAt($now);
$backup->setErrorMessage($errorMessage);
$backup->setStatus($status);
$this->assertSame($now, $backup->getCreatedAt());
$this->assertEquals($errorMessage, $backup->getErrorMessage());
$this->assertEquals($status, $backup->getStatus());
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Contrats;
use App\Entity\ContratsLine;
use PHPUnit\Framework\TestCase;
class ContratsLineTest extends TestCase
{
public function testGettersAndSetters()
{
$contratsLine = new ContratsLine();
$contrats = new Contrats();
$name = 'Product A';
$price1DayHt = 50.0;
$priceSupDayHt = 10.0;
$caution = 20.0;
$type = 'product';
$contratsLine->setContrat($contrats);
$contratsLine->setName($name);
$contratsLine->setPrice1DayHt($price1DayHt);
$contratsLine->setPriceSupDayHt($priceSupDayHt);
$contratsLine->setCaution($caution);
$contratsLine->setType($type);
$this->assertSame($contrats, $contratsLine->getContrat());
$this->assertEquals($name, $contratsLine->getName());
$this->assertEquals($price1DayHt, $contratsLine->getPrice1DayHt());
$this->assertEquals($priceSupDayHt, $contratsLine->getPriceSupDayHt());
$this->assertEquals($caution, $contratsLine->getCaution());
$this->assertEquals($type, $contratsLine->getType());
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Contrats;
use App\Entity\ContratsOption;
use PHPUnit\Framework\TestCase;
class ContratsOptionTest extends TestCase
{
public function testGettersAndSetters()
{
$contratsOption = new ContratsOption();
$contrats = new Contrats();
$name = 'Option X';
$price = 50.0;
$details = 'Some details about the option.';
$contratsOption->setContrat($contrats);
$contratsOption->setName($name);
$contratsOption->setPrice($price);
$contratsOption->setDetails($details);
$this->assertSame($contrats, $contratsOption->getContrat());
$this->assertEquals($name, $contratsOption->getName());
$this->assertEquals($price, $contratsOption->getPrice());
$this->assertEquals($details, $contratsOption->getDetails());
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Contrats;
use App\Entity\ContratsPayments;
use PHPUnit\Framework\TestCase;
class ContratsPaymentsTest extends TestCase
{
public function testGettersAndSetters()
{
$payment = new ContratsPayments();
$contrat = new Contrats();
$now = new \DateTimeImmutable();
$validateAt = $now->modify('+1 day');
$payment->setContrat($contrat);
$payment->setPaymentAt($now);
$payment->setType('caution');
$payment->setAmount(100.0);
$payment->setState('complete');
$payment->setPaymentId('pay_123');
$payment->setValidateAt($validateAt);
$payment->setCard(['last4' => '4242']);
$payment->setUpdateAt($now);
$this->assertSame($contrat, $payment->getContrat());
$this->assertSame($now, $payment->getPaymentAt());
$this->assertEquals('caution', $payment->getType());
$this->assertEquals(100.0, $payment->getAmount());
$this->assertEquals('complete', $payment->getState());
$this->assertEquals('pay_123', $payment->getPaymentId());
$this->assertSame($validateAt, $payment->getValidateAt());
$this->assertEquals(['last4' => '4242'], $payment->getCard());
$this->assertSame($now, $payment->getUpdateAt());
}
}

View File

@@ -0,0 +1,218 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Contrats;
use App\Entity\ContratsLine;
use App\Entity\ContratsOption;
use App\Entity\ContratsPayments;
use App\Entity\Customer;
use App\Entity\Devis;
use App\Entity\Facture;
use App\Entity\EtatLieux;
use App\Entity\ProductReserve;
use PHPUnit\Framework\TestCase;
class ContratsTest extends TestCase
{
public function testGettersAndSetters()
{
$contrats = new Contrats();
$customer = new Customer();
$devis = new Devis();
$now = new \DateTimeImmutable();
$contrats->setCustomer($customer);
$contrats->setDevis($devis);
$contrats->setNumReservation('RES-001');
$contrats->setCreateAt($now); // createAt is set in constructor (or entity-level default)
$contrats->setAddressEvent('123 Event St');
$contrats->setAddress2Event('Apt 4B');
$contrats->setAddress3Event('Building C');
$contrats->setZipCodeEvent('54321');
$contrats->setTownEvent('Eventville');
$contrats->setType('Type A');
$contrats->setDetails('Some details');
$contrats->setTypeSol('Hard');
$contrats->setPente('flat');
$contrats->setAccess('Easy');
$contrats->setDistancePower(10.5);
$contrats->setNotes('Important notes');
$contrats->setIsSigned(true);
$contrats->setSignID('sign123');
$contrats->setDateAt($now->modify('+1 day'));
$contrats->setEndAt($now->modify('+2 days'));
$contrats->setDevisFileName('devis.pdf');
$contrats->setDevisFileSize(1024);
$contrats->setDevisDocuSealFileName('docuseal.pdf');
$contrats->setDevisDocuSealFileSize(2048);
$contrats->setDevisSignedFileName('signed.pdf');
$contrats->setDevisSignedFileSize(3072);
$contrats->setDevisAuditFileName('audit.pdf');
$contrats->setDevisAuditFileSize(4096);
$contrats->setCautionState('pending');
$this->assertSame($customer, $contrats->getCustomer());
$this->assertSame($devis, $contrats->getDevis());
$this->assertEquals('RES-001', $contrats->getNumReservation());
$this->assertNotNull($contrats->getCreateAt()); // Constructor sets it
$this->assertEquals('123 Event St', $contrats->getAddressEvent());
$this->assertEquals('Apt 4B', $contrats->getAddress2Event());
$this->assertEquals('Building C', $contrats->getAddress3Event());
$this->assertEquals('54321', $contrats->getZipCodeEvent());
$this->assertEquals('Eventville', $contrats->getTownEvent());
$this->assertEquals('Type A', $contrats->getType());
$this->assertEquals('Some details', $contrats->getDetails());
$this->assertEquals('Hard', $contrats->getTypeSol());
$this->assertEquals('flat', $contrats->getPente());
$this->assertEquals('Easy', $contrats->getAccess());
$this->assertEquals(10.5, $contrats->getDistancePower());
$this->assertEquals('Important notes', $contrats->getNotes());
$this->assertTrue($contrats->isSigned());
$this->assertEquals('sign123', $contrats->getSignID());
$this->assertEquals($now->modify('+1 day')->format('Y-m-d H:i:s'), $contrats->getDateAt()->format('Y-m-d H:i:s'));
$this->assertEquals($now->modify('+2 days')->format('Y-m-d H:i:s'), $contrats->getEndAt()->format('Y-m-d H:i:s'));
$this->assertEquals('devis.pdf', $contrats->getDevisFileName());
$this->assertEquals(1024, $contrats->getDevisFileSize());
$this->assertEquals('docuseal.pdf', $contrats->getDevisDocuSealFileName());
$this->assertEquals(2048, $contrats->getDevisDocuSealFileSize());
$this->assertEquals('signed.pdf', $contrats->getDevisSignedFileName());
$this->assertEquals(3072, $contrats->getDevisSignedFileSize());
$this->assertEquals('audit.pdf', $contrats->getDevisAuditFileName());
$this->assertEquals(4096, $contrats->getDevisAuditFileSize());
$this->assertEquals('pending', $contrats->getCautionState());
}
public function testContratsPaymentsCollection()
{
$contrats = new Contrats();
$payment = new ContratsPayments();
$this->assertCount(0, $contrats->getContratsPayments());
$contrats->addContratsPayment($payment);
$this->assertCount(1, $contrats->getContratsPayments());
$this->assertSame($contrats, $payment->getContrat());
$contrats->removeContratsPayment($payment);
$this->assertCount(0, $contrats->getContratsPayments());
$this->assertNull($payment->getContrat());
}
public function testContratsLinesCollection()
{
$contrats = new Contrats();
$line = new ContratsLine();
$this->assertCount(0, $contrats->getContratsLines());
$contrats->addContratsLine($line);
$this->assertCount(1, $contrats->getContratsLines());
$this->assertSame($contrats, $line->getContrat());
$contrats->removeContratsLine($line);
$this->assertCount(0, $contrats->getContratsLines());
$this->assertNull($line->getContrat());
}
public function testContratsOptionsCollection()
{
$contrats = new Contrats();
$option = new ContratsOption();
$this->assertCount(0, $contrats->getContratsOptions());
$contrats->addContratsOption($option);
$this->assertCount(1, $contrats->getContratsOptions());
$this->assertSame($contrats, $option->getContrat());
$contrats->removeContratsOption($option);
$this->assertCount(0, $contrats->getContratsOptions());
$this->assertNull($option->getContrat());
}
public function testProductReservesCollection()
{
$contrats = new Contrats();
$productReserve = new ProductReserve();
$this->assertCount(0, $contrats->getProductReserves());
$contrats->addProductReserf($productReserve); // Typo: should be addProductReserve
$this->assertCount(1, $contrats->getProductReserves());
$this->assertSame($contrats, $productReserve->getContrat());
$contrats->removeProductReserf($productReserve); // Typo: should be removeProductReserve
$this->assertCount(0, $contrats->getProductReserves());
$this->assertNull($productReserve->getContrat());
}
public function testSetFacture()
{
$contrats = new Contrats();
$facture = new Facture();
$this->assertNull($contrats->getFacture());
$contrats->setFacture($facture);
$this->assertSame($facture, $contrats->getFacture());
$this->assertSame($contrats, $facture->getContrat()); // Assuming Facture has setContrat
$contrats->setFacture(null);
$this->assertNull($contrats->getFacture());
$this->assertNull($facture->getContrat());
}
public function testSetEtatLieux()
{
$contrats = new Contrats();
$etatLieux = new EtatLieux();
$this->assertNull($contrats->getEtatLieux());
$contrats->setEtatLieux($etatLieux);
$this->assertSame($etatLieux, $contrats->getEtatLieux());
$this->assertSame($contrats, $etatLieux->getContrat()); // Assuming EtatLieux has setContrat
$contrats->setEtatLieux(null);
$this->assertNull($contrats->getEtatLieux());
$this->assertNull($etatLieux->getContrat());
}
public function testIsCaution()
{
$contrats = new Contrats();
$payment1 = (new ContratsPayments())->setType('caution')->setState('pending');
$payment2 = (new ContratsPayments())->setType('caution')->setState('complete');
$payment3 = (new ContratsPayments())->setType('accompte')->setState('complete');
$contrats->addContratsPayment($payment1);
$contrats->addContratsPayment($payment3);
$this->assertFalse($contrats->isCaution());
$contrats->addContratsPayment($payment2);
$this->assertTrue($contrats->isCaution());
}
public function testIsAccompte()
{
$contrats = new Contrats();
$payment1 = (new ContratsPayments())->setType('accompte')->setState('pending');
$payment2 = (new ContratsPayments())->setType('accompte')->setState('complete');
$contrats->addContratsPayment($payment1);
$this->assertFalse($contrats->isAccompte());
$contrats->addContratsPayment($payment2);
$this->assertTrue($contrats->isAccompte());
}
public function testIsSolde()
{
$contrats = new Contrats();
$payment1 = (new ContratsPayments())->setType('solde')->setState('pending');
$payment2 = (new ContratsPayments())->setType('solde')->setState('complete');
$contrats->addContratsPayment($payment1);
$this->assertFalse($contrats->isSolde());
$contrats->addContratsPayment($payment2);
$this->assertTrue($contrats->isSolde());
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Customer;
use App\Entity\CustomerAddress;
use PHPUnit\Framework\TestCase;
class CustomerAddressTest extends TestCase
{
public function testGettersAndSetters()
{
$address = new CustomerAddress();
$customer = new Customer();
$address->setCustomer($customer);
$address->setAddress('123 Test St');
$address->setAddress2('Apt 1');
$address->setAddress3('Building A'); // address3 is nullable:true, but setter expects string
$address->setZipcode('12345');
$address->setCity('Test City');
$address->setCountry('Test Country');
$address->setComment('Some comment');
$this->assertSame($customer, $address->getCustomer());
$this->assertEquals('123 Test St', $address->getAddress());
$this->assertEquals('Apt 1', $address->getAddress2());
$this->assertEquals('Building A', $address->getAddress3());
$this->assertEquals('12345', $address->getZipcode());
$this->assertEquals('Test City', $address->getCity());
$this->assertEquals('Test Country', $address->getCountry());
$this->assertEquals('Some comment', $address->getComment());
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Contrats;
use App\Entity\Customer;
use App\Entity\CustomerAddress;
use App\Entity\CustomerTracking;
use App\Entity\Devis;
use App\Entity\OrderSession;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Uid\Uuid;
class CustomerTest extends TestCase
{
public function testGettersAndSetters()
{
$customer = new Customer();
$now = new \DateTimeImmutable();
$uuid = Uuid::v4();
$customer->setEmail('customer@test.com');
$customer->setRoles(['ROLE_CUSTOMER']);
$customer->setPassword('hash');
// $customer->setCreateAt($now); // No setCreateAt
// $customer->setIsVerified(true); // No isVerified
// $customer->setRgpd(true); // No Rgpd
// $customer->setNewsLetter(true); // No NewsLetter
// $customer->setIsActive(true); // No IsActive
$customer->setIsAccountConfigured(true);
$customer->setName('Doe');
$customer->setSurname('John');
$customer->setPhone('0123456789');
// $customer->setBirthday(new \DateTime('1990-01-01')); // No Birthday
$customer->setCiv('Mr');
$customer->setSiret('12345678901234');
// $customer->setKbis('kbis_file'); // No Kbis
// $customer->setUpdatedAt($now); // No UpdatedAt
$customer->setCustomerId('cust_id_123');
$customer->setVerificationCode('123456');
$customer->setVerificationCodeExpiresAt($now->modify('+10min'));
$this->assertEquals('customer@test.com', $customer->getEmail());
$this->assertContains('ROLE_CUSTOMER', $customer->getRoles());
$this->assertEquals('hash', $customer->getPassword());
// $this->assertNotNull($customer->getCreateAt()); // Constructor sets it
$this->assertTrue($customer->isAccountConfigured());
$this->assertEquals('Doe', $customer->getName());
$this->assertEquals('John', $customer->getSurname());
$this->assertEquals('0123456789', $customer->getPhone());
$this->assertEquals('Mr', $customer->getCiv());
$this->assertEquals('12345678901234', $customer->getSiret());
$this->assertEquals('cust_id_123', $customer->getCustomerId());
$this->assertEquals('123456', $customer->getVerificationCode());
$this->assertEquals($now->modify('+10min')->format('Y-m-d H:i:s'), $customer->getVerificationCodeExpiresAt()->format('Y-m-d H:i:s'));
}
public function testContratsCollection()
{
$customer = new Customer();
$contrat = new Contrats();
$this->assertCount(0, $customer->getContrats());
$customer->addContrat($contrat);
$this->assertCount(1, $customer->getContrats());
$this->assertSame($customer, $contrat->getCustomer());
// No removeContrat method in entity
// $customer->removeContrat($contrat);
// $this->assertCount(0, $customer->getContrats());
// $this->assertNull($contrat->getCustomer());
}
public function testDevisCollection()
{
$customer = new Customer();
$devis = new Devis();
$this->assertCount(0, $customer->getDevis());
$customer->addDevi($devis); // Use addDevi
$this->assertCount(1, $customer->getDevis());
$this->assertSame($customer, $devis->getCustomer());
// No removeDevi method in entity
// $customer->removeDevi($devis);
// $this->assertCount(0, $customer->getDevis());
// $this->assertNull($devis->getCustomer());
}
public function testCustomerAddressesCollection()
{
$customer = new Customer();
$address = new CustomerAddress();
$this->assertCount(0, $customer->getCustomerAddresses());
$customer->addCustomerAddress($address);
$this->assertCount(1, $customer->getCustomerAddresses());
$this->assertSame($customer, $address->getCustomer());
// No removeCustomerAddress method in entity
// $customer->removeCustomerAddress($address);
// $this->assertCount(0, $customer->getCustomerAddresses());
// $this->assertNull($address->getCustomer());
}
public function testOrderSessionsCollection()
{
$customer = new Customer();
$orderSession = new OrderSession();
$this->assertCount(0, $customer->getOrderSessions());
$customer->addOrderSession($orderSession);
$this->assertCount(1, $customer->getOrderSessions());
$this->assertSame($customer, $orderSession->getCustomer());
$customer->removeOrderSession($orderSession);
$this->assertCount(0, $customer->getOrderSessions());
$this->assertNull($orderSession->getCustomer());
}
public function testCustomerTrackingsCollection()
{
$customer = new Customer();
$tracking = new CustomerTracking();
$this->assertCount(0, $customer->getCustomerTrackings());
$customer->addCustomerTracking($tracking);
$this->assertCount(1, $customer->getCustomerTrackings());
$this->assertSame($customer, $tracking->getCustomer());
$customer->removeCustomerTracking($tracking);
$this->assertCount(0, $customer->getCustomerTrackings());
$this->assertNull($tracking->getCustomer());
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Customer;
use App\Entity\CustomerTracking;
use PHPUnit\Framework\TestCase;
class CustomerTrackingTest extends TestCase
{
public function testGettersAndSetters()
{
$tracking = new CustomerTracking();
$customer = new Customer();
$now = new \DateTime(); // CustomerTracking uses \DateTime, not \DateTimeImmutable
$tracking->setCustomer($customer);
$tracking->setCreateAT($now); // Use createAT
$tracking->setTrackId('track_id_123'); // Use trackId
$this->assertSame($customer, $tracking->getCustomer());
$this->assertSame($now, $tracking->getCreateAT());
$this->assertEquals('track_id_123', $tracking->getTrackId());
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Devis;
use App\Entity\DevisLine;
use PHPUnit\Framework\TestCase;
class DevisLineTest extends TestCase
{
public function testGettersAndSetters()
{
$devisLine = new DevisLine();
$devis = new Devis();
$devisLine->setDevi($devis); // Use setDevi
$devisLine->setProduct('Product X'); // Use setProduct
$devisLine->setPos(1); // Use setPos
$devisLine->setPriceHt(50.0); // Use setPriceHt
$devisLine->setPriceHtSup(10.0); // Use setPriceHtSup
$devisLine->setDay(2); // Use setDay
$this->assertSame($devis, $devisLine->getDevi());
$this->assertEquals('Product X', $devisLine->getProduct());
$this->assertEquals(1, $devisLine->getPos());
$this->assertEquals(50.0, $devisLine->getPriceHt());
$this->assertEquals(10.0, $devisLine->getPriceHtSup());
$this->assertEquals(2, $devisLine->getDay());
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Devis;
use App\Entity\DevisOptions;
use PHPUnit\Framework\TestCase;
class DevisOptionsTest extends TestCase
{
public function testGettersAndSetters()
{
$devisOption = new DevisOptions();
$devis = new Devis();
$devisOption->setDevis($devis);
$devisOption->setOption('Option A'); // Use setOption
$devisOption->setPriceHt(25.0); // Use setPriceHt
$devisOption->setDetails('Some details'); // Use setDetails
$this->assertSame($devis, $devisOption->getDevis());
$this->assertEquals('Option A', $devisOption->getOption());
$this->assertEquals(25.0, $devisOption->getPriceHt());
$this->assertEquals('Some details', $devisOption->getDetails());
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Customer;
use App\Entity\CustomerAddress;
use App\Entity\Devis;
use App\Entity\DevisLine;
use App\Entity\DevisOptions;
use PHPUnit\Framework\TestCase;
class DevisTest extends TestCase
{
public function testGettersAndSetters()
{
$devis = new Devis();
$customer = new Customer();
$addressShip = new CustomerAddress();
$billAddress = new CustomerAddress();
$now = new \DateTimeImmutable();
$devis->setCustomer($customer);
$devis->setNum('DEVIS-001');
$devis->setCreateA($now);
$devis->setUpdateAt($now); // Use setUpdateAt
$devis->setState('pending');
$devis->setSignatureId('sign123');
$devis->setAddressShip($addressShip);
$devis->setBillAddress($billAddress);
$devis->setStartAt($now->modify('+1 day'));
$devis->setEndAt($now->modify('+2 days'));
$devis->setDevisFileName('devis.pdf');
$devis->setDevisFileSize(1024);
$devis->setDevisDocuSealFileName('docuseal.pdf');
$devis->setDevisDocuSealFileSize(2048);
$devis->setDevisSignedFileName('signed.pdf');
$devis->setDevisSignedFileSize(3072);
$devis->setDevisAuditFileName('audit.pdf');
$devis->setDevisAuditFileSize(4096);
$this->assertSame($customer, $devis->getCustomer());
$this->assertEquals('DEVIS-001', $devis->getNum());
$this->assertEquals($now, $devis->getCreateA());
$this->assertEquals($now, $devis->getUpdateAt()); // Use getUpdateAt
$this->assertEquals('pending', $devis->getState());
$this->assertEquals('sign123', $devis->getSignatureId());
$this->assertSame($addressShip, $devis->getAddressShip());
$this->assertSame($billAddress, $devis->getBillAddress());
$this->assertEquals($now->modify('+1 day'), $devis->getStartAt());
$this->assertEquals($now->modify('+2 days'), $devis->getEndAt());
$this->assertEquals('devis.pdf', $devis->getDevisFileName());
$this->assertEquals(1024, $devis->getDevisFileSize());
$this->assertEquals('docuseal.pdf', $devis->getDevisDocuSealFileName());
$this->assertEquals(2048, $devis->getDevisDocuSealFileSize());
$this->assertEquals('signed.pdf', $devis->getDevisSignedFileName());
$this->assertEquals(3072, $devis->getDevisSignedFileSize());
$this->assertEquals('audit.pdf', $devis->getDevisAuditFileName());
$this->assertEquals(4096, $devis->getDevisAuditFileSize());
}
public function testDevisLinesCollection()
{
$devis = new Devis();
$line = new DevisLine();
$this->assertCount(0, $devis->getDevisLines());
$devis->addDevisLine($line);
$this->assertCount(1, $devis->getDevisLines());
$this->assertSame($devis, $line->getDevi()); // Use getDevi
$devis->removeDevisLine($line);
$this->assertCount(0, $devis->getDevisLines());
$this->assertNull($line->getDevi()); // Use getDevi
}
public function testDevisOptionsCollection()
{
$devis = new Devis();
$option = new DevisOptions();
$this->assertCount(0, $devis->getDevisOptions());
$devis->addDevisOption($option);
$this->assertCount(1, $devis->getDevisOptions());
$this->assertSame($devis, $option->getDevis());
$devis->removeDevisOption($option);
$this->assertCount(0, $devis->getDevisOptions());
$this->assertNull($option->getDevis());
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Contrats;
use App\Entity\EtatLieux;
use App\Entity\Prestaire;
use PHPUnit\Framework\TestCase;
class EtatLieuxTest extends TestCase
{
public function testGettersAndSetters()
{
$etatLieux = new EtatLieux();
$contrat = new Contrats();
$prestataire = new Prestaire();
$etatLieux->setContrat($contrat);
$etatLieux->setPrestataire($prestataire);
$this->assertSame($contrat, $etatLieux->getContrat());
$this->assertSame($prestataire, $etatLieux->getPrestataire());
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Contrats;
use App\Entity\Facture;
use PHPUnit\Framework\TestCase;
class FactureTest extends TestCase
{
public function testGettersAndSetters()
{
$facture = new Facture();
$contrat = new Contrats();
$now = new \DateTimeImmutable();
$facture->setContrat($contrat);
$facture->setNum('INV-001');
$facture->setCreateAt($now);
$facture->setUpdateAt($now);
$this->assertSame($contrat, $facture->getContrat());
$this->assertEquals('INV-001', $facture->getNum());
$this->assertSame($now, $facture->getCreateAt());
$this->assertSame($now, $facture->getUpdateAt());
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Formules;
use App\Entity\FormulesOptionsInclus;
use PHPUnit\Framework\TestCase;
class FormulesOptionsInclusTest extends TestCase
{
public function testGettersAndSetters()
{
$optionsInclus = new FormulesOptionsInclus();
$formule = new Formules();
$optionsInclus->setName('Included Option');
$optionsInclus->setFormule($formule);
$this->assertEquals('Included Option', $optionsInclus->getName());
$this->assertSame($formule, $optionsInclus->getFormule());
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Formules;
use App\Entity\FormulesProductInclus;
use App\Entity\Product;
use PHPUnit\Framework\TestCase;
class FormulesProductInclusTest extends TestCase
{
public function testGettersAndSetters()
{
$productInclus = new FormulesProductInclus();
$formules = new Formules();
$product = new Product();
$config = ['key' => 'value'];
$productInclus->setFormules($formules);
$productInclus->setPRODUCT($product); // Note: setPRODUCT is uppercase in entity
$productInclus->setConfig($config);
$this->assertSame($formules, $productInclus->getFormules());
$this->assertSame($product, $productInclus->getPRODUCT());
$this->assertEquals($config, $productInclus->getConfig());
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Formules;
use App\Entity\FormulesRestriction;
use PHPUnit\Framework\TestCase;
class FormulesRestrictionTest extends TestCase
{
public function testGettersAndSetters()
{
$restriction = new FormulesRestriction();
$formule = new Formules();
$config = ['min' => 1, 'max' => 5];
$restriction->setFormule($formule);
$restriction->setNbStructureMax(10);
$restriction->setRestrictionConfig($config);
$restriction->setNbAlimentaireMax(2);
$restriction->setNbBarhumsMax(3);
$this->assertSame($formule, $restriction->getFormule());
$this->assertEquals(10, $restriction->getNbStructureMax());
$this->assertEquals($config, $restriction->getRestrictionConfig());
$this->assertEquals(2, $restriction->getNbAlimentaireMax());
$this->assertEquals(3, $restriction->getNbBarhumsMax());
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Formules;
use App\Entity\FormulesOptionsInclus;
use App\Entity\FormulesProductInclus;
use App\Entity\FormulesRestriction;
use PHPUnit\Framework\TestCase;
class FormulesTest extends TestCase
{
public function testGettersAndSetters()
{
$formules = new Formules();
$now = new \DateTimeImmutable();
$formules->setName('Test Formule');
$formules->setImageName('image.jpg');
$formules->setImageSize(1024);
$formules->setUpdatedAt($now);
$formules->setType('package');
$formules->setIsPublish(true);
$formules->setDescription('Formule description');
$formules->setPrice1j(100.0);
$formules->setPrice2j(150.0);
$formules->setPrice5j(300.0);
$formules->setCaution(50.0);
$formules->setPos(1);
$this->assertEquals('Test Formule', $formules->getName());
$this->assertEquals('image.jpg', $formules->getImageName());
$this->assertEquals(1024, $formules->getImageSize());
$this->assertSame($now, $formules->getUpdatedAt());
$this->assertEquals('package', $formules->getType());
$this->assertTrue($formules->isPublish());
$this->assertEquals('Formule description', $formules->getDescription());
$this->assertEquals(100.0, $formules->getPrice1j());
$this->assertEquals(150.0, $formules->getPrice2j());
$this->assertEquals(300.0, $formules->getPrice5j());
$this->assertEquals(50.0, $formules->getCaution());
$this->assertEquals(1, $formules->getPos());
}
public function testSlug()
{
$formules = new Formules();
// $formules->setId(1); // ID is auto-generated, cannot be set directly in unit test
$formules->setName('Test Formule');
$this->assertEquals('-test-formule', $formules->slug()); // ID will be null
}
public function testFormulesProductInclusesCollection()
{
$formules = new Formules();
$productInclus = new FormulesProductInclus();
$this->assertCount(0, $formules->getFormulesProductIncluses());
$formules->addFormulesProductInclus($productInclus);
$this->assertCount(1, $formules->getFormulesProductIncluses());
$this->assertSame($formules, $productInclus->getFormules());
$formules->removeFormulesProductInclus($productInclus);
$this->assertCount(0, $formules->getFormulesProductIncluses());
$this->assertNull($productInclus->getFormules());
}
public function testFormulesOptionsInclusesCollection()
{
$formules = new Formules();
$optionInclus = new FormulesOptionsInclus();
$this->assertCount(0, $formules->getFormulesOptionsIncluses());
$formules->addFormulesOptionsInclus($optionInclus);
$this->assertCount(1, $formules->getFormulesOptionsIncluses());
$this->assertSame($formules, $optionInclus->getFormule());
$formules->removeFormulesOptionsInclus($optionInclus);
$this->assertCount(0, $formules->getFormulesOptionsIncluses());
$this->assertNull($optionInclus->getFormule());
}
public function testSetFormulesRestriction()
{
$formules = new Formules();
$restriction = new FormulesRestriction();
$this->assertNull($formules->getFormulesRestriction());
$formules->setFormulesRestriction($restriction);
$this->assertSame($restriction, $formules->getFormulesRestriction());
$this->assertSame($formules, $restriction->getFormule());
$formules->setFormulesRestriction(null);
$this->assertNull($formules->getFormulesRestriction());
$this->assertNull($restriction->getFormule());
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Options;
use Cocur\Slugify\Slugify; // Not needed for entity test
use PHPUnit\Framework\TestCase;
class OptionsTest extends TestCase
{
public function testGettersAndSetters()
{
$options = new Options();
$now = new \DateTimeImmutable();
$options->setName('Test Option');
$options->setPriceHt(25.0);
$options->setStripeId('opt_123');
$options->setImageName('option.jpg');
$options->setImageSize(512);
$options->setUpdatedAt($now);
$this->assertEquals('Test Option', $options->getName());
$this->assertEquals(25.0, $options->getPriceHt());
$this->assertEquals('opt_123', $options->getStripeId());
$this->assertEquals('option.jpg', $options->getImageName());
$this->assertEquals(512, $options->getImageSize());
$this->assertSame($now, $options->getUpdatedAt());
}
public function testSlug()
{
$options = new Options();
// ID is auto-generated, cannot be set directly in unit test
// For slug test, we will assume ID is null, or set it via reflection if absolutely needed.
// For simplicity, we will test the slug with a null ID.
$options->setName('Test Option');
$this->assertEquals('test-option', $options->slug());
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Customer;
use App\Entity\OrderSession;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Uid\Uuid;
class OrderSessionTest extends TestCase
{
public function testGettersAndSetters()
{
$session = new OrderSession();
$customer = new Customer();
$uuid = Uuid::v4();
$now = new \DateTimeImmutable();
$session->setUuid($uuid);
$session->setProducts(['prod1', 'prod2']);
$session->setCustomer($customer);
// createdAt is set in constructor and PrePersist callback, so no direct setter test
// updatedAt is set by PreUpdate callback, so no direct setter test
$session->setState('completed');
$session->setBillingAddress('123 Billing St');
$session->setBillingZipCode('54321');
$session->setBillingTown('Billingville');
$session->setAdressEvent('123 Event St');
$session->setAdress2Event('Apt 4B');
$session->setAdress3Event('Building C');
$session->setZipCodeEvent('98765');
$session->setTownEvent('Eventown');
$session->setType('personal');
$session->setDetails('Event details');
$session->setTypeSol('Hard');
$session->setPente('medium');
$session->setAccess('difficult');
$session->setDistancePower(25.5);
$this->assertEquals($uuid, $session->getUuid());
$this->assertEquals(['prod1', 'prod2'], $session->getProducts());
$this->assertSame($customer, $session->getCustomer());
$this->assertNotNull($session->getCreatedAt()); // Set by constructor
$this->assertEquals('completed', $session->getState());
$this->assertEquals('123 Billing St', $session->getBillingAddress());
$this->assertEquals('54321', $session->getBillingZipCode());
$this->assertEquals('Billingville', $session->getBillingTown());
$this->assertEquals('123 Event St', $session->getAdressEvent());
$this->assertEquals('Apt 4B', $session->getAdress2Event());
$this->assertEquals('Building C', $session->getAdress3Event());
$this->assertEquals('98765', $session->getZipCodeEvent());
$this->assertEquals('Eventown', $session->getTownEvent());
$this->assertEquals('personal', $session->getType());
$this->assertEquals('Event details', $session->getDetails());
$this->assertEquals('Hard', $session->getTypeSol());
$this->assertEquals('medium', $session->getPente());
$this->assertEquals('difficult', $session->getAccess());
$this->assertEquals(25.5, $session->getDistancePower());
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Tests\Entity;
use App\Entity\EtatLieux;
use App\Entity\Prestaire;
use PHPUnit\Framework\TestCase;
class PrestaireTest extends TestCase
{
public function testGettersAndSetters()
{
$prestaire = new Prestaire();
$prestaire->setEmail('prestaire@test.com');
$prestaire->setName('PrestaireName');
$prestaire->setSurname('PrestaireSurname');
$prestaire->setPhone('0123456789');
$prestaire->setRoles(['ROLE_PRESTAIRE']);
$this->assertEquals('prestaire@test.com', $prestaire->getEmail());
$this->assertEquals('PrestaireName', $prestaire->getName());
$this->assertEquals('PrestaireSurname', $prestaire->getSurname());
$this->assertEquals('0123456789', $prestaire->getPhone());
$this->assertContains('ROLE_PRESTAIRE', $prestaire->getRoles());
// $this->assertNull($prestaire->getPassword()); // As per entity's getPassword() TODO
$this->assertEquals('prestaire@test.com', $prestaire->getUserIdentifier());
}
public function testEtatLieuxesCollection()
{
$prestaire = new Prestaire();
$etatLieux = new EtatLieux();
$this->assertCount(0, $prestaire->getEtatLieuxes());
$prestaire->addEtatLieux($etatLieux);
$this->assertCount(1, $prestaire->getEtatLieuxes());
$this->assertSame($prestaire, $etatLieux->getPrestataire());
$prestaire->removeEtatLieux($etatLieux);
$this->assertCount(0, $prestaire->getEtatLieuxes());
$this->assertNull($etatLieux->getPrestataire());
}
public function testEraseCredentials()
{
$prestaire = new Prestaire();
$prestaire->eraseCredentials(); // Should do nothing as per entity's eraseCredentials() TODO
$this->assertTrue(true); // Just ensure it doesn't crash
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Tests\Entity;
use App\Entity\Product;
use App\Entity\ProductDoc;
use PHPUnit\Framework\TestCase;
class ProductDocTest extends TestCase
{
public function testGettersAndSetters()
{
$productDoc = new ProductDoc();
$product = new Product();
$now = new \DateTimeImmutable();
$productDoc->setProduct($product);
$productDoc->setName('Document 1');
$productDoc->setIsPublic(true);
$productDoc->setDocProductName('doc1.pdf');
$productDoc->setDocProductSize(1024);
$productDoc->setUpdatedAt($now);
$this->assertSame($product, $productDoc->getProduct());
$this->assertEquals('Document 1', $productDoc->getName());
$this->assertTrue($productDoc->isPublic());
$this->assertEquals('doc1.pdf', $productDoc->getDocProductName());
$this->assertEquals(1024, $productDoc->getDocProductSize());
$this->assertSame($now, $productDoc->getUpdatedAt());
}
public function testJson()
{
$product = new Product();
$product->setName('Main Product');
$productDoc = new ProductDoc();
$productDoc->setProduct($product);
$productDoc->setName('Doc Name');
$expectedJson = json_encode([
'name' => 'Doc Name',
'product' => 'Main Product',
]);
$this->assertEquals($expectedJson, $productDoc->json());
}
}

View File

@@ -0,0 +1,153 @@
<?php
namespace App\Tests\Entity;
use App\Entity\FormulesProductInclus;
use App\Entity\Product;
use App\Entity\ProductDoc;
use App\Entity\ProductPhotos;
use App\Entity\ProductReserve;
use App\Entity\ProductVideo;
use PHPUnit\Framework\TestCase;
use Cocur\Slugify\Slugify; // To test the slug() method
class ProductTest extends TestCase
{
public function testGettersAndSetters()
{
$product = new Product();
$now = new \DateTimeImmutable();
$product->setRef('PROD-001');
$product->setCategory('Category A');
$product->setName('Test Product');
$product->setPriceDay(10.0);
$product->setPriceSup(5.0);
$product->setCaution(20.0);
$product->setImageName('product.jpg');
$product->setImageSize(1024);
$product->setUpdatedAt($now);
$product->setProductId('ext_prod_id');
$product->setDescription('Product description');
$product->setQt(5);
$product->setDimW(100.0);
$product->setDimH(50.0);
$product->setDimP(20.0);
$this->assertEquals('PROD-001', $product->getRef());
$this->assertEquals('Category A', $product->getCategory());
$this->assertEquals('Test Product', $product->getName());
$this->assertEquals(10.0, $product->getPriceDay());
$this->assertEquals(5.0, $product->getPriceSup());
$this->assertEquals(20.0, $product->getCaution());
$this->assertEquals('product.jpg', $product->getImageName());
$this->assertEquals(1024, $product->getImageSize());
$this->assertSame($now, $product->getUpdatedAt());
$this->assertEquals('ext_prod_id', $product->getProductId());
$this->assertEquals('Product description', $product->getDescription());
$this->assertEquals(5, $product->getQt());
$this->assertEquals(100.0, $product->getDimW());
$this->assertEquals(50.0, $product->getDimH());
$this->assertEquals(20.0, $product->getDimP());
}
public function testSlug()
{
$product = new Product();
// $product->setId(1); // ID is auto-generated, cannot be set directly in unit test
$product->setName('Awesome Product');
$this->assertEquals('awesome-product', $product->slug());
}
public function testJson()
{
$product = new Product();
// $product->setId(1); // ID is auto-generated
$product->setRef('PROD-JSON');
$product->setName('JSON Product');
$expectedJson = json_encode([
'id' => null, // ID is null for new entity
'ref' => 'PROD-JSON',
'name' => 'JSON Product',
]);
$this->assertEquals($expectedJson, $product->json());
}
public function testProductReservesCollection()
{
$product = new Product();
$reserve = new ProductReserve();
$this->assertCount(0, $product->getProductReserves());
$product->addProductReserf($reserve); // Typo: addProductReserf
$this->assertCount(1, $product->getProductReserves());
$this->assertSame($product, $reserve->getProduct());
$product->removeProductReserf($reserve); // Typo: removeProductReserf
$this->assertCount(0, $product->getProductReserves());
$this->assertNull($reserve->getProduct());
}
public function testProductDocsCollection()
{
$product = new Product();
$doc = new ProductDoc();
$this->assertCount(0, $product->getProductDocs());
$product->addProductDoc($doc);
$this->assertCount(1, $product->getProductDocs());
$this->assertSame($product, $doc->getProduct());
$product->removeProductDoc($doc);
$this->assertCount(0, $product->getProductDocs());
$this->assertNull($doc->getProduct());
}
public function testFormulesProductInclusesCollection()
{
$product = new Product();
$inclus = new FormulesProductInclus();
$this->assertCount(0, $product->getFormulesProductIncluses());
$product->addFormulesProductInclus($inclus);
$this->assertCount(1, $product->getFormulesProductIncluses());
$this->assertSame($product, $inclus->getPRODUCT());
$product->removeFormulesProductInclus($inclus);
$this->assertCount(0, $product->getFormulesProductIncluses());
$this->assertNull($inclus->getPRODUCT());
}
public function testProductPhotosCollection()
{
$product = new Product();
$photo = new ProductPhotos();
$this->assertCount(0, $product->getProductPhotos());
$product->addProductPhoto($photo);
$this->assertCount(1, $product->getProductPhotos());
$this->assertSame($product, $photo->getProduct());
$product->removeProductPhoto($photo);
$this->assertCount(0, $product->getProductPhotos());
$this->assertNull($photo->getProduct());
}
public function testProductVideosCollection()
{
$product = new Product();
$video = new ProductVideo();
$this->assertCount(0, $product->getProductVideos());
$product->addProductVideo($video);
$this->assertCount(1, $product->getProductVideos());
$this->assertSame($product, $video->getProduct());
$product->removeProductVideo($video);
$this->assertCount(0, $product->getProductVideos());
$this->assertNull($video->getProduct());
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Tests\Event\Object;
use App\Entity\Account;
use App\Event\Object\EventAdminCreate;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
#[AllowMockObjectsWithoutExpectations]
class EventAdminCreateTest extends TestCase
{
public function testGetters()
{
$account = $this->createMock(Account::class);
$requestedAccount = $this->createMock(Account::class);
$event = new EventAdminCreate($account, $requestedAccount);
$this->assertSame($account, $event->getAccount());
$this->assertSame($requestedAccount, $event->getRequestedAccount());
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Tests\Event\Object;
use App\Entity\Account;
use App\Event\Object\EventAdminDeleted;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
#[AllowMockObjectsWithoutExpectations]
class EventAdminDeletedTest extends TestCase
{
public function testGetters()
{
$account = $this->createMock(Account::class);
$requestedAccount = $this->createMock(Account::class);
$event = new EventAdminDeleted($account, $requestedAccount);
$this->assertSame($account, $event->getAccount());
$this->assertSame($requestedAccount, $event->getRequestedAccount());
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace App\Tests\Event\Service;
use App\Entity\Account;
use App\Entity\AccountResetPasswordRequest;
use App\Event\Object\EventAdminCreate;
use App\Event\Object\EventAdminDeleted;
use App\Event\Service\AdminEvent;
use App\Service\Mailer\Mailer;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
#[AllowMockObjectsWithoutExpectations]
class AdminEventTest extends TestCase
{
private $mailer;
private $urlGenerator;
private $entityManager;
private $adminEvent;
protected function setUp(): void
{
$this->mailer = $this->createMock(Mailer::class);
$this->urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$this->entityManager = $this->createMock(EntityManagerInterface::class);
$this->adminEvent = new AdminEvent(
$this->mailer,
$this->urlGenerator,
$this->entityManager
);
}
public function testOnAdminCreate()
{
$account = $this->createMock(Account::class);
$account->method('getId')->willReturn(1);
$account->method('getEmail')->willReturn('test@example.com');
$account->method('getFirstName')->willReturn('John');
$account->method('getName')->willReturn('Doe');
$requestedAccount = $this->createMock(Account::class);
$event = new EventAdminCreate($account, $requestedAccount);
$repo = $this->createMock(EntityRepository::class);
$this->entityManager->method('getRepository')->with(AccountResetPasswordRequest::class)->willReturn($repo);
$repo->method('findOneBy')->willReturn(null);
$this->entityManager->expects($this->once())->method('persist');
$this->entityManager->expects($this->once())->method('flush');
$this->urlGenerator->expects($this->once())
->method('generate')
->willReturn('http://example.com/reset');
$this->mailer->expects($this->once())
->method('send')
->with(
'test@example.com',
'John Doe',
'[Intranet Ludikevent] Activation de votre accès administrateur'
);
$this->adminEvent->onAdminCreate($event);
}
public function testOnAdminCreateExistingValidRequest()
{
$account = $this->createMock(Account::class);
$account->method('getId')->willReturn(1);
$account->method('getEmail')->willReturn('test@example.com');
$requestedAccount = $this->createMock(Account::class);
$event = new EventAdminCreate($account, $requestedAccount);
$existingRequest = new AccountResetPasswordRequest();
$existingRequest->setExpiresAt(new \DateTimeImmutable('+1 hour'));
$existingRequest->setToken('existing_token');
$repo = $this->createMock(EntityRepository::class);
$this->entityManager->method('getRepository')->with(AccountResetPasswordRequest::class)->willReturn($repo);
$repo->method('findOneBy')->willReturn($existingRequest);
// Should NOT persist a new one, but reuse existing
$this->entityManager->expects($this->never())->method('persist');
// Should generate URL with existing token
$this->urlGenerator->expects($this->once())
->method('generate')
->with(
'app_forgot_password_confirm',
['id' => 1, 'token' => 'existing_token']
);
$this->adminEvent->onAdminCreate($event);
}
public function testOnAdminCreateExistingExpiredRequest()
{
$account = $this->createMock(Account::class);
$account->method('getId')->willReturn(1);
$account->method('getEmail')->willReturn('test@example.com');
$requestedAccount = $this->createMock(Account::class);
$event = new EventAdminCreate($account, $requestedAccount);
$existingRequest = new AccountResetPasswordRequest();
$existingRequest->setExpiresAt(new \DateTimeImmutable('-1 hour'));
$repo = $this->createMock(EntityRepository::class);
$this->entityManager->method('getRepository')->with(AccountResetPasswordRequest::class)->willReturn($repo);
$repo->method('findOneBy')->willReturn($existingRequest);
// Should remove expired
$this->entityManager->expects($this->once())->method('remove')->with($existingRequest);
// Should persist new one
$this->entityManager->expects($this->atLeastOnce())->method('persist');
// Flush called for remove and persist
$this->entityManager->expects($this->atLeast(2))->method('flush');
$this->adminEvent->onAdminCreate($event);
}
public function testOnAdminDeleted()
{
$account = $this->createMock(Account::class);
$requestedAccount = $this->createMock(Account::class);
$event = new EventAdminDeleted($account, $requestedAccount);
$this->mailer->expects($this->exactly(2))
->method('send')
->with(
$this->stringContains('@'), // Simple check for email arg
$this->stringContains('Notification'),
"[Intranet Ludikevent] - Suppression d'un administrateur"
);
$this->adminEvent->onAdminDeleted($event);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Tests\Event\Service;
use App\Entity\Account;
use App\Entity\AccountLoginRegister;
use App\Event\Service\LoginStatsSubscriber;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;
#[AllowMockObjectsWithoutExpectations]
class LoginStatsSubscriberTest extends TestCase
{
private $entityManager;
private $subscriber;
protected function setUp(): void
{
$this->entityManager = $this->createMock(EntityManagerInterface::class);
$this->subscriber = new LoginStatsSubscriber($this->entityManager);
}
public function testOnLoginSuccessWithAccount()
{
$user = $this->createMock(Account::class);
$request = new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']);
$request->headers->set('User-Agent', 'TestAgent');
// Manually setting client IP on Request is tricky without relying on 'REMOTE_ADDR' server var or trusted proxies
// But getClientIp() defaults to null or 127.0.0.1 depending on setup.
// We can just rely on the default behavior for this test or mock the request if it was an interface/mockable.
// Since Request is a concrete class, we use it as is.
$event = $this->createMock(LoginSuccessEvent::class);
$event->method('getUser')->willReturn($user);
$event->method('getRequest')->willReturn($request);
$this->entityManager->expects($this->once())
->method('persist')
->with($this->isInstanceOf(AccountLoginRegister::class));
$this->entityManager->expects($this->once())->method('flush');
$this->subscriber->onLoginSuccess($event);
}
public function testOnLoginSuccessWithNonAccountUser()
{
$user = $this->createMock(UserInterface::class);
$event = $this->createMock(LoginSuccessEvent::class);
$event->method('getUser')->willReturn($user);
$this->entityManager->expects($this->never())->method('persist');
$this->entityManager->expects($this->never())->method('flush');
$this->subscriber->onLoginSuccess($event);
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Tests\Event\Signature;
use App\Entity\Contrats;
use App\Event\Signature\ContratEvent;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
#[AllowMockObjectsWithoutExpectations]
class ContratEventTest extends TestCase
{
public function testGetContrats()
{
$contrats = $this->createMock(Contrats::class);
$event = new ContratEvent($contrats);
$this->assertSame($contrats, $event->getContrats());
}
}

View File

@@ -0,0 +1,135 @@
<?php
namespace App\Tests\Event\Signature;
use App\Entity\Contrats;
use App\Entity\Customer;
use App\Event\Signature\ContratEvent;
use App\Event\Signature\ContratSubscriber;
use App\Service\Mailer\Mailer;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Vich\UploaderBundle\Storage\StorageInterface;
use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
#[AllowMockObjectsWithoutExpectations]
class ContratSubscriberTest extends TestCase
{
private $mailer;
private $urlGenerator;
private $kernel;
private $uploaderHelper;
private $storage;
private $subscriber;
private $tempDir;
protected function setUp(): void
{
$this->mailer = $this->createMock(Mailer::class);
$this->urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$this->kernel = $this->createMock(KernelInterface::class);
$this->storage = $this->createMock(StorageInterface::class);
$this->uploaderHelper = new UploaderHelper($this->storage); // Real class as it's final
$this->tempDir = sys_get_temp_dir() . '/contrat_test_' . uniqid();
mkdir($this->tempDir . '/public', 0777, true);
$this->subscriber = new ContratSubscriber(
$this->mailer,
$this->urlGenerator,
$this->kernel,
$this->uploaderHelper
);
$_ENV['CONTRAT_BASEURL'] = 'https://baseurl.com';
}
protected function tearDown(): void
{
$this->removeDirectory($this->tempDir);
}
public function testOnContratSendWithAttachment()
{
// 1. Setup File
$relativePath = '/uploads/contrats/c1.pdf';
mkdir(dirname($this->tempDir . '/public' . $relativePath), 0777, true);
file_put_contents($this->tempDir . '/public' . $relativePath, 'PDF Content');
$this->kernel->method('getProjectDir')->willReturn($this->tempDir);
// 2. Setup Entities
$customer = $this->createMock(Customer::class);
$customer->method('getEmail')->willReturn('cust@test.com');
$customer->method('getSurname')->willReturn('John');
$customer->method('getName')->willReturn('Doe');
$contrat = $this->createMock(Contrats::class);
$contrat->method('getCustomer')->willReturn($customer);
$contrat->method('getNumReservation')->willReturn('RES-123');
$this->storage->method('resolveUri')->willReturn($relativePath);
$event = new ContratEvent($contrat);
// 3. Expectations
$this->urlGenerator->expects($this->once())
->method('generate')
->with('gestion_contrat_view', ['num' => 'RES-123']);
$this->mailer->expects($this->once())
->method('send')
->with(
'cust@test.com',
'John Doe',
'[Ludikevent] - Contrat de location N°RES-123',
'mails/sign/contrat.twig',
$this->anything(),
$this->callback(function($attachments) {
return count($attachments) === 1;
})
);
$this->subscriber->onContratSend($event);
}
public function testOnContratSendWithoutAttachment()
{
// No file created
$this->kernel->method('getProjectDir')->willReturn($this->tempDir);
$customer = $this->createMock(Customer::class);
$customer->method('getEmail')->willReturn('cust@test.com'); // Mock email
$contrat = $this->createMock(Contrats::class);
$contrat->method('getCustomer')->willReturn($customer);
$contrat->method('getNumReservation')->willReturn('RES-123');
$this->storage->method('resolveUri')->willReturn(null);
$event = new ContratEvent($contrat);
$this->mailer->expects($this->once())
->method('send')
->with(
$this->anything(),
$this->anything(),
$this->anything(),
$this->anything(),
$this->anything(),
[] // Empty attachments
);
$this->subscriber->onContratSend($event);
}
private function removeDirectory($dir) {
if (!is_dir($dir)) return;
$files = array_diff(scandir($dir), array('.','..'));
foreach ($files as $file) {
(is_dir("$dir/$file")) ? $this->removeDirectory("$dir/$file") : unlink("$dir/$file");
}
rmdir($dir);
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Tests\Event\Signature;
use App\Entity\Devis;
use App\Event\Signature\DevisSend;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
#[AllowMockObjectsWithoutExpectations]
class DevisSendTest extends TestCase
{
public function testGetDevis()
{
$devis = $this->createMock(Devis::class);
$event = new DevisSend($devis);
$this->assertSame($devis, $event->getDevis());
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Tests\Event\Signature;
use App\Entity\Customer;
use App\Entity\Devis;
use App\Event\Signature\DevisSend;
use App\Event\Signature\DevisSubscriber;
use App\Service\Mailer\Mailer;
use App\Service\Signature\Client;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
#[AllowMockObjectsWithoutExpectations]
class DevisSubscriberTest extends TestCase
{
private $entityManager;
private $mailer;
private $client;
private $subscriber;
protected function setUp(): void
{
$this->entityManager = $this->createMock(EntityManagerInterface::class);
$this->mailer = $this->createMock(Mailer::class);
$this->client = $this->createMock(Client::class);
$this->subscriber = new DevisSubscriber(
$this->entityManager,
$this->mailer,
$this->client
);
}
public function testOnDevisSend()
{
$customer = $this->createMock(Customer::class);
$customer->method('getEmail')->willReturn('cust@test.com');
$customer->method('getName')->willReturn('Doe');
$customer->method('getSurname')->willReturn('John');
$devis = $this->createMock(Devis::class);
$devis->method('getCustomer')->willReturn($customer);
$devis->method('getSignatureId')->willReturn('sign_id_123');
$event = new DevisSend($devis);
$this->client->expects($this->once())
->method('getLinkSign')
->with('sign_id_123')
->willReturn('http://sign.link');
$this->mailer->expects($this->once())
->method('send')
->with(
'cust@test.com',
'Doe John',
'[Signature Ludikevent] - Signature de votre devis pour votre location',
'mails/sign/devis.twig',
$this->callback(function($context) use ($devis) {
return $context['devis'] === $devis && $context['signLink'] === 'http://sign.link';
})
);
$this->subscriber->onDevisSend($event);
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace App\Tests\Logger;
use App\Entity\Account;
use App\Entity\AuditLog;
use App\Logger\AppLogger;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\User\UserInterface;
#[AllowMockObjectsWithoutExpectations]
class AppLoggerTest extends TestCase
{
private $entityManager;
private $security;
private $requestStack;
private $logger;
protected function setUp(): void
{
$this->entityManager = $this->createMock(EntityManagerInterface::class);
$this->security = $this->createMock(Security::class);
$this->requestStack = $this->createMock(RequestStack::class);
$this->logger = new AppLogger(
$this->entityManager,
$this->security,
$this->requestStack
);
}
public function testRecordWithAccountUser()
{
$user = $this->createMock(Account::class);
$this->security->method('getUser')->willReturn($user);
$request = new Request();
$request->headers->set('User-Agent', 'TestAgent');
$this->requestStack->method('getCurrentRequest')->willReturn($request);
$this->entityManager->expects($this->once())
->method('persist')
->with($this->isInstanceOf(AuditLog::class));
$this->entityManager->expects($this->once())->method('flush');
$this->logger->record('TEST_TYPE', 'Test Message');
}
public function testRecordWithNonAccountUser()
{
$user = $this->createMock(UserInterface::class);
$this->security->method('getUser')->willReturn($user);
$this->entityManager->expects($this->never())->method('persist');
$this->entityManager->expects($this->never())->method('flush');
$this->logger->record('TEST_TYPE', 'Test Message');
}
public function testRecordWithNoRequest()
{
$user = $this->createMock(Account::class);
$this->security->method('getUser')->willReturn($user);
$this->requestStack->method('getCurrentRequest')->willReturn(null);
$this->entityManager->expects($this->once())
->method('persist')
->with($this->callback(function(AuditLog $log) {
return $log->getPath() === 'CLI/Internal';
}));
$this->entityManager->expects($this->once())->method('flush');
$this->logger->record('TEST_TYPE', 'Test Message');
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace App\Tests\Repository;
use App\Entity\Account;
use App\Repository\AccountRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Uid\Uuid;
class AccountRepositoryTest extends KernelTestCase
{
private ?EntityManagerInterface $entityManager;
private ?AccountRepository $repository;
protected function setUp(): void
{
$kernel = self::bootKernel();
$this->entityManager = $kernel->getContainer()
->get('doctrine')
->getManager();
$this->repository = $this->entityManager->getRepository(Account::class);
}
protected function tearDown(): void
{
parent::tearDown();
$this->entityManager->close();
$this->entityManager = null;
$this->repository = null;
}
public function testFindAdmin()
{
// 1. Create Admins
$admin = new Account();
$admin->setEmail('admin_' . uniqid() . '@test.com');
$admin->setRoles(['ROLE_ADMIN']);
$admin->setPassword('password');
$admin->setName('Admin');
$admin->setFirstName('User');
$admin->setUsername('admin_user_' . uniqid());
$admin->setUuid(Uuid::v4());
$admin->setIsActif(true);
$admin->setIsFirstLogin(false);
$this->entityManager->persist($admin);
// 2. Create Non-Admin
$user = new Account();
$user->setEmail('user_' . uniqid() . '@test.com');
$user->setRoles(['ROLE_USER']);
$user->setPassword('password');
$user->setName('User');
$user->setFirstName('Normal');
$user->setUsername('normal_user_' . uniqid());
$user->setUuid(Uuid::v4());
$user->setIsActif(true);
$user->setIsFirstLogin(false);
$this->entityManager->persist($user);
$this->entityManager->flush();
// 3. Test findAdmin
$admins = $this->repository->findAdmin();
$this->assertGreaterThanOrEqual(1, count($admins));
$found = false;
foreach ($admins as $a) {
if ($a->getEmail() === 'admin@test.com') {
$found = true;
}
// Ensure no user is returned (this might be tricky if other tests persist data,
// but we check if our non-admin is in the list)
if ($a->getEmail() === 'user@test.com') {
$this->fail('Non-admin user returned in findAdmin()');
}
}
$this->assertTrue($found, 'Admin user not found in findAdmin() result');
}
public function testUpgradePassword()
{
$user = new Account();
$user->setEmail('upgrade_' . uniqid() . '@test.com');
$user->setRoles(['ROLE_USER']);
$user->setPassword('old_hash');
$user->setName('Upgrade');
$user->setFirstName('User');
$user->setUsername('upgrade_user_' . uniqid());
$user->setUuid(Uuid::v4());
$user->setIsActif(true);
$user->setIsFirstLogin(false);
$this->entityManager->persist($user);
$this->entityManager->flush();
$this->repository->upgradePassword($user, 'new_encoded_password');
$updatedUser = $this->repository->find($user->getId());
$this->assertEquals('new_encoded_password', $updatedUser->getPassword());
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace App\Tests\Repository;
use App\Entity\Contrats;
use App\Entity\Customer;
use App\Repository\ContratsRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class ContratsRepositoryTest extends KernelTestCase
{
private ?EntityManagerInterface $entityManager;
private ?ContratsRepository $repository;
protected function setUp(): void
{
$kernel = self::bootKernel();
$this->entityManager = $kernel->getContainer()
->get('doctrine')
->getManager();
$this->repository = $this->entityManager->getRepository(Contrats::class);
}
protected function tearDown(): void
{
parent::tearDown();
$this->entityManager->close();
$this->entityManager = null;
$this->repository = null;
}
public function testFindBetweenDates()
{
$customer = new Customer();
$customer->setEmail('contrat_' . uniqid() . '@test.com');
$customer->setName('Test');
$customer->setSurname('Contrat');
$customer->setPhone('0600000000');
$customer->setCiv('Mr');
$customer->setType('pro');
$this->entityManager->persist($customer);
// 1. Create Contrats
// Target: 2026-06-15
$contratTarget = new Contrats();
$contratTarget->setDateAt(new \DateTimeImmutable('2026-06-15 12:00:00'));
$contratTarget->setEndAt(new \DateTimeImmutable('2026-06-16 12:00:00'));
$contratTarget->setCreateAt(new \DateTimeImmutable());
$contratTarget->setNumReservation('RES-TARGET');
$contratTarget->setAddressEvent('123 Main St');
$contratTarget->setZipCodeEvent('12345');
$contratTarget->setTownEvent('City');
$contratTarget->setType('event');
$contratTarget->setPente('flat');
$contratTarget->setIsSigned(false);
$contratTarget->setCustomer($customer);
$this->entityManager->persist($contratTarget);
// Before: 2026-06-01
$contratBefore = new Contrats();
$contratBefore->setDateAt(new \DateTimeImmutable('2026-06-01 12:00:00'));
$contratBefore->setEndAt(new \DateTimeImmutable('2026-06-02 12:00:00'));
$contratBefore->setCreateAt(new \DateTimeImmutable());
$contratBefore->setNumReservation('RES-BEFORE');
$contratBefore->setAddressEvent('123 Main St');
$contratBefore->setZipCodeEvent('12345');
$contratBefore->setTownEvent('City');
$contratBefore->setType('event');
$contratBefore->setPente('flat');
$contratBefore->setIsSigned(false);
$contratBefore->setCustomer($customer);
$this->entityManager->persist($contratBefore);
// After: 2026-07-01
$contratAfter = new Contrats();
$contratAfter->setDateAt(new \DateTimeImmutable('2026-07-01 12:00:00'));
$contratAfter->setEndAt(new \DateTimeImmutable('2026-07-02 12:00:00'));
$contratAfter->setCreateAt(new \DateTimeImmutable());
$contratAfter->setNumReservation('RES-AFTER');
$contratAfter->setAddressEvent('123 Main St');
$contratAfter->setZipCodeEvent('12345');
$contratAfter->setTownEvent('City');
$contratAfter->setType('event');
$contratAfter->setPente('flat');
$contratAfter->setIsSigned(false);
$contratAfter->setCustomer($customer);
$this->entityManager->persist($contratAfter);
$this->entityManager->flush();
// 2. Search Interval: 2026-06-10 to 2026-06-20
$start = new \DateTimeImmutable('2026-06-10 00:00:00');
$end = new \DateTimeImmutable('2026-06-20 00:00:00');
$results = $this->repository->findBetweenDates($start, $end);
$this->assertGreaterThanOrEqual(1, count($results));
$foundTarget = false;
foreach ($results as $contrat) {
if ($contrat->getNumReservation() === 'RES-TARGET') {
$foundTarget = true;
}
if ($contrat->getNumReservation() === 'RES-BEFORE') {
$this->fail('Contrat BEFORE interval should not be found');
}
if ($contrat->getNumReservation() === 'RES-AFTER') {
$this->fail('Contrat AFTER interval should not be found');
}
}
$this->assertTrue($foundTarget, 'Target contrat not found');
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Tests\Security;
use App\Security\AccessDeniedHandler;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Twig\Environment;
#[AllowMockObjectsWithoutExpectations]
class AccessDeniedHandlerTest extends TestCase
{
private $urlGenerator;
private $twig;
private $handler;
protected function setUp(): void
{
$this->urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$this->twig = $this->createMock(Environment::class);
$this->handler = new AccessDeniedHandler($this->urlGenerator, $this->twig);
}
public function testHandleAdminPathRedirectsToHome()
{
$request = Request::create('/admin/dashboard');
$exception = new AccessDeniedException();
$this->urlGenerator->expects($this->once())
->method('generate')
->with('app_home')
->willReturn('/home');
$response = $this->handler->handle($request, $exception);
$this->assertInstanceOf(RedirectResponse::class, $response);
$this->assertEquals('/home', $response->getTargetUrl());
}
public function testHandleJsonRequestReturnsForbidden()
{
$request = Request::create('/api/data');
$request->headers->set('Accept', 'application/json');
$exception = new AccessDeniedException();
$response = $this->handler->handle($request, $exception);
$this->assertInstanceOf(JsonResponse::class, $response);
$this->assertEquals(Response::HTTP_FORBIDDEN, $response->getStatusCode());
}
public function testHandleDefaultReturnsForbidden()
{
$request = Request::create('/some/other/path');
$exception = new AccessDeniedException();
$response = $this->handler->handle($request, $exception);
$this->assertInstanceOf(JsonResponse::class, $response);
$this->assertEquals(Response::HTTP_FORBIDDEN, $response->getStatusCode());
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace App\Tests\Security;
use App\Entity\Account;
use App\Entity\Customer;
use App\Security\AuthenticationEntryPoint;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
#[AllowMockObjectsWithoutExpectations]
class AuthenticationEntryPointTest extends TestCase
{
private $urlGenerator;
private $tokenStorage;
private $entryPoint;
protected function setUp(): void
{
$this->urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$this->tokenStorage = $this->createMock(TokenStorageInterface::class);
$this->entryPoint = new AuthenticationEntryPoint($this->urlGenerator, $this->tokenStorage);
}
public function testStartJsonRequestReturnsForbidden()
{
$request = Request::create('/api/resource');
$request->headers->set('Accept', 'application/json');
$response = $this->entryPoint->start($request);
$this->assertInstanceOf(JsonResponse::class, $response);
$this->assertEquals(Response::HTTP_FORBIDDEN, $response->getStatusCode());
}
public function testStartCrmPathWithCustomerRedirectsToHome()
{
$request = Request::create('/crm/dashboard');
$customer = $this->createMock(Customer::class);
$token = $this->createMock(TokenInterface::class);
$token->method('getUser')->willReturn($customer);
$this->tokenStorage->method('getToken')->willReturn($token);
$this->urlGenerator->expects($this->once())
->method('generate')
->with('app_home')
->willReturn('/home');
$response = $this->entryPoint->start($request);
$this->assertInstanceOf(RedirectResponse::class, $response);
$this->assertEquals('/home', $response->getTargetUrl());
}
public function testStartCrmPathWithoutUserRedirectsToHome()
{
$request = Request::create('/crm/dashboard');
$this->tokenStorage->method('getToken')->willReturn(null);
$this->urlGenerator->expects($this->once())
->method('generate')
->with('app_home')
->willReturn('/home');
$response = $this->entryPoint->start($request);
$this->assertInstanceOf(RedirectResponse::class, $response);
$this->assertEquals('/home', $response->getTargetUrl());
}
public function testStartReservationPathWithoutUserRedirectsToReservationLogin()
{
$request = Request::create('/reservation/book');
$this->tokenStorage->method('getToken')->willReturn(null);
$this->urlGenerator->expects($this->once())
->method('generate')
->with('reservation_login')
->willReturn('/reservation/login');
$response = $this->entryPoint->start($request);
$this->assertInstanceOf(RedirectResponse::class, $response);
$this->assertEquals('/reservation/login', $response->getTargetUrl());
}
public function testStartReservationPathWithUserRedirectsToHomeOnlyBecauseDefaultFallback()
{
// Note: The logic in AuthenticationEntryPoint for /reservation only handles !$user.
// If user exists (Account or Customer), it falls through to the default return at the end.
$request = Request::create('/reservation/book');
$user = $this->createMock(Account::class);
$token = $this->createMock(TokenInterface::class);
$token->method('getUser')->willReturn($user);
$this->tokenStorage->method('getToken')->willReturn($token);
$this->urlGenerator->expects($this->once())
->method('generate')
->with('app_home')
->willReturn('/home');
$response = $this->entryPoint->start($request);
$this->assertInstanceOf(RedirectResponse::class, $response);
$this->assertEquals('/home', $response->getTargetUrl());
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace App\Tests\Security;
use App\Entity\Customer;
use App\Security\CustomerAuthenticator;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
#[AllowMockObjectsWithoutExpectations]
class CustomerAuthenticatorTest extends TestCase
{
private $urlGenerator;
private $entityManager;
private $authenticator;
protected function setUp(): void
{
$this->urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$this->entityManager = $this->createMock(EntityManagerInterface::class);
$this->authenticator = new CustomerAuthenticator($this->urlGenerator, $this->entityManager);
}
public function testSupports()
{
$request = Request::create('/reservation/login', 'POST');
$request->attributes->set('_route', 'reservation_login');
$this->assertTrue($this->authenticator->supports($request));
$requestInvalid = Request::create('/reservation/login', 'GET');
$requestInvalid->attributes->set('_route', 'reservation_login');
$this->assertFalse($this->authenticator->supports($requestInvalid));
}
public function testAuthenticate()
{
$request = Request::create('/login', 'POST', [
'_username' => 'test@test.com',
'_password' => 'password',
'_csrf_token' => 'token'
]);
$session = $this->createMock(SessionInterface::class);
$request->setSession($session);
$repository = $this->createMock(EntityRepository::class);
$this->entityManager->method('getRepository')->with(Customer::class)->willReturn($repository);
$repository->method('findOneBy')->with(['email' => 'test@test.com'])->willReturn(new Customer());
$passport = $this->authenticator->authenticate($request);
$this->assertInstanceOf(Passport::class, $passport);
$this->assertTrue($passport->hasBadge(UserBadge::class));
$this->assertTrue($passport->hasBadge(CsrfTokenBadge::class));
}
public function testOnAuthenticationSuccessRedirectsToReservation()
{
$request = Request::create('/login');
$session = $this->createMock(SessionInterface::class);
$request->setSession($session);
$token = $this->createMock(TokenInterface::class);
$this->urlGenerator->expects($this->once())
->method('generate')
->with('reservation')
->willReturn('/reservation');
$response = $this->authenticator->onAuthenticationSuccess($request, $token, 'main');
$this->assertInstanceOf(RedirectResponse::class, $response);
$this->assertEquals('/reservation', $response->getTargetUrl());
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Tests\Security;
use App\Security\ErrorListener;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Twig\Environment;
#[AllowMockObjectsWithoutExpectations]
class ErrorListenerTest extends TestCase
{
private $twig;
private $listener;
protected function setUp(): void
{
$this->twig = $this->createMock(Environment::class);
$this->listener = new ErrorListener($this->twig);
}
public function testOnKernelExceptionInDevModeDoesNothing()
{
$_ENV['APP_ENV'] = 'dev';
$kernel = $this->createMock(HttpKernelInterface::class);
$request = new Request();
$event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, new \Exception());
$this->listener->onKernelException($event);
$this->assertNull($event->getResponse());
unset($_ENV['APP_ENV']); // Cleanup
}
public function testOnKernelExceptionJsonRequest()
{
$_ENV['APP_ENV'] = 'prod';
$kernel = $this->createMock(HttpKernelInterface::class);
$request = new Request();
$request->headers->set('Accept', 'application/json');
$exception = new NotFoundHttpException('Not found');
$event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $exception);
$this->listener->onKernelException($event);
$response = $event->getResponse();
$this->assertInstanceOf(JsonResponse::class, $response);
$this->assertEquals(404, $response->getStatusCode());
$content = json_decode($response->getContent(), true);
$this->assertEquals('error', $content['status']);
$this->assertEquals('Resource not found', $content['message']);
unset($_ENV['APP_ENV']);
}
public function testOnKernelExceptionHtmlRequest()
{
$_ENV['APP_ENV'] = 'prod';
$kernel = $this->createMock(HttpKernelInterface::class);
$request = new Request();
$exception = new \Exception('Error');
$event = new ExceptionEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $exception);
$this->twig->expects($this->once())
->method('render')
->with('error/500.twig', $this->anything())
->willReturn('<html>Error</html>');
$this->listener->onKernelException($event);
$response = $event->getResponse();
$this->assertInstanceOf(Response::class, $response);
$this->assertEquals(500, $response->getStatusCode());
$this->assertEquals('<html>Error</html>', $response->getContent());
unset($_ENV['APP_ENV']);
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Tests\Security;
use App\Entity\Customer;
use App\Entity\OrderSession;
use App\Security\FlowAuthenticator;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
#[AllowMockObjectsWithoutExpectations]
class FlowAuthenticatorTest extends TestCase
{
private $urlGenerator;
private $entityManager;
private $authenticator;
protected function setUp(): void
{
$this->urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$this->entityManager = $this->createMock(EntityManagerInterface::class);
$this->authenticator = new FlowAuthenticator($this->urlGenerator, $this->entityManager);
}
public function testSupports()
{
$request = Request::create('/reservation/flow', 'POST');
$request->attributes->set('_route', 'reservation_flow');
$this->assertTrue($this->authenticator->supports($request));
}
public function testAuthenticate()
{
$request = Request::create('/reservation/flow', 'POST', [
'_username' => 'test@test.com',
'_password' => 'password',
'_csrf_token' => 'token'
]);
$session = $this->createMock(SessionInterface::class);
$request->setSession($session);
$repository = $this->createMock(EntityRepository::class);
$this->entityManager->method('getRepository')->with(Customer::class)->willReturn($repository);
$repository->method('findOneBy')->with(['email' => 'test@test.com'])->willReturn(new Customer());
$passport = $this->authenticator->authenticate($request);
$this->assertInstanceOf(Passport::class, $passport);
$this->assertTrue($passport->hasBadge(UserBadge::class));
}
public function testOnAuthenticationSuccess()
{
$request = Request::create('/reservation/flow');
$request->attributes->set('sessionId', 'session-123');
$token = $this->createMock(TokenInterface::class);
$customer = new Customer();
$token->method('getUser')->willReturn($customer);
$sessionRepo = $this->createMock(EntityRepository::class);
$orderSession = $this->createMock(OrderSession::class);
$this->entityManager->method('getRepository')->with(OrderSession::class)->willReturn($sessionRepo);
$sessionRepo->method('findOneBy')->with(['uuid' => 'session-123'])->willReturn($orderSession);
$orderSession->expects($this->once())->method('setCustomer')->with($customer);
$this->entityManager->expects($this->once())->method('flush');
$this->urlGenerator->expects($this->once())
->method('generate')
->with('reservation_flow', ['sessionId' => 'session-123'])
->willReturn('/reservation/flow/session-123');
$response = $this->authenticator->onAuthenticationSuccess($request, $token, 'main');
$this->assertInstanceOf(RedirectResponse::class, $response);
$this->assertEquals('/reservation/flow/session-123', $response->getTargetUrl());
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Tests\Security;
use App\Security\IntranetLocked;
use App\Service\Mailer\Mailer;
use App\Service\Signature\Client as SignatureClient;
use App\Service\Search\Client as SearchClient;
use App\Service\Stripe\Client as StripeClient;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\Request;
use Twig\Environment;
#[AllowMockObjectsWithoutExpectations]
class IntranetLockedTest extends TestCase
{
private $twig;
private $signatureClient;
private $searchClient;
private $stripeClient;
private $mailer;
private $listener;
protected function setUp(): void
{
$this->twig = $this->createMock(Environment::class);
$this->signatureClient = $this->createMock(SignatureClient::class);
$this->searchClient = $this->createMock(SearchClient::class);
$this->stripeClient = $this->createMock(StripeClient::class);
$this->mailer = $this->createMock(Mailer::class);
$this->listener = new IntranetLocked(
$this->twig,
$this->signatureClient,
$this->searchClient,
$this->stripeClient,
$this->mailer
);
}
public function testOnLockedWhenEnabled()
{
$_ENV['INTRANET_LOCK'] = 'true';
$kernel = $this->createMock(HttpKernelInterface::class);
$request = new Request();
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
$this->twig->expects($this->once())->method('render')->with('security/locked.twig')->willReturn('Locked');
$this->listener->onLocked($event);
$response = $event->getResponse();
$this->assertInstanceOf(Response::class, $response);
$this->assertEquals(Response::HTTP_FORBIDDEN, $response->getStatusCode());
unset($_ENV['INTRANET_LOCK']);
}
public function testOnControlAllServicesUp()
{
$kernel = $this->createMock(HttpKernelInterface::class);
$request = new Request();
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
$this->signatureClient->method('status')->willReturn(true);
$this->searchClient->method('status')->willReturn(true);
$this->stripeClient->method('status')->willReturn(true);
$this->listener->onControl($event);
$this->assertNull($event->getResponse());
}
public function testOnControlServiceDown()
{
$kernel = $this->createMock(HttpKernelInterface::class);
$request = new Request();
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
$this->signatureClient->method('status')->willReturn(false);
$this->mailer->expects($this->once())->method('send');
$this->twig->expects($this->once())->method('render')->with('security/error.twig')->willReturn('Error');
$this->listener->onControl($event);
$response = $event->getResponse();
$this->assertInstanceOf(Response::class, $response);
$this->assertEquals(Response::HTTP_FORBIDDEN, $response->getStatusCode());
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace App\Tests\Security;
use App\Entity\Account;
use App\Security\KeycloakAuthenticator;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Client\OAuth2ClientInterface;
use League\OAuth2\Client\Provider\GenericResourceOwner;
use League\OAuth2\Client\Token\AccessToken;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
use Stevenmaguire\OAuth2\Client\Provider\KeycloakResourceOwner;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
#[AllowMockObjectsWithoutExpectations]
class KeycloakAuthenticatorTest extends TestCase
{
private $clientRegistry;
private $entityManager;
private $router;
private $authenticator;
protected function setUp(): void
{
$this->clientRegistry = $this->createMock(ClientRegistry::class);
$this->entityManager = $this->createMock(EntityManagerInterface::class);
$this->router = $this->createMock(RouterInterface::class);
$this->authenticator = new KeycloakAuthenticator(
$this->clientRegistry,
$this->entityManager,
$this->router
);
}
public function testSupports()
{
$request = Request::create('/connect/keycloak/check');
$request->attributes->set('_route', 'connect_keycloak_check');
$this->assertTrue($this->authenticator->supports($request));
}
public function testAuthenticateExistingUser()
{
$request = Request::create('/connect/keycloak/check');
$client = $this->createMock(OAuth2ClientInterface::class);
$this->clientRegistry->method('getClient')->with('keycloak')->willReturn($client);
$accessToken = new AccessToken(['access_token' => 'token']);
$client->method('getAccessToken')->willReturn($accessToken);
// Mock Keycloak User
$keycloakUser = $this->createMock(KeycloakResourceOwner::class);
$keycloakUser->method('getId')->willReturn('keycloak-id-123');
$keycloakUser->method('getEmail')->willReturn('user@test.com');
$client->method('fetchUserFromToken')->willReturn($keycloakUser);
// Mock Repository
$repository = $this->createMock(EntityRepository::class);
$this->entityManager->method('getRepository')->with(Account::class)->willReturn($repository);
// Existing user by Keycloak ID
$existingUser = new Account();
$repository->method('findOneBy')->willReturnMap([
[['keycloakId' => 'keycloak-id-123'], null, $existingUser]
]);
$passport = $this->authenticator->authenticate($request);
$this->assertInstanceOf(Passport::class, $passport);
$userBadge = $passport->getBadge(UserBadge::class);
$userLoader = $userBadge->getUserLoader();
$loadedUser = $userLoader();
$this->assertSame($existingUser, $loadedUser);
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace App\Tests\Security;
use App\Entity\Account;
use App\Security\LoginFormAuthenticator;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
#[AllowMockObjectsWithoutExpectations]
class LoginFormAuthenticatorTest extends TestCase
{
private $entityManager;
private $urlGenerator;
private $security;
private $authenticator;
protected function setUp(): void
{
$this->entityManager = $this->createMock(EntityManagerInterface::class);
$this->urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$this->security = $this->createMock(Security::class);
$this->authenticator = new LoginFormAuthenticator(
$this->entityManager,
$this->urlGenerator,
$this->security
);
}
public function testSupports()
{
$request = Request::create('/login', 'POST');
$request->attributes->set('_route', 'app_home');
$request->headers->set('HOST', 'intranet.ludikevent.fr');
$this->assertTrue($this->authenticator->supports($request));
}
public function testAuthenticateUserNotFound()
{
$request = Request::create('/login', 'POST', [
'_username' => 'unknown@test.com',
'_password' => 'pass',
'_csrf_token' => 'token'
]);
$request->setSession($this->createMock(SessionInterface::class));
$repo = $this->createMock(EntityRepository::class);
$this->entityManager->method('getRepository')->willReturn($repo);
$repo->method('findOneBy')->willReturn(null);
$this->expectException(CustomUserMessageAuthenticationException::class);
$passport = $this->authenticator->authenticate($request);
$passport->getBadge(\Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge::class)->getUserLoader()('unknown@test.com');
}
public function testOnAuthenticationSuccess()
{
$request = Request::create('/login');
$request->setSession($this->createMock(SessionInterface::class));
$token = $this->createMock(TokenInterface::class);
$this->urlGenerator->expects($this->once())
->method('generate')
->with('app_crm')
->willReturn('/crm');
$response = $this->authenticator->onAuthenticationSuccess($request, $token, 'main');
$this->assertInstanceOf(RedirectResponse::class, $response);
$this->assertEquals('/crm', $response->getTargetUrl());
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace App\Tests\Security;
use App\Security\MaintenanceListener;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Twig\Environment;
#[AllowMockObjectsWithoutExpectations]
class MaintenanceListenerTest extends TestCase
{
private $twig;
private $tempDir;
private $listener;
protected function setUp(): void
{
$this->twig = $this->createMock(Environment::class);
$this->tempDir = sys_get_temp_dir() . '/maintenance_test_' . uniqid();
mkdir($this->tempDir . '/var', 0777, true);
$this->listener = new MaintenanceListener($this->twig, $this->tempDir);
}
protected function tearDown(): void
{
$this->removeDirectory($this->tempDir);
}
public function testOnKernelRequestMaintenanceFileExists()
{
touch($this->tempDir . '/var/.maintenance');
$kernel = $this->createMock(HttpKernelInterface::class);
$request = Request::create('/');
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
$this->twig->expects($this->once())
->method('render')
->with('security/maintenance.twig')
->willReturn('Maintenance Mode');
$this->listener->onKernelRequest($event);
$response = $event->getResponse();
$this->assertInstanceOf(Response::class, $response);
$this->assertEquals(503, $response->getStatusCode());
$this->assertEquals('Maintenance Mode', $response->getContent());
}
public function testOnKernelRequestNoMaintenanceFile()
{
$kernel = $this->createMock(HttpKernelInterface::class);
$request = Request::create('/');
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
$this->listener->onKernelRequest($event);
$this->assertNull($event->getResponse());
}
private function removeDirectory($dir) {
if (!is_dir($dir)) return;
$files = array_diff(scandir($dir), array('.','..'));
foreach ($files as $file) {
(is_dir("$dir/$file")) ? $this->removeDirectory("$dir/$file") : unlink("$dir/$file");
}
rmdir($dir);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Tests\Security;
use App\Security\PasswordGenerator;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
#[AllowMockObjectsWithoutExpectations]
class PasswordGeneratorTest extends TestCase
{
public function testGenerateLength()
{
$generator = new PasswordGenerator(16);
$password = $generator->generate();
$this->assertEquals(16, strlen($password));
}
public function testGenerateContainsRequiredCharacters()
{
$generator = new PasswordGenerator(12);
$password = $generator->generate();
$this->assertMatchesRegularExpression('/[a-z]/', $password);
$this->assertMatchesRegularExpression('/[A-Z]/', $password);
$this->assertMatchesRegularExpression('/[0-9]/', $password);
$this->assertMatchesRegularExpression('/[@#_\-]/', $password);
}
public function testInvalidLengthThrowsException()
{
$this->expectException(\InvalidArgumentException::class);
new PasswordGenerator(3);
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Tests\Security;
use App\Security\RedirecListener;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
#[AllowMockObjectsWithoutExpectations]
class RedirecListenerTest extends TestCase
{
private $listener;
protected function setUp(): void
{
// We need to control $_ENV['APP_ENV'] for the constructor logic
$_ENV['APP_ENV'] = 'prod';
$this->listener = new RedirecListener();
}
public function testIntranetRootRedirect()
{
$kernel = $this->createMock(HttpKernelInterface::class);
$request = Request::create('/');
$request->headers->set('HOST', 'intranet.ludikevent.fr');
$response = new Response();
$event = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response);
$this->listener->onResponse($event);
$newResponse = $event->getResponse();
$this->assertInstanceOf(RedirectResponse::class, $newResponse);
$this->assertEquals('https://intranet.ludikevent.fr/intranet', $newResponse->getTargetUrl());
}
public function testReservationRedirect()
{
$kernel = $this->createMock(HttpKernelInterface::class);
$request = Request::create('/reservation/catalogue');
$request->headers->set('HOST', 'reservation.ludikevent.fr');
$response = new Response();
$event = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response);
$this->listener->onResponse($event);
$newResponse = $event->getResponse();
$this->assertInstanceOf(RedirectResponse::class, $newResponse);
$this->assertEquals('https://reservation.ludikevent.fr/catalogue', $newResponse->getTargetUrl());
$this->assertEquals(Response::HTTP_MOVED_PERMANENTLY, $newResponse->getStatusCode());
}
public function testDevNgrokRedirect()
{
$_ENV['APP_ENV'] = 'dev';
$listener = new RedirecListener(); // Re-instantiate to pick up dev env
$kernel = $this->createMock(HttpKernelInterface::class);
$request = Request::create('/some/path?foo=bar');
$request->headers->set('HOST', 'my-app.ngrok-free.app');
$response = new Response();
$event = new ResponseEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST, $response);
$listener->onResponse($event);
$newResponse = $event->getResponse();
$this->assertInstanceOf(RedirectResponse::class, $newResponse);
$this->assertEquals('https://esyweb.local/some/path?foo=bar', $newResponse->getTargetUrl());
$_ENV['APP_ENV'] = 'prod'; // Restore
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Tests\Security;
use App\Entity\Formules;
use App\Entity\Product;
use App\Repository\FormulesRepository;
use App\Repository\OptionsRepository;
use App\Repository\ProductRepository;
use App\Security\SiteMapListener;
use Doctrine\ORM\EntityManagerInterface;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
use Presta\SitemapBundle\Event\SitemapPopulateEvent;
use Presta\SitemapBundle\Service\UrlContainerInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Vich\UploaderBundle\Storage\StorageInterface;
use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
#[AllowMockObjectsWithoutExpectations]
class SiteMapListenerTest extends TestCase
{
private $uploaderHelper;
private $storage;
private $optionsRepository;
private $productRepository;
private $formulesRepository;
private $entityManager;
private $listener;
protected function setUp(): void
{
$this->storage = $this->createMock(StorageInterface::class);
$this->uploaderHelper = new UploaderHelper($this->storage); // Instantiate real class
$this->optionsRepository = $this->createMock(OptionsRepository::class);
$this->productRepository = $this->createMock(ProductRepository::class);
$this->formulesRepository = $this->createMock(FormulesRepository::class);
$this->entityManager = $this->createMock(EntityManagerInterface::class);
$this->listener = new SiteMapListener(
$this->uploaderHelper,
$this->optionsRepository,
$this->productRepository,
$this->formulesRepository,
$this->entityManager
);
}
public function testPopulate()
{
$urlContainer = $this->createMock(UrlContainerInterface::class);
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
$event = new SitemapPopulateEvent($urlContainer, $urlGenerator);
$urlGenerator->method('generate')->willReturn('https://example.com/page');
// Mock Data
$formule = $this->createMock(Formules::class);
$formule->method('slug')->willReturn('f-1');
$formule->method('getUpdatedAt')->willReturn(new \DateTimeImmutable());
$this->formulesRepository->method('findBy')->willReturn([$formule]);
// Mock UploaderHelper behavior via Storage
$this->storage->method('resolveUri')->willReturn('/images/formule.jpg');
$product = $this->createMock(Product::class);
$product->method('slug')->willReturn('p-1');
$product->method('getUpdatedAt')->willReturn(new \DateTimeImmutable());
$product->method('getName')->willReturn('Prod 1');
$this->productRepository->method('findAll')->willReturn([$product]);
$this->optionsRepository->method('findAll')->willReturn([]);
$urlContainer->expects($this->atLeastOnce())->method('addUrl');
$this->listener->populate($event);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Tests\Security;
use App\Entity\Account;
use App\Security\UserChecker;
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
use Symfony\Component\Security\Core\User\UserInterface;
#[AllowMockObjectsWithoutExpectations]
class UserCheckerTest extends TestCase
{
private $checker;
protected function setUp(): void
{
$this->checker = new UserChecker();
}
public function testCheckPreAuthActiveUser()
{
$user = $this->createMock(Account::class);
$user->method('isActif')->willReturn(true);
$this->checker->checkPreAuth($user);
$this->assertTrue(true); // No exception thrown
}
public function testCheckPreAuthInactiveUserThrowsException()
{
$user = $this->createMock(Account::class);
$user->method('isActif')->willReturn(false);
$this->expectException(CustomUserMessageAccountStatusException::class);
$this->expectExceptionMessage('Votre compte a été désactivé.');
$this->checker->checkPreAuth($user);
}
public function testCheckPreAuthNonAccountUserIgnores()
{
$user = $this->createMock(UserInterface::class);
$this->checker->checkPreAuth($user);
$this->assertTrue(true);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Tests\Service\AI;
use App\Service\AI\GeminiClient;
use GeminiAPI\Client;
use GeminiAPI\GenerativeModel;
use GeminiAPI\Resources\Parts\TextPart;
use GeminiAPI\Responses\GenerateContentResponse;
use PHPUnit\Framework\TestCase;
class GeminiClientTest extends TestCase
{
public function testGenerateFriendlyMessageSuccess(): void
{
// Mock the response
$responseMock = $this->createStub(GenerateContentResponse::class);
$responseMock->method('text')
->willReturn('This is a friendly update.');
// Mock the model
$modelMock = $this->createMock(GenerativeModel::class);
$modelMock->expects($this->once())
->method('generateContent')
->with($this->isInstanceOf(TextPart::class))
->willReturn($responseMock);
// Mock the client
$clientMock = $this->createMock(Client::class);
$clientMock->expects($this->once())
->method('withV1BetaVersion')
->willReturnSelf();
$clientMock->expects($this->once())
->method('generativeModel')
->with('gemini-3-pro-preview')
->willReturn($modelMock);
// Instantiate GeminiClient with mocked Client
$geminiClient = new GeminiClient('fake-api-key', $clientMock);
$result = $geminiClient->generateFriendlyMessage('Raw technical message');
$this->assertEquals('This is a friendly update.', $result);
}
public function testGenerateFriendlyMessageException(): void
{
// Mock the client to throw an exception
$clientMock = $this->createStub(Client::class);
$clientMock->method('withV1BetaVersion')
->willThrowException(new \Exception('API Error'));
// Instantiate GeminiClient with mocked Client
$geminiClient = new GeminiClient('fake-api-key', $clientMock);
$result = $geminiClient->generateFriendlyMessage('Raw technical message');
$this->assertNull($result);
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Tests\Service\Generator;
use App\Service\Generator\TempPasswordGenerator;
use PHPUnit\Framework\TestCase;
class TempPasswordGeneratorTest extends TestCase
{
public function testGenerateDefaultLength(): void
{
$password = TempPasswordGenerator::generate();
$this->assertEquals(12, strlen($password));
}
public function testGenerateCustomLength(): void
{
$length = 16;
$password = TempPasswordGenerator::generate($length);
$this->assertEquals($length, strlen($password));
}
public function testGenerateInvalidLength(): void
{
$password = TempPasswordGenerator::generate(-5);
$this->assertEquals(12, strlen($password)); // Should fallback to default
}
public function testGenerateCustomCharacters(): void
{
$chars = 'ABC';
$password = TempPasswordGenerator::generate(10, $chars);
$this->assertEquals(10, strlen($password));
$this->assertMatchesRegularExpression('/^[ABC]+$/', $password);
}
public function testIsComplexValid(): void
{
// Needs 8+ chars, Upper, Lower, Digit, Special
$password = 'Ab1!defg';
$this->assertTrue(TempPasswordGenerator::isComplex($password));
}
public function testIsComplexTooShort(): void
{
$password = 'Ab1!de'; // 6 chars
$this->assertFalse(TempPasswordGenerator::isComplex($password));
}
public function testIsComplexMissingUpper(): void
{
$password = 'ab1!defg';
$this->assertFalse(TempPasswordGenerator::isComplex($password));
}
public function testIsComplexMissingLower(): void
{
$password = 'AB1!DEFG';
$this->assertFalse(TempPasswordGenerator::isComplex($password));
}
public function testIsComplexMissingDigit(): void
{
$password = 'Abc!defg';
$this->assertFalse(TempPasswordGenerator::isComplex($password));
}
public function testIsComplexMissingSpecial(): void
{
$password = 'Ab12defg';
$this->assertFalse(TempPasswordGenerator::isComplex($password));
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Tests\Service\Mailer\Event;
use App\Entity\Account;
use App\Service\Mailer\Event\CreatedAdminEvent;
use PHPUnit\Framework\TestCase;
class CreatedAdminEventTest extends TestCase
{
public function testGetters(): void
{
$account = new Account();
$password = 'secret123';
$event = new CreatedAdminEvent($account, $password);
$this->assertSame($account, $event->getAccount());
$this->assertSame($password, $event->getPassword());
}
}

Some files were not shown because too many files have changed in this diff Show More