Les administrateurs peuvent désormais configurer l'identité visuelle de leur établissement : upload d'un logo (PNG/JPG, redimensionné automatiquement via Imagick) et choix d'une couleur principale appliquée aux boutons et à la navigation. La couleur est validée côté client et serveur pour garantir la conformité WCAG AA (contraste ≥ 4.5:1 sur fond blanc). Les personnalisations sont injectées dynamiquement via CSS variables et visibles immédiatement après sauvegarde.
191 lines
5.7 KiB
PHP
191 lines
5.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Administration\Application\Command\DeleteLogo;
|
|
|
|
use App\Administration\Application\Command\DeleteLogo\DeleteLogoCommand;
|
|
use App\Administration\Application\Command\DeleteLogo\DeleteLogoHandler;
|
|
use App\Administration\Application\Service\LogoUploader;
|
|
use App\Administration\Domain\Event\BrandingModifie;
|
|
use App\Administration\Domain\Exception\SchoolBrandingNotFoundException;
|
|
use App\Administration\Domain\Model\SchoolBranding\LogoUrl;
|
|
use App\Administration\Domain\Model\SchoolBranding\SchoolBranding;
|
|
use App\Administration\Domain\Model\SchoolClass\SchoolId;
|
|
use App\Administration\Infrastructure\Persistence\InMemory\InMemorySchoolBrandingRepository;
|
|
use App\Administration\Infrastructure\Storage\InMemoryImageProcessor;
|
|
use App\Administration\Infrastructure\Storage\InMemoryLogoStorage;
|
|
use App\Shared\Domain\Clock;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class DeleteLogoHandlerTest extends TestCase
|
|
{
|
|
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
|
private const string SCHOOL_ID = '550e8400-e29b-41d4-a716-446655440002';
|
|
|
|
private InMemorySchoolBrandingRepository $brandingRepository;
|
|
private InMemoryLogoStorage $logoStorage;
|
|
private Clock $clock;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->brandingRepository = new InMemorySchoolBrandingRepository();
|
|
$this->logoStorage = new InMemoryLogoStorage();
|
|
$this->clock = new class implements Clock {
|
|
public function now(): DateTimeImmutable
|
|
{
|
|
return new DateTimeImmutable('2026-02-20 10:00:00');
|
|
}
|
|
};
|
|
}
|
|
|
|
#[Test]
|
|
public function itDeletesLogoFromBranding(): void
|
|
{
|
|
$this->seedBrandingWithLogo();
|
|
$handler = $this->createHandler();
|
|
|
|
$command = new DeleteLogoCommand(
|
|
tenantId: self::TENANT_ID,
|
|
schoolId: self::SCHOOL_ID,
|
|
);
|
|
|
|
$branding = $handler($command);
|
|
|
|
self::assertNull($branding->logoUrl);
|
|
self::assertNull($branding->logoUpdatedAt);
|
|
}
|
|
|
|
#[Test]
|
|
public function itDeletesFileFromStorage(): void
|
|
{
|
|
$this->seedBrandingWithLogo();
|
|
self::assertSame(1, $this->logoStorage->count());
|
|
|
|
$handler = $this->createHandler();
|
|
|
|
$command = new DeleteLogoCommand(
|
|
tenantId: self::TENANT_ID,
|
|
schoolId: self::SCHOOL_ID,
|
|
);
|
|
|
|
$handler($command);
|
|
|
|
self::assertSame(0, $this->logoStorage->count());
|
|
}
|
|
|
|
#[Test]
|
|
public function itRecordsDomainEventOnDeletion(): void
|
|
{
|
|
$this->seedBrandingWithLogo();
|
|
$handler = $this->createHandler();
|
|
|
|
$command = new DeleteLogoCommand(
|
|
tenantId: self::TENANT_ID,
|
|
schoolId: self::SCHOOL_ID,
|
|
);
|
|
|
|
$branding = $handler($command);
|
|
|
|
$events = $branding->pullDomainEvents();
|
|
self::assertCount(1, $events);
|
|
self::assertInstanceOf(BrandingModifie::class, $events[0]);
|
|
}
|
|
|
|
#[Test]
|
|
public function itPersistsBrandingAfterDeletion(): void
|
|
{
|
|
$this->seedBrandingWithLogo();
|
|
$handler = $this->createHandler();
|
|
|
|
$command = new DeleteLogoCommand(
|
|
tenantId: self::TENANT_ID,
|
|
schoolId: self::SCHOOL_ID,
|
|
);
|
|
|
|
$handler($command);
|
|
|
|
$persisted = $this->brandingRepository->findBySchoolId(
|
|
SchoolId::fromString(self::SCHOOL_ID),
|
|
TenantId::fromString(self::TENANT_ID),
|
|
);
|
|
self::assertNotNull($persisted);
|
|
self::assertNull($persisted->logoUrl);
|
|
}
|
|
|
|
#[Test]
|
|
public function itDoesNothingWhenNoLogoExists(): void
|
|
{
|
|
$this->seedBrandingWithoutLogo();
|
|
$handler = $this->createHandler();
|
|
|
|
$command = new DeleteLogoCommand(
|
|
tenantId: self::TENANT_ID,
|
|
schoolId: self::SCHOOL_ID,
|
|
);
|
|
|
|
$branding = $handler($command);
|
|
|
|
$events = $branding->pullDomainEvents();
|
|
self::assertCount(0, $events);
|
|
}
|
|
|
|
#[Test]
|
|
public function itThrowsWhenBrandingDoesNotExist(): void
|
|
{
|
|
$handler = $this->createHandler();
|
|
|
|
$command = new DeleteLogoCommand(
|
|
tenantId: self::TENANT_ID,
|
|
schoolId: self::SCHOOL_ID,
|
|
);
|
|
|
|
$this->expectException(SchoolBrandingNotFoundException::class);
|
|
|
|
$handler($command);
|
|
}
|
|
|
|
private function createHandler(): DeleteLogoHandler
|
|
{
|
|
return new DeleteLogoHandler(
|
|
$this->brandingRepository,
|
|
new LogoUploader($this->logoStorage, new InMemoryImageProcessor()),
|
|
$this->clock,
|
|
);
|
|
}
|
|
|
|
private function seedBrandingWithLogo(): void
|
|
{
|
|
// Store a file first so we can verify deletion
|
|
$this->logoStorage->store('fake-content', 'logos/tenant/test.png', 'image/png');
|
|
|
|
$branding = SchoolBranding::creer(
|
|
schoolId: SchoolId::fromString(self::SCHOOL_ID),
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
createdAt: new DateTimeImmutable('2026-02-01 10:00:00'),
|
|
);
|
|
|
|
$branding->changerLogo(
|
|
new LogoUrl('https://storage.example.com/logos/tenant/test.png'),
|
|
new DateTimeImmutable('2026-02-01 10:00:00'),
|
|
);
|
|
$branding->pullDomainEvents();
|
|
|
|
$this->brandingRepository->save($branding);
|
|
}
|
|
|
|
private function seedBrandingWithoutLogo(): void
|
|
{
|
|
$branding = SchoolBranding::creer(
|
|
schoolId: SchoolId::fromString(self::SCHOOL_ID),
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
createdAt: new DateTimeImmutable('2026-02-01 10:00:00'),
|
|
);
|
|
|
|
$this->brandingRepository->save($branding);
|
|
}
|
|
}
|