organization = new Organization('Test', 'test'); $french = new Locale('fr-FR', 'French', 'Français', TextDirection::Ltr, ['one', 'many', 'other']); $this->spanish = new Locale('es-ES', 'Spanish', 'Español', TextDirection::Ltr, ['one', 'many', 'other']); $this->german = new Locale('de-DE', 'German', 'Deutsch', TextDirection::Ltr, ['one', 'other']); $this->project = new Project($this->organization, 'Test', 'test', $french); foreach ([$french, $this->spanish, $this->german] as $locale) { new ProjectLocale($this->project, $locale); } } public function testDeveloperWritesNothingDespiteCoveringAllLocales(): void { // Le cas du bug : aucune langue assignée, donc coversAllLocales() est // vrai. C'est le RÔLE qui doit trancher, et en premier. $member = $this->member(ProjectRole::Developer); self::assertTrue($member->coversAllLocales()); self::assertSame([], $this->resolverFor($member)->codesFor($this->project)); self::assertSame([], $this->resolverFor($member)->localesFor($this->project)); } public function testViewerWritesNothing(): void { $member = $this->member(ProjectRole::Viewer); self::assertSame([], $this->resolverFor($member)->codesFor($this->project)); } public function testTranslatorIsLimitedToItsLocales(): void { $member = $this->member(ProjectRole::Translator); $member->addLocale($this->spanish); self::assertSame(['es-ES'], $this->resolverFor($member)->codesFor($this->project)); self::assertSame( ['es-ES'], array_map( static fn (Locale $l): string => $l->getCode(), $this->resolverFor($member)->localesFor($this->project), ), ); } public function testTranslatorWithoutLocalesCoversThemAll(): void { // null, pas [] : « toutes les langues ». La distinction entre les deux // porte tout le sens de la classe. $member = $this->member(ProjectRole::Translator); self::assertNull($this->resolverFor($member)->codesFor($this->project)); self::assertCount(2, $this->resolverFor($member)->localesFor($this->project)); } public function testSuperAdminCoversEverythingWithoutMembership(): void { $user = new User($this->organization, 'root@tranquilys.test', 'Root'); $user->setRoles([User::ROLE_SUPER_ADMIN]); $members = $this->createMock(ProjectMemberRepository::class); // Aucune requête d'appartenance ne doit partir : le super-admin est // tranché avant, et interroger la base pour rien coûterait une requête // sur chaque écran. $members->expects(self::never())->method('findMembership'); $security = $this->createStub(Security::class); $security->method('getUser')->willReturn($user); self::assertNull((new WritableLocaleResolver($security, $members))->codesFor($this->project)); } public function testAnonymousCallerIsNotRestricted(): void { // Le CLI présente une clé API, pas un User. Le cloisonnement par langue // ne le concerne pas : ses permissions sont portées par les scopes de // la clé et tranchées par les Voters. $security = $this->createStub(Security::class); $security->method('getUser')->willReturn(null); $resolver = new WritableLocaleResolver($security, $this->createStub(ProjectMemberRepository::class)); self::assertNull($resolver->codesFor($this->project)); } private function member(ProjectRole $role): ProjectMember { $user = new User($this->organization, 'someone@tranquilys.test', 'Quelqu\'un'); return new ProjectMember($this->project, $user, $role); } private function resolverFor(ProjectMember $member): WritableLocaleResolver { $members = $this->createStub(ProjectMemberRepository::class); $members->method('findMembership')->willReturn($member); $security = $this->createStub(Security::class); $security->method('getUser')->willReturn($member->getUser()); return new WritableLocaleResolver($security, $members); } }