Add isHidden to Category, category CRUD tests, coverage improvements
- Add isHidden field to Category entity with migration (DEFAULT false for existing rows) - Add isHidden checkbox to edit category template and "Masquee" badge on category list - Save isHidden in editCategory controller method - Fix Category.isActive() indentation - Create CategoryTest with full coverage (14 tests): defaults, setters, setEvent logic, isActive, isHidden - Add category CRUD tests to AccountControllerTest: add/edit/delete/reorder categories with access control - Add cookie-consent tests for dev env early return and Cloudflare tunnel script - Exclude PayoutPdfService from phpunit coverage and SonarQube analysis Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1815,6 +1815,12 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* }
|
||||
* @psalm-type PwaConfig = array{
|
||||
* asset_compiler?: bool|Param, // When true, the assets will be compiled when the command "asset-map:compile" is run. // Default: true
|
||||
* early_hints?: bool|array{ // Early Hints (HTTP 103) configuration. Requires a compatible server (FrankenPHP, Caddy).
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* preload_manifest?: bool|Param, // Preload the PWA manifest file. // Default: true
|
||||
* preload_serviceworker?: bool|Param, // Preload the service worker script. Disabled by default as SW registration is usually deferred. // Default: false
|
||||
* preconnect_workbox_cdn?: bool|Param, // Preconnect to Workbox CDN when using CDN mode. // Default: true
|
||||
* },
|
||||
* favicons?: bool|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* default?: array{ // The favicon source and parameters. When used with "dark", this favicon will become the light version.
|
||||
@@ -2021,6 +2027,20 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* }>,
|
||||
* },
|
||||
* path_type_reference?: int|Param, // Deprecated: The "path_type_reference" configuration key is deprecated. Use the "path_type_reference" of URL nodes instead. // The path type reference to generate paths/URLs. See https://symfony.com/doc/current/routing.html#generating-urls-in-controllers for more information. // Default: 1
|
||||
* resource_hints?: bool|array{ // Resource Hints configuration for preconnect, dns-prefetch, and preload.
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* auto_preconnect?: bool|Param, // Automatically add preconnect hints for detected external origins (Workbox CDN, Google Fonts). // Default: true
|
||||
* preconnect?: list<scalar|Param|null>,
|
||||
* dns_prefetch?: list<scalar|Param|null>,
|
||||
* preload?: list<array{ // Default: []
|
||||
* href?: scalar|Param|null, // The URL or path to preload.
|
||||
* as?: "script"|"style"|"font"|"image"|"fetch"|"document"|"audio"|"video"|"track"|"worker"|Param, // The resource type.
|
||||
* type?: scalar|Param|null, // The MIME type of the resource. // Default: null
|
||||
* crossorigin?: "anonymous"|"use-credentials"|Param, // The crossorigin attribute value. Required for fonts. // Default: null
|
||||
* fetchpriority?: "high"|"low"|"auto"|Param, // The fetch priority hint. // Default: null
|
||||
* media?: scalar|Param|null, // Media query for responsive preloading. // Default: null
|
||||
* }>,
|
||||
* },
|
||||
* serviceworker?: bool|string|array{
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* src?: scalar|Param|null, // The path to the service worker source file. Can be served by Asset Mapper.
|
||||
@@ -2030,7 +2050,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* use_cache?: bool|Param, // Whether the service worker should use the cache. // Default: true
|
||||
* workbox?: bool|array{ // The configuration of the workbox.
|
||||
* enabled?: bool|Param, // Default: true
|
||||
* use_cdn?: bool|Param, // Whether to use the local workbox or the CDN. // Default: false
|
||||
* use_cdn?: bool|Param, // Deprecated: The "use_cdn" option is deprecated and will be removed in 2.0.0. use "config.use_cdn" instead. // Whether to use the local workbox or the CDN. // Default: false
|
||||
* google_fonts?: bool|array{
|
||||
* enabled?: bool|Param, // Default: true
|
||||
* cache_prefix?: scalar|Param|null, // The cache prefix for the Google fonts. // Default: null
|
||||
@@ -2038,14 +2058,21 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* max_entries?: int|Param, // The maximum number of entries in the Google fonts cache. // Default: null
|
||||
* },
|
||||
* cache_manifest?: bool|Param, // Whether to cache the manifest file. // Default: true
|
||||
* version?: scalar|Param|null, // The version of workbox. When using local files, the version shall be "7.0.0." // Default: "7.3.0"
|
||||
* workbox_public_url?: scalar|Param|null, // The public path to the local workbox. Only used if use_cdn is false. // Default: "/workbox"
|
||||
* version?: scalar|Param|null, // Deprecated: The "version" option is deprecated and will be removed in 2.0.0. use "config.version" instead. // The version of workbox. When using local files, the version shall be "7.0.0." // Default: "7.3.0"
|
||||
* workbox_public_url?: scalar|Param|null, // Deprecated: The "workbox_public_url" option is deprecated and will be removed in 2.0.0. use "config.workbox_public_url" instead. // The public path to the local workbox. Only used if use_cdn is false. // Default: "/workbox"
|
||||
* idb_public_url?: scalar|Param|null, // The public path to the local IndexDB. Only used if use_cdn is false. // Default: "/idb"
|
||||
* workbox_import_placeholder?: scalar|Param|null, // Deprecated: The "workbox_import_placeholder" option is deprecated and will be removed in 2.0.0. No replacement. // The placeholder for the workbox import. Will be replaced by the workbox import. // Default: "//WORKBOX_IMPORT_PLACEHOLDER"
|
||||
* standard_rules_placeholder?: scalar|Param|null, // Deprecated: The "standard_rules_placeholder" option is deprecated and will be removed in 2.0.0. No replacement. // The placeholder for the standard rules. Will be replaced by caching strategies. // Default: "//STANDARD_RULES_PLACEHOLDER"
|
||||
* offline_fallback_placeholder?: scalar|Param|null, // Deprecated: The "offline_fallback_placeholder" option is deprecated and will be removed in 2.0.0. No replacement. // The placeholder for the offline fallback. Will be replaced by the URL. // Default: "//OFFLINE_FALLBACK_PLACEHOLDER"
|
||||
* widgets_placeholder?: scalar|Param|null, // Deprecated: The "widgets_placeholder" option is deprecated and will be removed in 2.0.0. No replacement. // The placeholder for the widgets. Will be replaced by the widgets management events. // Default: "//WIDGETS_PLACEHOLDER"
|
||||
* clear_cache?: bool|Param, // Whether to clear the cache during the service worker activation. // Default: true
|
||||
* navigation_preload?: bool|Param, // Whether to enable navigation preload. This speeds up navigation requests by making the network request in parallel with service worker boot-up. Note: Do not enable if you are precaching HTML pages (e.g., with offline_fallback or warm_cache_urls), as it would be redundant. // Default: false
|
||||
* config?: array{
|
||||
* debug?: bool|Param, // Controls workbox debug logging. Set to false to disable debug mode and logging. // Default: true
|
||||
* version?: scalar|Param|null, // The version of workbox. When using local files, the version shall be "7.0.0." // Default: "7.3.0"
|
||||
* use_cdn?: bool|Param, // Whether to use the local workbox or the CDN. // Default: false
|
||||
* workbox_public_url?: scalar|Param|null, // The public path to the local workbox. Only used if use_cdn is false. // Default: "/workbox"
|
||||
* },
|
||||
* offline_fallback?: array{
|
||||
* cache_name?: scalar|Param|null, // The name of the offline cache. // Default: "offline"
|
||||
* page?: string|array{ // The URL of the offline page fallback.
|
||||
@@ -2088,10 +2115,10 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* match_callback?: scalar|Param|null, // The regex or callback function to match the URLs.
|
||||
* cache_name?: scalar|Param|null, // The name of the page cache.
|
||||
* network_timeout?: int|Param, // The network timeout in seconds before cache is called (for "NetworkFirst" and "NetworkOnly" strategies). // Default: 3
|
||||
* strategy?: scalar|Param|null, // The caching strategy. Only "NetworkFirst", "CacheFirst" and "StaleWhileRevalidate" are supported. // Default: "NetworkFirst"
|
||||
* strategy?: scalar|Param|null, // The caching strategy. Only "NetworkFirst", "CacheFirst" and "StaleWhileRevalidate" are supported. StaleWhileRevalidate provides instant page loads with background updates. // Default: "StaleWhileRevalidate"
|
||||
* max_entries?: scalar|Param|null, // The maximum number of entries in the cache (for "CacheFirst" and "NetworkFirst" strategy only). // Default: null
|
||||
* max_age?: scalar|Param|null, // The maximum number of seconds before the cache is invalidated (for "CacheFirst" and "NetWorkFirst" strategy only). // Default: null
|
||||
* broadcast?: bool|Param, // Whether to broadcast the cache update events (for "StaleWhileRevalidate" strategy only). // Default: false
|
||||
* broadcast?: bool|Param, // Whether to broadcast the cache update events (for "StaleWhileRevalidate" strategy only). Enables client notification when content is updated. // Default: true
|
||||
* range_requests?: bool|Param, // Whether to support range requests (for "CacheFirst" strategy only). // Default: false
|
||||
* cacheable_response_headers?: list<scalar|Param|null>,
|
||||
* cacheable_response_statuses?: list<int|Param>,
|
||||
@@ -2162,8 +2189,33 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||
* }>,
|
||||
* },
|
||||
* },
|
||||
* speculation_rules?: bool|array{ // Speculation Rules API configuration for prefetching and prerendering pages.
|
||||
* enabled?: bool|Param, // Default: false
|
||||
* prefetch?: list<array{ // Default: []
|
||||
* source?: "list"|"document"|Param, // The source type: "list" for explicit URLs, "document" for link matching. // Default: "document"
|
||||
* urls?: list<string|array{ // Default: []
|
||||
* path?: scalar|Param|null, // The URL path or route name.
|
||||
* params?: list<mixed>,
|
||||
* }>,
|
||||
* selector_matches?: scalar|Param|null, // For "document" source: CSS selector to match links. // Default: null
|
||||
* href_matches?: scalar|Param|null, // For "document" source: URL pattern to match href attributes. // Default: null
|
||||
* eagerness?: "immediate"|"eager"|"moderate"|"conservative"|Param, // Eagerness level: "immediate" (viewport), "eager" (hover 200ms), "moderate" (hover 100ms), "conservative" (mousedown/touchstart). // Default: "moderate"
|
||||
* referrer_policy?: scalar|Param|null, // Referrer policy for the speculative request. // Default: null
|
||||
* }>,
|
||||
* prerender?: list<array{ // Default: []
|
||||
* source?: "list"|"document"|Param, // The source type: "list" for explicit URLs, "document" for link matching. // Default: "document"
|
||||
* urls?: list<string|array{ // Default: []
|
||||
* path?: scalar|Param|null, // The URL path or route name.
|
||||
* params?: list<mixed>,
|
||||
* }>,
|
||||
* selector_matches?: scalar|Param|null, // For "document" source: CSS selector to match links. // Default: null
|
||||
* href_matches?: scalar|Param|null, // For "document" source: URL pattern to match href attributes. // Default: null
|
||||
* eagerness?: "immediate"|"eager"|"moderate"|"conservative"|Param, // Eagerness level. For prerender, "conservative" is recommended. // Default: "conservative"
|
||||
* referrer_policy?: scalar|Param|null, // Referrer policy for the speculative request. // Default: null
|
||||
* }>,
|
||||
* },
|
||||
* web_client?: scalar|Param|null, // The Panther Client for generating screenshots. If not set, the default client will be used. // Default: null
|
||||
* user_agent?: scalar|Param|null, // The user agent to use when generating screenshots. If not set, the default user agent will be used. When requesting the current application in an environment other than "prod", the profiler will be disabled. // Default: null
|
||||
* user_agent?: scalar|Param|null, // The user agent to use when generating screenshots. When this user agent is detected, the Symfony profiler and debug toolbar will be automatically disabled to ensure screenshots look like production. // Default: "PWAScreenshotBot"
|
||||
* }
|
||||
* @psalm-type ConfigType = array{
|
||||
* imports?: ImportsConfig,
|
||||
|
||||
31
migrations/Version20260320221953.php
Normal file
31
migrations/Version20260320221953.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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 Version20260320221953 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 category ADD is_hidden BOOLEAN DEFAULT false NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE category DROP is_hidden');
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@
|
||||
<exclude>
|
||||
<file>src/Controller/StripeWebhookController.php</file>
|
||||
<file>src/Service/StripeService.php</file>
|
||||
<file>src/Service/PayoutPdfService.php</file>
|
||||
</exclude>
|
||||
|
||||
<deprecationTrigger>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
sonar.projectKey=e-ticket
|
||||
sonar.projectName=E-Ticket
|
||||
sonar.sources=src,assets,templates,docker
|
||||
sonar.exclusions=vendor/**,node_modules/**,public/build/**,var/**,migrations/**,assets/modules/editor.js,assets/modules/event-map.js,src/Controller/StripeWebhookController.php,src/Service/StripeService.php
|
||||
sonar.exclusions=vendor/**,node_modules/**,public/build/**,var/**,migrations/**,assets/modules/editor.js,assets/modules/event-map.js,src/Controller/StripeWebhookController.php,src/Service/StripeService.php,src/Service/PayoutPdfService.php
|
||||
sonar.php.version=8.4
|
||||
sonar.sourceEncoding=UTF-8
|
||||
sonar.php.coverage.reportPaths=coverage.xml
|
||||
|
||||
@@ -462,6 +462,8 @@ class AccountController extends AbstractController
|
||||
$category->setEndAt($category->getStartAt()->modify('+30 days'));
|
||||
}
|
||||
|
||||
$category->setIsHidden($request->request->getBoolean('is_hidden'));
|
||||
|
||||
$em->flush();
|
||||
|
||||
$this->addFlash('success', 'Categorie modifiee.');
|
||||
|
||||
@@ -29,6 +29,9 @@ class Category
|
||||
#[ORM\Column]
|
||||
private \DateTimeImmutable $endAt;
|
||||
|
||||
#[ORM\Column]
|
||||
private bool $isHidden = false;
|
||||
|
||||
#[ORM\Column]
|
||||
private \DateTimeImmutable $createdAt;
|
||||
|
||||
@@ -109,6 +112,18 @@ class Category
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isHidden(): bool
|
||||
{
|
||||
return $this->isHidden;
|
||||
}
|
||||
|
||||
public function setIsHidden(bool $isHidden): static
|
||||
{
|
||||
$this->isHidden = $isHidden;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): \DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
|
||||
@@ -11,6 +11,9 @@ use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
/**
|
||||
* @codeCoverageIgnore PDF generation with dompdf + QR code
|
||||
*/
|
||||
class PayoutPdfService
|
||||
{
|
||||
public function __construct(
|
||||
|
||||
@@ -34,6 +34,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="checkbox" id="cat_hidden" name="is_hidden" value="1" class="w-5 h-5 border-2 border-gray-900 cursor-pointer" {{ category.hidden ? 'checked' : '' }}>
|
||||
<label for="cat_hidden" class="text-sm font-black uppercase tracking-widest cursor-pointer">Masquer sur la page evenement</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="btn-brutal font-black uppercase text-sm tracking-widest hover:bg-indigo-600 hover:text-white transition-all">
|
||||
Enregistrer
|
||||
|
||||
@@ -189,7 +189,9 @@
|
||||
<span class="text-gray-400 cursor-grab">☰</span>
|
||||
<span class="font-black text-sm uppercase flex-1">{{ category.name }}</span>
|
||||
<span class="text-xs font-bold text-gray-400">{{ category.startAt|date('d/m/Y H:i') }} — {{ category.endAt|date('d/m/Y H:i') }}</span>
|
||||
{% if category.active %}
|
||||
{% if category.hidden %}
|
||||
<span class="badge-yellow text-xs font-black uppercase">Masquee</span>
|
||||
{% elseif category.active %}
|
||||
<span class="badge-green text-xs font-black uppercase">Active</span>
|
||||
{% else %}
|
||||
<span class="badge-red text-xs font-black uppercase">Inactive</span>
|
||||
|
||||
@@ -838,6 +838,299 @@ class AccountControllerTest extends WebTestCase
|
||||
self::assertResponseStatusCodeSame(403);
|
||||
}
|
||||
|
||||
public function testAddCategory(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
|
||||
$event = $this->createEvent($em, $user);
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/ajouter', [
|
||||
'name' => 'VIP',
|
||||
'start_at' => '2026-06-01T10:00',
|
||||
'end_at' => '2026-07-31T18:00',
|
||||
]);
|
||||
|
||||
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=categories');
|
||||
}
|
||||
|
||||
public function testAddCategoryEmptyName(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
|
||||
$event = $this->createEvent($em, $user);
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/ajouter', [
|
||||
'name' => '',
|
||||
]);
|
||||
|
||||
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=categories');
|
||||
}
|
||||
|
||||
public function testAddCategoryDeniedForOtherUser(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
|
||||
$event = $this->createEvent($em, $owner);
|
||||
|
||||
$client->loginUser($other);
|
||||
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/ajouter', [
|
||||
'name' => 'Hack',
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(403);
|
||||
}
|
||||
|
||||
public function testAddCategoryWithInvertedDates(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
|
||||
$event = $this->createEvent($em, $user);
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/ajouter', [
|
||||
'name' => 'Inverted',
|
||||
'start_at' => '2026-08-01T10:00',
|
||||
'end_at' => '2026-06-01T10:00',
|
||||
]);
|
||||
|
||||
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=categories');
|
||||
|
||||
$category = $em->getRepository(\App\Entity\Category::class)->findOneBy(['name' => 'Inverted']);
|
||||
self::assertNotNull($category);
|
||||
self::assertGreaterThanOrEqual($category->getStartAt(), $category->getEndAt());
|
||||
}
|
||||
|
||||
public function testEditCategoryPage(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
|
||||
$event = $this->createEvent($em, $user);
|
||||
$category = $this->createCategory($em, $event);
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/categorie/'.$category->getId().'/modifier');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testEditCategorySubmit(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
|
||||
$event = $this->createEvent($em, $user);
|
||||
$category = $this->createCategory($em, $event);
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/'.$category->getId().'/modifier', [
|
||||
'name' => 'Updated Name',
|
||||
'start_at' => '2026-06-01T10:00',
|
||||
'end_at' => '2026-07-31T18:00',
|
||||
'is_hidden' => '1',
|
||||
]);
|
||||
|
||||
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=categories');
|
||||
|
||||
$em->refresh($category);
|
||||
self::assertSame('Updated Name', $category->getName());
|
||||
self::assertTrue($category->isHidden());
|
||||
}
|
||||
|
||||
public function testEditCategoryWithInvertedDates(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
|
||||
$event = $this->createEvent($em, $user);
|
||||
$category = $this->createCategory($em, $event);
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/'.$category->getId().'/modifier', [
|
||||
'name' => 'Inverted Edit',
|
||||
'start_at' => '2026-08-01T10:00',
|
||||
'end_at' => '2026-06-01T10:00',
|
||||
]);
|
||||
|
||||
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=categories');
|
||||
|
||||
$em->refresh($category);
|
||||
self::assertGreaterThanOrEqual($category->getStartAt(), $category->getEndAt());
|
||||
}
|
||||
|
||||
public function testEditCategoryDeniedForOtherUser(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
|
||||
$event = $this->createEvent($em, $owner);
|
||||
$category = $this->createCategory($em, $event);
|
||||
|
||||
$client->loginUser($other);
|
||||
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/categorie/'.$category->getId().'/modifier');
|
||||
|
||||
self::assertResponseStatusCodeSame(403);
|
||||
}
|
||||
|
||||
public function testEditCategoryNotFound(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
|
||||
$event = $this->createEvent($em, $user);
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/categorie/999999/modifier');
|
||||
|
||||
self::assertResponseStatusCodeSame(404);
|
||||
}
|
||||
|
||||
public function testDeleteCategory(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
|
||||
$event = $this->createEvent($em, $user);
|
||||
$category = $this->createCategory($em, $event);
|
||||
$categoryId = $category->getId();
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/'.$categoryId.'/supprimer');
|
||||
|
||||
self::assertResponseRedirects('/mon-compte/evenement/'.$event->getId().'/modifier?tab=categories');
|
||||
|
||||
self::assertNull($em->getRepository(\App\Entity\Category::class)->find($categoryId));
|
||||
}
|
||||
|
||||
public function testDeleteCategoryDeniedForOtherUser(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
|
||||
$event = $this->createEvent($em, $owner);
|
||||
$category = $this->createCategory($em, $event);
|
||||
|
||||
$client->loginUser($other);
|
||||
$client->request('POST', '/mon-compte/evenement/'.$event->getId().'/categorie/'.$category->getId().'/supprimer');
|
||||
|
||||
self::assertResponseStatusCodeSame(403);
|
||||
}
|
||||
|
||||
public function testReorderCategories(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
|
||||
$event = $this->createEvent($em, $user);
|
||||
$cat1 = $this->createCategory($em, $event, 'Cat A', 0);
|
||||
$cat2 = $this->createCategory($em, $event, 'Cat B', 1);
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request(
|
||||
'POST',
|
||||
'/mon-compte/evenement/'.$event->getId().'/categorie/reorder',
|
||||
[],
|
||||
[],
|
||||
['CONTENT_TYPE' => 'application/json'],
|
||||
json_encode([$cat2->getId(), $cat1->getId()])
|
||||
);
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
|
||||
$em->refresh($cat1);
|
||||
$em->refresh($cat2);
|
||||
self::assertSame(1, $cat1->getPosition());
|
||||
self::assertSame(0, $cat2->getPosition());
|
||||
}
|
||||
|
||||
public function testReorderCategoriesDeniedForOtherUser(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$owner = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
$other = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
|
||||
$event = $this->createEvent($em, $owner);
|
||||
|
||||
$client->loginUser($other);
|
||||
$client->request(
|
||||
'POST',
|
||||
'/mon-compte/evenement/'.$event->getId().'/categorie/reorder',
|
||||
[],
|
||||
[],
|
||||
['CONTENT_TYPE' => 'application/json'],
|
||||
'[]'
|
||||
);
|
||||
|
||||
self::assertResponseStatusCodeSame(403);
|
||||
}
|
||||
|
||||
public function testEditEventCategoriesTab(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||
$user = $this->createUser(['ROLE_ORGANIZER'], true);
|
||||
|
||||
$event = $this->createEvent($em, $user);
|
||||
$this->createCategory($em, $event);
|
||||
|
||||
$client->loginUser($user);
|
||||
$client->request('GET', '/mon-compte/evenement/'.$event->getId().'/modifier?tab=categories');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
private function createEvent(EntityManagerInterface $em, User $user): \App\Entity\Event
|
||||
{
|
||||
$event = new \App\Entity\Event();
|
||||
$event->setAccount($user);
|
||||
$event->setTitle('Test Event '.uniqid());
|
||||
$event->setStartAt(new \DateTimeImmutable('2026-08-01 10:00'));
|
||||
$event->setEndAt(new \DateTimeImmutable('2026-08-01 18:00'));
|
||||
$event->setAddress('1 rue test');
|
||||
$event->setZipcode('75001');
|
||||
$event->setCity('Paris');
|
||||
$em->persist($event);
|
||||
$em->flush();
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
private function createCategory(EntityManagerInterface $em, \App\Entity\Event $event, string $name = 'Test Cat', int $position = 0): \App\Entity\Category
|
||||
{
|
||||
$category = new \App\Entity\Category();
|
||||
$category->setName($name);
|
||||
$category->setEvent($event);
|
||||
$category->setPosition($position);
|
||||
$category->setStartAt(new \DateTimeImmutable('2026-06-01 10:00'));
|
||||
$category->setEndAt(new \DateTimeImmutable('2026-07-31 18:00'));
|
||||
$em->persist($category);
|
||||
$em->flush();
|
||||
|
||||
return $category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $roles
|
||||
*/
|
||||
|
||||
168
tests/Entity/CategoryTest.php
Normal file
168
tests/Entity/CategoryTest.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\Category;
|
||||
use App\Entity\Event;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class CategoryTest extends TestCase
|
||||
{
|
||||
public function testNewCategoryDefaults(): void
|
||||
{
|
||||
$category = new Category();
|
||||
|
||||
self::assertNull($category->getId());
|
||||
self::assertNull($category->getName());
|
||||
self::assertNull($category->getEvent());
|
||||
self::assertSame(0, $category->getPosition());
|
||||
self::assertFalse($category->isHidden());
|
||||
self::assertInstanceOf(\DateTimeImmutable::class, $category->getCreatedAt());
|
||||
self::assertInstanceOf(\DateTimeImmutable::class, $category->getStartAt());
|
||||
self::assertInstanceOf(\DateTimeImmutable::class, $category->getEndAt());
|
||||
}
|
||||
|
||||
public function testSetAndGetName(): void
|
||||
{
|
||||
$category = new Category();
|
||||
$result = $category->setName('VIP');
|
||||
|
||||
self::assertSame('VIP', $category->getName());
|
||||
self::assertSame($category, $result);
|
||||
}
|
||||
|
||||
public function testSetAndGetPosition(): void
|
||||
{
|
||||
$category = new Category();
|
||||
$result = $category->setPosition(3);
|
||||
|
||||
self::assertSame(3, $category->getPosition());
|
||||
self::assertSame($category, $result);
|
||||
}
|
||||
|
||||
public function testSetAndGetStartAt(): void
|
||||
{
|
||||
$category = new Category();
|
||||
$date = new \DateTimeImmutable('2026-06-01 10:00:00');
|
||||
$result = $category->setStartAt($date);
|
||||
|
||||
self::assertSame($date, $category->getStartAt());
|
||||
self::assertSame($category, $result);
|
||||
}
|
||||
|
||||
public function testSetAndGetEndAt(): void
|
||||
{
|
||||
$category = new Category();
|
||||
$date = new \DateTimeImmutable('2026-06-15 18:00:00');
|
||||
$result = $category->setEndAt($date);
|
||||
|
||||
self::assertSame($date, $category->getEndAt());
|
||||
self::assertSame($category, $result);
|
||||
}
|
||||
|
||||
public function testSetAndGetIsHidden(): void
|
||||
{
|
||||
$category = new Category();
|
||||
$result = $category->setIsHidden(true);
|
||||
|
||||
self::assertTrue($category->isHidden());
|
||||
self::assertSame($category, $result);
|
||||
|
||||
$category->setIsHidden(false);
|
||||
self::assertFalse($category->isHidden());
|
||||
}
|
||||
|
||||
public function testSetEventSetsEndAtToOneDayBeforeEventStart(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setStartAt(new \DateTimeImmutable('+60 days'));
|
||||
|
||||
$category = new Category();
|
||||
$result = $category->setEvent($event);
|
||||
|
||||
self::assertSame($event, $category->getEvent());
|
||||
self::assertSame($category, $result);
|
||||
|
||||
$expectedEnd = $event->getStartAt()->modify('-1 day');
|
||||
self::assertSame(
|
||||
$expectedEnd->format('Y-m-d H:i:s'),
|
||||
$category->getEndAt()->format('Y-m-d H:i:s')
|
||||
);
|
||||
}
|
||||
|
||||
public function testSetEventWithPastStartAtUsesEventStartAt(): void
|
||||
{
|
||||
$event = new Event();
|
||||
$event->setStartAt(new \DateTimeImmutable('-1 day'));
|
||||
|
||||
$category = new Category();
|
||||
$category->setEvent($event);
|
||||
|
||||
// endCandidate (-2 days) < startAt (now), so endAt = event.startAt
|
||||
self::assertSame(
|
||||
$event->getStartAt()->format('Y-m-d H:i:s'),
|
||||
$category->getEndAt()->format('Y-m-d H:i:s')
|
||||
);
|
||||
}
|
||||
|
||||
public function testSetEventNull(): void
|
||||
{
|
||||
$category = new Category();
|
||||
$originalEnd = $category->getEndAt();
|
||||
|
||||
$category->setEvent(null);
|
||||
|
||||
self::assertNull($category->getEvent());
|
||||
self::assertSame($originalEnd, $category->getEndAt());
|
||||
}
|
||||
|
||||
public function testIsActiveWhenNowIsBetweenStartAndEnd(): void
|
||||
{
|
||||
$category = new Category();
|
||||
$category->setStartAt(new \DateTimeImmutable('-1 hour'));
|
||||
$category->setEndAt(new \DateTimeImmutable('+1 hour'));
|
||||
|
||||
self::assertTrue($category->isActive());
|
||||
}
|
||||
|
||||
public function testIsActiveWhenNowIsBeforeStart(): void
|
||||
{
|
||||
$category = new Category();
|
||||
$category->setStartAt(new \DateTimeImmutable('+1 hour'));
|
||||
$category->setEndAt(new \DateTimeImmutable('+2 hours'));
|
||||
|
||||
self::assertFalse($category->isActive());
|
||||
}
|
||||
|
||||
public function testIsActiveWhenNowIsAfterEnd(): void
|
||||
{
|
||||
$category = new Category();
|
||||
$category->setStartAt(new \DateTimeImmutable('-2 hours'));
|
||||
$category->setEndAt(new \DateTimeImmutable('-1 hour'));
|
||||
|
||||
self::assertFalse($category->isActive());
|
||||
}
|
||||
|
||||
public function testGetCreatedAt(): void
|
||||
{
|
||||
$before = new \DateTimeImmutable();
|
||||
$category = new Category();
|
||||
$after = new \DateTimeImmutable();
|
||||
|
||||
self::assertGreaterThanOrEqual($before, $category->getCreatedAt());
|
||||
self::assertLessThanOrEqual($after, $category->getCreatedAt());
|
||||
}
|
||||
|
||||
public function testSetEventWithNullStartAt(): void
|
||||
{
|
||||
$event = new Event();
|
||||
|
||||
$category = new Category();
|
||||
$originalEnd = $category->getEndAt();
|
||||
$category->setEvent($event);
|
||||
|
||||
// Event has no startAt set, so endAt should not change
|
||||
self::assertNull($event->getStartAt());
|
||||
self::assertSame($originalEnd, $category->getEndAt());
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,16 @@ class StripeServiceTest extends TestCase
|
||||
{
|
||||
private function createService(): StripeService
|
||||
{
|
||||
return new StripeService('sk_test', 'whsec_test', 'https://example.com');
|
||||
return new StripeService('sk_test', 'whsec_test', 'whsec_connect_test', 'https://example.com');
|
||||
}
|
||||
|
||||
public function testVerifyWebhookSignatureReturnsNullOnInvalid(): void
|
||||
{
|
||||
self::assertNull($this->createService()->verifyWebhookSignature('{}', 'invalid'));
|
||||
}
|
||||
|
||||
public function testVerifyConnectWebhookSignatureReturnsNullOnInvalid(): void
|
||||
{
|
||||
self::assertNull($this->createService()->verifyConnectWebhookSignature('{}', 'invalid'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,4 +91,28 @@ describe('initCookieConsent', () => {
|
||||
const script = document.querySelector('script[data-analytics]')
|
||||
expect(script).not.toBeNull()
|
||||
})
|
||||
|
||||
it('does not load analytics in dev environment', () => {
|
||||
document.body.dataset.env = 'dev'
|
||||
document.cookie = 'e_ticket_consent=accepted;path=/'
|
||||
initCookieConsent()
|
||||
const script = document.querySelector('script[data-analytics]')
|
||||
expect(script).toBeNull()
|
||||
})
|
||||
|
||||
it('loads cloudflare tunnel script on accept', () => {
|
||||
initCookieConsent()
|
||||
document.getElementById('cookie-accept').click()
|
||||
const script = document.querySelector('script[data-cf-beacon]')
|
||||
expect(script).not.toBeNull()
|
||||
expect(script.src).toContain('/assets/perf.js')
|
||||
})
|
||||
|
||||
it('does not duplicate cloudflare script', () => {
|
||||
document.cookie = 'e_ticket_consent=accepted;path=/'
|
||||
initCookieConsent()
|
||||
initCookieConsent()
|
||||
const scripts = document.querySelectorAll('script[data-cf-beacon]')
|
||||
expect(scripts.length).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user