Livre l'infrastructure permettant aux modules metier de declarer leurs entites comme "scopees par site" via SiteAwareInterface. Strictement opt-in : aucune entite metier touchee, aucune migration sur tables existantes. Composants : - SiteAwareInterface (Shared/Domain/Contract) : getSite/setSite - CurrentSiteProvider + interface (Module/Sites/Application) : resolve ?Site selon 3 conditions (module actif, user authentifie, currentSite). Interface extraite pour mockabilite en tests (implementation reste final). - SiteScopedQueryExtension : QueryCollection + QueryItem API Platform, ajoute WHERE site = :currentSite si resource SiteAware + provider non-null + pas sites.bypass_scope. - SiteAwareInjectionProcessor : decorator de api_platform.doctrine.orm. state.persist_processor (#[AsDecorator]). Injecte currentSite sur entites SiteAware sans site ; throw 400 si provider null. - Permission sites.bypass_scope declaree dans SitesModule::permissions(). Tests : - FakeSiteAwareEntity dans tests/Fixtures/ + mapping when@test dans doctrine.yaml. Table creee a la volee via SchemaTool dans setUp. schema:update --force ajoute dans test-db-setup pour que fixtures:load ne crashe pas au purger. - 17 tests dedies au ticket 4 (CurrentSiteProvider unitaire, Injection Processor unitaire, Extension integration avec 7 cas couvrant filtrage collection + item, bypass, no-op, resource non SiteAware). - SitesModuleTest : verifie le set de 3 permissions + que le decorator est bien enregistre sur le persist processor. Documentation docs/modules/site-aware.md : guide developpeur 8 sections (quand/ne pas adopter, comment, migration, mode degrade, anti-patterns, exemple d'adoption Supplier, cascade delete). Upgrade @malio/layer-ui 1.4.0 → 1.4.2 (bug 1.4.0 : tailwind.config.ts oublie dans les files publies npm → classe rounded-malio manquante sur les DataTables). Simplification tailwind.config.ts Coltura : retrait des colors/fontFamily/borderRadius dupliques, seule la specifique projet (primary, secondary, tertiary, m.secondary, m.tertiary) est conservee. Tests : 201/201 avec et sans SitesModule actif (2 skipped en disabled). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
111 lines
3.7 KiB
PHP
111 lines
3.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Module\Sites\Application\Service;
|
|
|
|
use App\Module\Core\Domain\Entity\User;
|
|
use App\Module\Sites\Application\Service\CurrentSiteProvider;
|
|
use App\Module\Sites\Domain\Entity\Site;
|
|
use PHPUnit\Framework\TestCase;
|
|
use ReflectionClass;
|
|
use Symfony\Bundle\SecurityBundle\Security;
|
|
use Symfony\Component\Security\Core\User\InMemoryUser;
|
|
|
|
/**
|
|
* Tests unitaires du CurrentSiteProvider.
|
|
*
|
|
* Le provider lit `config/modules.php` au boot via un `require`. Pour les
|
|
* tests, on force la valeur du flag `sitesActive` via reflection plutot
|
|
* que de mock le filesystem : le comportement du constructeur
|
|
* (file_exists + require) est assez trivial pour etre couvert par un
|
|
* test d'integration si besoin ; ici on se concentre sur la logique de
|
|
* `get()`.
|
|
*
|
|
* @internal
|
|
*/
|
|
final class CurrentSiteProviderTest extends TestCase
|
|
{
|
|
public function testReturnsNullIfSitesModuleInactive(): void
|
|
{
|
|
$user = new User();
|
|
$user->setCurrentSite(new Site('Site', 'Rue', null, '12345', 'Ville', '#000000'));
|
|
|
|
$security = $this->createStub(Security::class);
|
|
$security->method('getUser')->willReturn($user);
|
|
|
|
$provider = $this->makeProvider($security, sitesActive: false);
|
|
|
|
self::assertNull($provider->get());
|
|
}
|
|
|
|
public function testReturnsNullIfNoUser(): void
|
|
{
|
|
$security = $this->createStub(Security::class);
|
|
$security->method('getUser')->willReturn(null);
|
|
|
|
$provider = $this->makeProvider($security, sitesActive: true);
|
|
|
|
self::assertNull($provider->get());
|
|
}
|
|
|
|
public function testReturnsNullIfUserIsNotAppUser(): void
|
|
{
|
|
// Un InMemoryUser Symfony n'est pas une instance de App\User donc
|
|
// le provider ne peut pas lire son currentSite -> null defensif.
|
|
$security = $this->createStub(Security::class);
|
|
$security->method('getUser')->willReturn(new InMemoryUser('foo', 'bar'));
|
|
|
|
$provider = $this->makeProvider($security, sitesActive: true);
|
|
|
|
self::assertNull($provider->get());
|
|
}
|
|
|
|
public function testReturnsNullIfUserHasNoCurrentSite(): void
|
|
{
|
|
$user = new User();
|
|
// Pas d'appel a setCurrentSite, donc null par defaut.
|
|
|
|
$security = $this->createStub(Security::class);
|
|
$security->method('getUser')->willReturn($user);
|
|
|
|
$provider = $this->makeProvider($security, sitesActive: true);
|
|
|
|
self::assertNull($provider->get());
|
|
}
|
|
|
|
public function testReturnsSiteWhenAllConditionsMet(): void
|
|
{
|
|
$site = new Site('Chatellerault', 'Rue', null, '86100', 'Chatellerault', '#056CF2');
|
|
$user = new User();
|
|
$user->setCurrentSite($site);
|
|
|
|
$security = $this->createStub(Security::class);
|
|
$security->method('getUser')->willReturn($user);
|
|
|
|
$provider = $this->makeProvider($security, sitesActive: true);
|
|
|
|
self::assertSame($site, $provider->get());
|
|
}
|
|
|
|
/**
|
|
* Factory helper : construit un provider avec `$sitesActive` force a
|
|
* la valeur donnee, bypassant la lecture reelle de config/modules.php.
|
|
*/
|
|
private function makeProvider(Security $security, bool $sitesActive): CurrentSiteProvider
|
|
{
|
|
// Instance via reflection pour eviter l'appel reel au constructeur
|
|
// qui require config/modules.php (non deterministe en test unit).
|
|
$reflection = new ReflectionClass(CurrentSiteProvider::class);
|
|
$provider = $reflection->newInstanceWithoutConstructor();
|
|
|
|
$securityProp = $reflection->getProperty('security');
|
|
$securityProp->setValue($provider, $security);
|
|
|
|
$sitesActiveProp = $reflection->getProperty('sitesActive');
|
|
$sitesActiveProp->setValue($provider, $sitesActive);
|
|
|
|
return $provider;
|
|
}
|
|
}
|