feat: Permettre la personnalisation du logo et de la couleur principale de l'établissement
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.
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Application\Command\UpdateBranding;
|
||||
|
||||
use App\Administration\Application\Command\UpdateBranding\UpdateBrandingCommand;
|
||||
use App\Administration\Application\Command\UpdateBranding\UpdateBrandingHandler;
|
||||
use App\Administration\Domain\Event\BrandingModifie;
|
||||
use App\Administration\Domain\Exception\BrandColorInvalideException;
|
||||
use App\Administration\Domain\Exception\ContrasteInsuffisantException;
|
||||
use App\Administration\Domain\Model\SchoolBranding\BrandColor;
|
||||
use App\Administration\Domain\Model\SchoolBranding\ContrastValidator;
|
||||
use App\Administration\Domain\Model\SchoolBranding\SchoolBranding;
|
||||
use App\Administration\Domain\Model\SchoolClass\SchoolId;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemorySchoolBrandingRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class UpdateBrandingHandlerTest 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 ContrastValidator $contrastValidator;
|
||||
private Clock $clock;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->brandingRepository = new InMemorySchoolBrandingRepository();
|
||||
$this->contrastValidator = new ContrastValidator();
|
||||
$this->clock = new class implements Clock {
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('2026-02-20 10:00:00');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itCreatesBrandingWhenNoneExists(): void
|
||||
{
|
||||
$handler = new UpdateBrandingHandler($this->brandingRepository, $this->contrastValidator, $this->clock);
|
||||
$command = new UpdateBrandingCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
primaryColor: '#1E40AF',
|
||||
secondaryColor: null,
|
||||
accentColor: null,
|
||||
);
|
||||
|
||||
$branding = $handler($command);
|
||||
|
||||
self::assertNotNull($branding->primaryColor);
|
||||
self::assertSame('#1E40AF', (string) $branding->primaryColor);
|
||||
self::assertNull($branding->secondaryColor);
|
||||
self::assertNull($branding->accentColor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itUpdatesExistingBranding(): void
|
||||
{
|
||||
$this->seedBranding();
|
||||
|
||||
$handler = new UpdateBrandingHandler($this->brandingRepository, $this->contrastValidator, $this->clock);
|
||||
$command = new UpdateBrandingCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
primaryColor: '#B91C1C',
|
||||
secondaryColor: '#1E40AF',
|
||||
accentColor: '#60A5FA',
|
||||
);
|
||||
|
||||
$branding = $handler($command);
|
||||
|
||||
self::assertNotNull($branding->primaryColor);
|
||||
self::assertSame('#B91C1C', (string) $branding->primaryColor);
|
||||
self::assertNotNull($branding->secondaryColor);
|
||||
self::assertSame('#1E40AF', (string) $branding->secondaryColor);
|
||||
self::assertNotNull($branding->accentColor);
|
||||
self::assertSame('#60A5FA', (string) $branding->accentColor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itResetsColorsToNull(): void
|
||||
{
|
||||
$this->seedBranding(primaryColor: '#1E40AF');
|
||||
|
||||
$handler = new UpdateBrandingHandler($this->brandingRepository, $this->contrastValidator, $this->clock);
|
||||
$command = new UpdateBrandingCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
primaryColor: null,
|
||||
secondaryColor: null,
|
||||
accentColor: null,
|
||||
);
|
||||
|
||||
$branding = $handler($command);
|
||||
|
||||
self::assertNull($branding->primaryColor);
|
||||
self::assertNull($branding->secondaryColor);
|
||||
self::assertNull($branding->accentColor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itRecordsDomainEvent(): void
|
||||
{
|
||||
$handler = new UpdateBrandingHandler($this->brandingRepository, $this->contrastValidator, $this->clock);
|
||||
$command = new UpdateBrandingCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
primaryColor: '#1E40AF',
|
||||
secondaryColor: null,
|
||||
accentColor: null,
|
||||
);
|
||||
|
||||
$branding = $handler($command);
|
||||
|
||||
$events = $branding->pullDomainEvents();
|
||||
self::assertCount(1, $events);
|
||||
self::assertInstanceOf(BrandingModifie::class, $events[0]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itPersistsBranding(): void
|
||||
{
|
||||
$handler = new UpdateBrandingHandler($this->brandingRepository, $this->contrastValidator, $this->clock);
|
||||
$command = new UpdateBrandingCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
primaryColor: '#1E40AF',
|
||||
secondaryColor: null,
|
||||
accentColor: null,
|
||||
);
|
||||
|
||||
$handler($command);
|
||||
|
||||
$persisted = $this->brandingRepository->findBySchoolId(
|
||||
SchoolId::fromString(self::SCHOOL_ID),
|
||||
TenantId::fromString(self::TENANT_ID),
|
||||
);
|
||||
self::assertNotNull($persisted);
|
||||
self::assertNotNull($persisted->primaryColor);
|
||||
self::assertSame('#1E40AF', (string) $persisted->primaryColor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itRejectsColorWithInsufficientContrast(): void
|
||||
{
|
||||
$this->expectException(ContrasteInsuffisantException::class);
|
||||
|
||||
$handler = new UpdateBrandingHandler($this->brandingRepository, $this->contrastValidator, $this->clock);
|
||||
$command = new UpdateBrandingCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
primaryColor: '#FFFF00',
|
||||
secondaryColor: null,
|
||||
accentColor: null,
|
||||
);
|
||||
|
||||
$handler($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itThrowsExceptionForInvalidColor(): void
|
||||
{
|
||||
$this->expectException(BrandColorInvalideException::class);
|
||||
|
||||
$handler = new UpdateBrandingHandler($this->brandingRepository, $this->contrastValidator, $this->clock);
|
||||
$command = new UpdateBrandingCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
primaryColor: 'not-a-color',
|
||||
secondaryColor: null,
|
||||
accentColor: null,
|
||||
);
|
||||
|
||||
$handler($command);
|
||||
}
|
||||
|
||||
private function seedBranding(?string $primaryColor = null): 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'),
|
||||
);
|
||||
|
||||
if ($primaryColor !== null) {
|
||||
$branding->modifierCouleurs(
|
||||
primaryColor: new BrandColor($primaryColor),
|
||||
secondaryColor: null,
|
||||
accentColor: null,
|
||||
at: new DateTimeImmutable('2026-02-01 10:00:00'),
|
||||
);
|
||||
$branding->pullDomainEvents();
|
||||
}
|
||||
|
||||
$this->brandingRepository->save($branding);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Application\Command\UploadLogo;
|
||||
|
||||
use App\Administration\Application\Command\UploadLogo\UploadLogoCommand;
|
||||
use App\Administration\Application\Command\UploadLogo\UploadLogoHandler;
|
||||
use App\Administration\Application\Service\LogoUploader;
|
||||
use App\Administration\Domain\Event\BrandingModifie;
|
||||
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;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
final class UploadLogoHandlerTest 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 itUploadsLogoAndCreatesBranding(): void
|
||||
{
|
||||
$handler = $this->createHandler();
|
||||
$file = $this->createTestPngFile();
|
||||
|
||||
$command = new UploadLogoCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
file: $file,
|
||||
);
|
||||
|
||||
$branding = $handler($command);
|
||||
|
||||
self::assertNotNull($branding->logoUrl);
|
||||
self::assertStringStartsWith('https://storage.example.com/logos/', (string) $branding->logoUrl);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itUploadsLogoToExistingBranding(): void
|
||||
{
|
||||
$this->seedBranding();
|
||||
$handler = $this->createHandler();
|
||||
$file = $this->createTestPngFile();
|
||||
|
||||
$command = new UploadLogoCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
file: $file,
|
||||
);
|
||||
|
||||
$branding = $handler($command);
|
||||
|
||||
self::assertNotNull($branding->logoUrl);
|
||||
self::assertStringStartsWith('https://storage.example.com/logos/', (string) $branding->logoUrl);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itDeletesOldFileWhenReplacingLogo(): void
|
||||
{
|
||||
$this->seedBranding();
|
||||
self::assertSame(1, $this->logoStorage->count());
|
||||
|
||||
$handler = $this->createHandler();
|
||||
$file = $this->createTestPngFile();
|
||||
|
||||
$command = new UploadLogoCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
file: $file,
|
||||
);
|
||||
|
||||
$handler($command);
|
||||
|
||||
// Old file deleted, new file stored → still 1
|
||||
self::assertSame(1, $this->logoStorage->count());
|
||||
self::assertFalse($this->logoStorage->has('logos/old-tenant/old-logo.png'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itRecordsDomainEvent(): void
|
||||
{
|
||||
$handler = $this->createHandler();
|
||||
$file = $this->createTestPngFile();
|
||||
|
||||
$command = new UploadLogoCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
file: $file,
|
||||
);
|
||||
|
||||
$branding = $handler($command);
|
||||
|
||||
$events = $branding->pullDomainEvents();
|
||||
self::assertCount(1, $events);
|
||||
self::assertInstanceOf(BrandingModifie::class, $events[0]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itPersistsBrandingAfterUpload(): void
|
||||
{
|
||||
$handler = $this->createHandler();
|
||||
$file = $this->createTestPngFile();
|
||||
|
||||
$command = new UploadLogoCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
file: $file,
|
||||
);
|
||||
|
||||
$handler($command);
|
||||
|
||||
$persisted = $this->brandingRepository->findBySchoolId(
|
||||
SchoolId::fromString(self::SCHOOL_ID),
|
||||
TenantId::fromString(self::TENANT_ID),
|
||||
);
|
||||
self::assertNotNull($persisted);
|
||||
self::assertNotNull($persisted->logoUrl);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itStoresFileInStorage(): void
|
||||
{
|
||||
$handler = $this->createHandler();
|
||||
$file = $this->createTestPngFile();
|
||||
|
||||
$command = new UploadLogoCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
schoolId: self::SCHOOL_ID,
|
||||
file: $file,
|
||||
);
|
||||
|
||||
$handler($command);
|
||||
|
||||
self::assertSame(1, $this->logoStorage->count());
|
||||
}
|
||||
|
||||
private function createHandler(): UploadLogoHandler
|
||||
{
|
||||
return new UploadLogoHandler(
|
||||
$this->brandingRepository,
|
||||
new LogoUploader($this->logoStorage, new InMemoryImageProcessor()),
|
||||
$this->clock,
|
||||
);
|
||||
}
|
||||
|
||||
private function createTestPngFile(): UploadedFile
|
||||
{
|
||||
// Minimal valid PNG (1x1 pixel, transparent)
|
||||
$pngData = base64_decode(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', true,
|
||||
);
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'logo_test_');
|
||||
|
||||
if ($tmpFile === false) {
|
||||
self::fail('Failed to create temp file');
|
||||
}
|
||||
|
||||
file_put_contents($tmpFile, $pngData);
|
||||
|
||||
return new UploadedFile(
|
||||
$tmpFile,
|
||||
'test-logo.png',
|
||||
'image/png',
|
||||
test: true,
|
||||
);
|
||||
}
|
||||
|
||||
private function seedBranding(): void
|
||||
{
|
||||
// Store an old file so we can verify it gets deleted on replacement
|
||||
$this->logoStorage->store('old-content', 'logos/old-tenant/old-logo.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/old-tenant/old-logo.png'),
|
||||
new DateTimeImmutable('2026-02-01 10:00:00'),
|
||||
);
|
||||
$branding->pullDomainEvents();
|
||||
|
||||
$this->brandingRepository->save($branding);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Domain\Model\SchoolBranding;
|
||||
|
||||
use App\Administration\Domain\Exception\BrandColorInvalideException;
|
||||
use App\Administration\Domain\Model\SchoolBranding\BrandColor;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class BrandColorTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function constructWithValidColor(): void
|
||||
{
|
||||
$color = new BrandColor('#3B82F6');
|
||||
|
||||
self::assertSame('#3B82F6', $color->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructNormalizesToUppercase(): void
|
||||
{
|
||||
$color = new BrandColor('#3b82f6');
|
||||
|
||||
self::assertSame('#3B82F6', $color->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructTrimsWhitespace(): void
|
||||
{
|
||||
$color = new BrandColor(' #3B82F6 ');
|
||||
|
||||
self::assertSame('#3B82F6', $color->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('validColorsProvider')]
|
||||
public function constructWithValidColors(string $input, string $expected): void
|
||||
{
|
||||
$color = new BrandColor($input);
|
||||
|
||||
self::assertSame($expected, $color->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string, string}>
|
||||
*/
|
||||
public static function validColorsProvider(): iterable
|
||||
{
|
||||
yield 'blue' => ['#3B82F6', '#3B82F6'];
|
||||
yield 'red' => ['#EF4444', '#EF4444'];
|
||||
yield 'green' => ['#10B981', '#10B981'];
|
||||
yield 'black' => ['#000000', '#000000'];
|
||||
yield 'white' => ['#FFFFFF', '#FFFFFF'];
|
||||
yield 'lowercase' => ['#aabbcc', '#AABBCC'];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('invalidColorsProvider')]
|
||||
public function constructThrowsExceptionForInvalidColor(string $invalidColor): void
|
||||
{
|
||||
$this->expectException(BrandColorInvalideException::class);
|
||||
|
||||
new BrandColor($invalidColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function invalidColorsProvider(): iterable
|
||||
{
|
||||
yield 'empty string' => [''];
|
||||
yield 'no hash' => ['3B82F6'];
|
||||
yield 'short format' => ['#FFF'];
|
||||
yield 'too short' => ['#3B82F'];
|
||||
yield 'too long' => ['#3B82F6F'];
|
||||
yield 'invalid characters' => ['#GGGGGG'];
|
||||
yield 'rgb format' => ['rgb(59,130,246)'];
|
||||
yield 'named color' => ['blue'];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function toRgbReturnsCorrectValues(): void
|
||||
{
|
||||
$color = new BrandColor('#3B82F6');
|
||||
$rgb = $color->toRgb();
|
||||
|
||||
self::assertSame(59, $rgb['r']);
|
||||
self::assertSame(130, $rgb['g']);
|
||||
self::assertSame(246, $rgb['b']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function toRgbForBlack(): void
|
||||
{
|
||||
$color = new BrandColor('#000000');
|
||||
$rgb = $color->toRgb();
|
||||
|
||||
self::assertSame(0, $rgb['r']);
|
||||
self::assertSame(0, $rgb['g']);
|
||||
self::assertSame(0, $rgb['b']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function toRgbForWhite(): void
|
||||
{
|
||||
$color = new BrandColor('#FFFFFF');
|
||||
$rgb = $color->toRgb();
|
||||
|
||||
self::assertSame(255, $rgb['r']);
|
||||
self::assertSame(255, $rgb['g']);
|
||||
self::assertSame(255, $rgb['b']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function equalsReturnsTrueForSameValue(): void
|
||||
{
|
||||
$color1 = new BrandColor('#3B82F6');
|
||||
$color2 = new BrandColor('#3B82F6');
|
||||
|
||||
self::assertTrue($color1->equals($color2));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function equalsReturnsTrueForDifferentCase(): void
|
||||
{
|
||||
$color1 = new BrandColor('#3B82F6');
|
||||
$color2 = new BrandColor('#3b82f6');
|
||||
|
||||
self::assertTrue($color1->equals($color2));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function equalsReturnsFalseForDifferentValue(): void
|
||||
{
|
||||
$color1 = new BrandColor('#3B82F6');
|
||||
$color2 = new BrandColor('#EF4444');
|
||||
|
||||
self::assertFalse($color1->equals($color2));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function toStringReturnsValue(): void
|
||||
{
|
||||
$color = new BrandColor('#3B82F6');
|
||||
|
||||
self::assertSame('#3B82F6', (string) $color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Domain\Model\SchoolBranding;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolBranding\BrandColor;
|
||||
use App\Administration\Domain\Model\SchoolBranding\ContrastValidator;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ContrastValidatorTest extends TestCase
|
||||
{
|
||||
private ContrastValidator $validator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->validator = new ContrastValidator();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function blackOnWhitePassesWcagAA(): void
|
||||
{
|
||||
$result = $this->validator->validate(
|
||||
new BrandColor('#000000'),
|
||||
new BrandColor('#FFFFFF'),
|
||||
);
|
||||
|
||||
self::assertGreaterThanOrEqual(4.5, $result->ratio);
|
||||
self::assertTrue($result->passesAA);
|
||||
self::assertTrue($result->passesAALarge);
|
||||
self::assertNull($result->suggestion);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function whiteOnBlackPassesWcagAA(): void
|
||||
{
|
||||
$result = $this->validator->validate(
|
||||
new BrandColor('#FFFFFF'),
|
||||
new BrandColor('#000000'),
|
||||
);
|
||||
|
||||
self::assertGreaterThanOrEqual(4.5, $result->ratio);
|
||||
self::assertTrue($result->passesAA);
|
||||
self::assertTrue($result->passesAALarge);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function blackOnWhiteHasMaxContrast(): void
|
||||
{
|
||||
$result = $this->validator->validate(
|
||||
new BrandColor('#000000'),
|
||||
new BrandColor('#FFFFFF'),
|
||||
);
|
||||
|
||||
self::assertEqualsWithDelta(21.0, $result->ratio, 0.1);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function sameColorHasMinContrast(): void
|
||||
{
|
||||
$result = $this->validator->validate(
|
||||
new BrandColor('#3B82F6'),
|
||||
new BrandColor('#3B82F6'),
|
||||
);
|
||||
|
||||
self::assertEqualsWithDelta(1.0, $result->ratio, 0.01);
|
||||
self::assertFalse($result->passesAA);
|
||||
self::assertFalse($result->passesAALarge);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function lightGrayOnWhiteFailsWcagAA(): void
|
||||
{
|
||||
$result = $this->validator->validate(
|
||||
new BrandColor('#CCCCCC'),
|
||||
new BrandColor('#FFFFFF'),
|
||||
);
|
||||
|
||||
self::assertLessThan(4.5, $result->ratio);
|
||||
self::assertFalse($result->passesAA);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function lightGrayOnWhitePassesAALarge(): void
|
||||
{
|
||||
$result = $this->validator->validate(
|
||||
new BrandColor('#767676'),
|
||||
new BrandColor('#FFFFFF'),
|
||||
);
|
||||
|
||||
self::assertGreaterThanOrEqual(3.0, $result->ratio);
|
||||
self::assertTrue($result->passesAALarge);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function failingContrastSuggestsAlternative(): void
|
||||
{
|
||||
$result = $this->validator->validate(
|
||||
new BrandColor('#CCCCCC'),
|
||||
new BrandColor('#FFFFFF'),
|
||||
);
|
||||
|
||||
self::assertFalse($result->passesAA);
|
||||
self::assertNotNull($result->suggestion);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function passingContrastDoesNotSuggestAlternative(): void
|
||||
{
|
||||
$result = $this->validator->validate(
|
||||
new BrandColor('#000000'),
|
||||
new BrandColor('#FFFFFF'),
|
||||
);
|
||||
|
||||
self::assertTrue($result->passesAA);
|
||||
self::assertNull($result->suggestion);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function suggestedAlternativePassesWcagAA(): void
|
||||
{
|
||||
$result = $this->validator->validate(
|
||||
new BrandColor('#CCCCCC'),
|
||||
new BrandColor('#FFFFFF'),
|
||||
);
|
||||
|
||||
self::assertNotNull($result->suggestion);
|
||||
|
||||
$suggestionResult = $this->validator->validate(
|
||||
$result->suggestion,
|
||||
new BrandColor('#FFFFFF'),
|
||||
);
|
||||
|
||||
self::assertTrue($suggestionResult->passesAA);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function blueOnWhiteCalculatesCorrectRatio(): void
|
||||
{
|
||||
$result = $this->validator->validate(
|
||||
new BrandColor('#3B82F6'),
|
||||
new BrandColor('#FFFFFF'),
|
||||
);
|
||||
|
||||
self::assertGreaterThan(1.0, $result->ratio);
|
||||
self::assertLessThan(21.0, $result->ratio);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Domain\Model\SchoolBranding;
|
||||
|
||||
use App\Administration\Domain\Exception\LogoUrlInvalideException;
|
||||
use App\Administration\Domain\Model\SchoolBranding\LogoUrl;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class LogoUrlTest extends TestCase
|
||||
{
|
||||
#[Test]
|
||||
public function constructWithValidHttpsUrl(): void
|
||||
{
|
||||
$url = new LogoUrl('https://s3.example.com/logos/school-logo.png');
|
||||
|
||||
self::assertSame('https://s3.example.com/logos/school-logo.png', $url->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function constructWithValidHttpUrl(): void
|
||||
{
|
||||
$url = new LogoUrl('http://localhost:9000/logos/test.png');
|
||||
|
||||
self::assertSame('http://localhost:9000/logos/test.png', $url->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('validUrlsProvider')]
|
||||
public function constructWithValidUrls(string $input): void
|
||||
{
|
||||
$url = new LogoUrl($input);
|
||||
|
||||
self::assertSame($input, $url->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function validUrlsProvider(): iterable
|
||||
{
|
||||
yield 'https with path' => ['https://s3.amazonaws.com/bucket/logos/school.png'];
|
||||
yield 'https with query' => ['https://cdn.example.com/logo.png?v=12345'];
|
||||
yield 'http localhost' => ['http://localhost:9000/logos/test.jpg'];
|
||||
yield 'https with subdomain' => ['https://storage.googleapis.com/classeo/logos/abc.png'];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('invalidUrlsProvider')]
|
||||
public function constructThrowsExceptionForInvalidUrl(string $invalidUrl): void
|
||||
{
|
||||
$this->expectException(LogoUrlInvalideException::class);
|
||||
|
||||
new LogoUrl($invalidUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function invalidUrlsProvider(): iterable
|
||||
{
|
||||
yield 'empty string' => [''];
|
||||
yield 'not a url' => ['not-a-url'];
|
||||
yield 'just path' => ['/logos/school.png'];
|
||||
yield 'missing scheme' => ['s3.example.com/logo.png'];
|
||||
yield 'ftp scheme' => ['ftp://files.example.com/logo.png'];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function equalsReturnsTrueForSameValue(): void
|
||||
{
|
||||
$url1 = new LogoUrl('https://s3.example.com/logo.png');
|
||||
$url2 = new LogoUrl('https://s3.example.com/logo.png');
|
||||
|
||||
self::assertTrue($url1->equals($url2));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function equalsReturnsFalseForDifferentValue(): void
|
||||
{
|
||||
$url1 = new LogoUrl('https://s3.example.com/logo1.png');
|
||||
$url2 = new LogoUrl('https://s3.example.com/logo2.png');
|
||||
|
||||
self::assertFalse($url1->equals($url2));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function toStringReturnsValue(): void
|
||||
{
|
||||
$url = new LogoUrl('https://s3.example.com/logo.png');
|
||||
|
||||
self::assertSame('https://s3.example.com/logo.png', (string) $url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Domain\Model\SchoolBranding;
|
||||
|
||||
use App\Administration\Domain\Event\BrandingModifie;
|
||||
use App\Administration\Domain\Model\SchoolBranding\BrandColor;
|
||||
use App\Administration\Domain\Model\SchoolBranding\LogoUrl;
|
||||
use App\Administration\Domain\Model\SchoolBranding\SchoolBranding;
|
||||
use App\Administration\Domain\Model\SchoolClass\SchoolId;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class SchoolBrandingTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string SCHOOL_ID = '550e8400-e29b-41d4-a716-446655440002';
|
||||
|
||||
#[Test]
|
||||
public function creerWithDefaultValues(): void
|
||||
{
|
||||
$branding = $this->creerBranding();
|
||||
|
||||
self::assertTrue($branding->schoolId->equals(SchoolId::fromString(self::SCHOOL_ID)));
|
||||
self::assertTrue($branding->tenantId->equals(TenantId::fromString(self::TENANT_ID)));
|
||||
self::assertNull($branding->logoUrl);
|
||||
self::assertNull($branding->primaryColor);
|
||||
self::assertNull($branding->secondaryColor);
|
||||
self::assertNull($branding->accentColor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function creerRecordsNoEvent(): void
|
||||
{
|
||||
$branding = $this->creerBranding();
|
||||
|
||||
self::assertCount(0, $branding->pullDomainEvents());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function modifierCouleursUpdatesPrimaryColor(): void
|
||||
{
|
||||
$branding = $this->creerBranding();
|
||||
$primaryColor = new BrandColor('#3B82F6');
|
||||
$now = new DateTimeImmutable('2026-02-20 10:00:00');
|
||||
|
||||
$branding->modifierCouleurs(
|
||||
primaryColor: $primaryColor,
|
||||
secondaryColor: null,
|
||||
accentColor: null,
|
||||
at: $now,
|
||||
);
|
||||
|
||||
self::assertNotNull($branding->primaryColor);
|
||||
self::assertTrue($branding->primaryColor->equals($primaryColor));
|
||||
self::assertNull($branding->secondaryColor);
|
||||
self::assertNull($branding->accentColor);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function modifierCouleursUpdatesAllColors(): void
|
||||
{
|
||||
$branding = $this->creerBranding();
|
||||
$primary = new BrandColor('#3B82F6');
|
||||
$secondary = new BrandColor('#1E40AF');
|
||||
$accent = new BrandColor('#60A5FA');
|
||||
$now = new DateTimeImmutable('2026-02-20 10:00:00');
|
||||
|
||||
$branding->modifierCouleurs(
|
||||
primaryColor: $primary,
|
||||
secondaryColor: $secondary,
|
||||
accentColor: $accent,
|
||||
at: $now,
|
||||
);
|
||||
|
||||
self::assertNotNull($branding->primaryColor);
|
||||
self::assertTrue($branding->primaryColor->equals($primary));
|
||||
self::assertNotNull($branding->secondaryColor);
|
||||
self::assertTrue($branding->secondaryColor->equals($secondary));
|
||||
self::assertNotNull($branding->accentColor);
|
||||
self::assertTrue($branding->accentColor->equals($accent));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function modifierCouleursRecordsEvent(): void
|
||||
{
|
||||
$branding = $this->creerBranding();
|
||||
$primaryColor = new BrandColor('#3B82F6');
|
||||
$now = new DateTimeImmutable('2026-02-20 10:00:00');
|
||||
|
||||
$branding->modifierCouleurs(
|
||||
primaryColor: $primaryColor,
|
||||
secondaryColor: null,
|
||||
accentColor: null,
|
||||
at: $now,
|
||||
);
|
||||
|
||||
$events = $branding->pullDomainEvents();
|
||||
self::assertCount(1, $events);
|
||||
self::assertInstanceOf(BrandingModifie::class, $events[0]);
|
||||
self::assertSame($now, $events[0]->occurredOn());
|
||||
self::assertTrue($branding->schoolId->value->equals($events[0]->aggregateId()));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function modifierCouleursDoesNotRecordEventWhenNothingChanges(): void
|
||||
{
|
||||
$branding = $this->creerBranding();
|
||||
$primaryColor = new BrandColor('#3B82F6');
|
||||
$now = new DateTimeImmutable('2026-02-20 10:00:00');
|
||||
|
||||
$branding->modifierCouleurs(
|
||||
primaryColor: $primaryColor,
|
||||
secondaryColor: null,
|
||||
accentColor: null,
|
||||
at: $now,
|
||||
);
|
||||
|
||||
$branding->pullDomainEvents();
|
||||
|
||||
$branding->modifierCouleurs(
|
||||
primaryColor: $primaryColor,
|
||||
secondaryColor: null,
|
||||
accentColor: null,
|
||||
at: new DateTimeImmutable('2026-02-20 11:00:00'),
|
||||
);
|
||||
|
||||
self::assertCount(0, $branding->pullDomainEvents());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function changerLogoSetsLogoUrl(): void
|
||||
{
|
||||
$branding = $this->creerBranding();
|
||||
$logoUrl = new LogoUrl('https://s3.example.com/logos/school.png');
|
||||
$now = new DateTimeImmutable('2026-02-20 10:00:00');
|
||||
|
||||
$branding->changerLogo($logoUrl, $now);
|
||||
|
||||
self::assertNotNull($branding->logoUrl);
|
||||
self::assertTrue($branding->logoUrl->equals($logoUrl));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function changerLogoRecordsEvent(): void
|
||||
{
|
||||
$branding = $this->creerBranding();
|
||||
$logoUrl = new LogoUrl('https://s3.example.com/logos/school.png');
|
||||
$now = new DateTimeImmutable('2026-02-20 10:00:00');
|
||||
|
||||
$branding->changerLogo($logoUrl, $now);
|
||||
|
||||
$events = $branding->pullDomainEvents();
|
||||
self::assertCount(1, $events);
|
||||
self::assertInstanceOf(BrandingModifie::class, $events[0]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function changerLogoDoesNotRecordEventWhenSame(): void
|
||||
{
|
||||
$branding = $this->creerBranding();
|
||||
$logoUrl = new LogoUrl('https://s3.example.com/logos/school.png');
|
||||
$now = new DateTimeImmutable('2026-02-20 10:00:00');
|
||||
|
||||
$branding->changerLogo($logoUrl, $now);
|
||||
$branding->pullDomainEvents();
|
||||
|
||||
$branding->changerLogo($logoUrl, new DateTimeImmutable('2026-02-20 11:00:00'));
|
||||
|
||||
self::assertCount(0, $branding->pullDomainEvents());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function supprimerLogoRemovesLogo(): void
|
||||
{
|
||||
$branding = $this->creerBranding();
|
||||
$logoUrl = new LogoUrl('https://s3.example.com/logos/school.png');
|
||||
$now = new DateTimeImmutable('2026-02-20 10:00:00');
|
||||
|
||||
$branding->changerLogo($logoUrl, $now);
|
||||
$branding->pullDomainEvents();
|
||||
|
||||
$branding->supprimerLogo(new DateTimeImmutable('2026-02-20 11:00:00'));
|
||||
|
||||
self::assertNull($branding->logoUrl);
|
||||
$events = $branding->pullDomainEvents();
|
||||
self::assertCount(1, $events);
|
||||
self::assertInstanceOf(BrandingModifie::class, $events[0]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function supprimerLogoDoesNotRecordEventWhenAlreadyNull(): void
|
||||
{
|
||||
$branding = $this->creerBranding();
|
||||
|
||||
$branding->supprimerLogo(new DateTimeImmutable('2026-02-20 10:00:00'));
|
||||
|
||||
self::assertCount(0, $branding->pullDomainEvents());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function reinitialiserResetsAllToDefaults(): void
|
||||
{
|
||||
$branding = $this->creerBranding();
|
||||
$now = new DateTimeImmutable('2026-02-20 10:00:00');
|
||||
|
||||
$branding->changerLogo(new LogoUrl('https://s3.example.com/logo.png'), $now);
|
||||
$branding->modifierCouleurs(
|
||||
primaryColor: new BrandColor('#3B82F6'),
|
||||
secondaryColor: new BrandColor('#1E40AF'),
|
||||
accentColor: new BrandColor('#60A5FA'),
|
||||
at: $now,
|
||||
);
|
||||
|
||||
$branding->pullDomainEvents();
|
||||
|
||||
$branding->reinitialiser(new DateTimeImmutable('2026-02-20 12:00:00'));
|
||||
|
||||
self::assertNull($branding->logoUrl);
|
||||
self::assertNull($branding->primaryColor);
|
||||
self::assertNull($branding->secondaryColor);
|
||||
self::assertNull($branding->accentColor);
|
||||
|
||||
$events = $branding->pullDomainEvents();
|
||||
self::assertCount(1, $events);
|
||||
self::assertInstanceOf(BrandingModifie::class, $events[0]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function reinitialiserDoesNotRecordEventWhenAlreadyDefault(): void
|
||||
{
|
||||
$branding = $this->creerBranding();
|
||||
|
||||
$branding->reinitialiser(new DateTimeImmutable('2026-02-20 10:00:00'));
|
||||
|
||||
self::assertCount(0, $branding->pullDomainEvents());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function estPersonnaliseReturnsFalseByDefault(): void
|
||||
{
|
||||
$branding = $this->creerBranding();
|
||||
|
||||
self::assertFalse($branding->estPersonnalise());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function estPersonnaliseReturnsTrueWhenColorSet(): void
|
||||
{
|
||||
$branding = $this->creerBranding();
|
||||
$branding->modifierCouleurs(
|
||||
primaryColor: new BrandColor('#3B82F6'),
|
||||
secondaryColor: null,
|
||||
accentColor: null,
|
||||
at: new DateTimeImmutable(),
|
||||
);
|
||||
|
||||
self::assertTrue($branding->estPersonnalise());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function estPersonnaliseReturnsTrueWhenLogoSet(): void
|
||||
{
|
||||
$branding = $this->creerBranding();
|
||||
$branding->changerLogo(
|
||||
new LogoUrl('https://s3.example.com/logo.png'),
|
||||
new DateTimeImmutable(),
|
||||
);
|
||||
|
||||
self::assertTrue($branding->estPersonnalise());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function reconstituteFromStorage(): void
|
||||
{
|
||||
$schoolId = SchoolId::fromString(self::SCHOOL_ID);
|
||||
$tenantId = TenantId::fromString(self::TENANT_ID);
|
||||
$logoUrl = new LogoUrl('https://s3.example.com/logo.png');
|
||||
$primary = new BrandColor('#3B82F6');
|
||||
$secondary = new BrandColor('#1E40AF');
|
||||
$accent = new BrandColor('#60A5FA');
|
||||
$createdAt = new DateTimeImmutable('2026-02-01 10:00:00');
|
||||
$updatedAt = new DateTimeImmutable('2026-02-15 14:30:00');
|
||||
$logoUpdatedAt = new DateTimeImmutable('2026-02-10 09:00:00');
|
||||
|
||||
$branding = SchoolBranding::reconstitute(
|
||||
schoolId: $schoolId,
|
||||
tenantId: $tenantId,
|
||||
logoUrl: $logoUrl,
|
||||
logoUpdatedAt: $logoUpdatedAt,
|
||||
primaryColor: $primary,
|
||||
secondaryColor: $secondary,
|
||||
accentColor: $accent,
|
||||
createdAt: $createdAt,
|
||||
updatedAt: $updatedAt,
|
||||
);
|
||||
|
||||
self::assertTrue($branding->schoolId->equals($schoolId));
|
||||
self::assertTrue($branding->tenantId->equals($tenantId));
|
||||
self::assertNotNull($branding->logoUrl);
|
||||
self::assertTrue($branding->logoUrl->equals($logoUrl));
|
||||
self::assertNotNull($branding->primaryColor);
|
||||
self::assertTrue($branding->primaryColor->equals($primary));
|
||||
self::assertNotNull($branding->secondaryColor);
|
||||
self::assertTrue($branding->secondaryColor->equals($secondary));
|
||||
self::assertNotNull($branding->accentColor);
|
||||
self::assertTrue($branding->accentColor->equals($accent));
|
||||
self::assertSame($createdAt, $branding->createdAt);
|
||||
self::assertSame($updatedAt, $branding->updatedAt);
|
||||
|
||||
self::assertCount(0, $branding->pullDomainEvents());
|
||||
}
|
||||
|
||||
private function creerBranding(): SchoolBranding
|
||||
{
|
||||
return SchoolBranding::creer(
|
||||
schoolId: SchoolId::fromString(self::SCHOOL_ID),
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
createdAt: new DateTimeImmutable('2026-02-01 10:00:00'),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user