diff --git a/src/Api/Controller/GetModListByNameAction.php b/src/Api/Controller/GetModListByNameAction.php index bd98cd12..63d7d8d5 100644 --- a/src/Api/Controller/GetModListByNameAction.php +++ b/src/Api/Controller/GetModListByNameAction.php @@ -17,9 +17,7 @@ public function __construct( public function __invoke(string $name): ?ModList { - $modList = $this->modListRepository->findOneBy([ - 'name' => $name, - ]); + $modList = $this->modListRepository->findOneByName($name); if (!$modList) { throw new NotFoundHttpException('Not Found'); diff --git a/src/Controller/ModListPublic/DownloadAction.php b/src/Controller/ModListPublic/DownloadAction.php index e476648c..dc50c1c9 100644 --- a/src/Controller/ModListPublic/DownloadAction.php +++ b/src/Controller/ModListPublic/DownloadAction.php @@ -13,6 +13,8 @@ use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; +use function Symfony\Component\Clock\now; + class DownloadAction extends AbstractController { public function __construct( @@ -24,7 +26,7 @@ public function __construct( #[IsGranted(PermissionsEnum::MOD_LIST_DOWNLOAD->value, 'modList')] public function __invoke(ModList $modList, string $optionalModsJson = null): Response { - $name = sprintf('ArmaForces %s %s', $modList->getName(), (new \DateTimeImmutable())->format('Y_m_d H_i')); + $name = sprintf('ArmaForces %s %s', $modList->getName(), now()->format('Y_m_d H_i')); $mods = $this->modRepository->findIncludedSteamWorkshopMods($modList); $optionalMods = json_decode($optionalModsJson ?? '', true) ?: []; diff --git a/src/Controller/ModListPublic/SelectAction.php b/src/Controller/ModListPublic/SelectAction.php index e627cdef..346a0393 100644 --- a/src/Controller/ModListPublic/SelectAction.php +++ b/src/Controller/ModListPublic/SelectAction.php @@ -37,7 +37,7 @@ public function __invoke(): Response $nextMissionModList = null; if ($nextMission) { - $nextMissionModList = $this->modListRepository->findOneBy(['name' => $nextMission->getModlist()]); + $nextMissionModList = $this->modListRepository->findOneByName($nextMission->getModlist()); } return $this->render('mod_list_public/select.html.twig', [ diff --git a/src/Repository/ModList/ModListRepository.php b/src/Repository/ModList/ModListRepository.php index 5e5b6179..314e98be 100644 --- a/src/Repository/ModList/ModListRepository.php +++ b/src/Repository/ModList/ModListRepository.php @@ -20,4 +20,9 @@ public function __construct(ManagerRegistry $registry) { parent::__construct($registry, ModList::class); } + + public function findOneByName(string $name): ?ModList + { + return $this->findOneBy(['name' => $name]); + } } diff --git a/tests/_support/FunctionalTester.php b/tests/_support/FunctionalTester.php index 0e049b03..08599391 100644 --- a/tests/_support/FunctionalTester.php +++ b/tests/_support/FunctionalTester.php @@ -4,8 +4,10 @@ namespace App\Tests; -use App\Entity\User\User; use App\Test\Traits\TimeTrait; +use App\Tests\Traits\DataTableAssertsTrait; +use App\Tests\Traits\ResponseAssertTrait; +use App\Tests\Traits\SecurityAssertsTrait; use Codeception\Actor; use Codeception\Lib\Friend; @@ -27,85 +29,8 @@ class FunctionalTester extends Actor { use _generated\FunctionalTesterActions; + use DataTableAssertsTrait; + use ResponseAssertTrait; + use SecurityAssertsTrait; use TimeTrait; - - public function amDiscordAuthenticatedAs(string $id, callable $preAuthCallback = null): User - { - /** @var User $user */ - $user = $this->grabEntityFromRepository(User::class, ['id' => $id]); - if ($preAuthCallback) { - $preAuthCallback($user); - - // Refresh user entity to avoid permission issues in subsequent requests - $this->haveInRepository($user); - } - $this->amLoggedInAs($user); - - return $user; - } - - public function seeResponseRedirectsTo(string $url): void - { - $this->seeResponseCodeIsRedirection(); - $this->seeHttpHeader('Location', $url); - } - - public function seeResponseRedirectsToLogInAction(): void - { - $this->seeResponseRedirectsTo('/security/connect/discord'); - } - - public function seeResponseRedirectsToDiscordOauth(): void - { - $this->seeResponseCodeIsRedirection(); - $redirect = $this->grabHttpHeader('Location'); - $this->assertTrue(str_starts_with($redirect, 'https://discord.com/oauth2/authorize')); - } - - public function seeActionButton(string $tooltip, string $url = null): void - { - $selector = sprintf('a i[title="%s"]', $tooltip); - if ($url) { - $selector = sprintf('a[href="%s"] i[title="%s"]', $url, $tooltip); - } - $this->seeElement($selector); - } - - public function dontSeeActionButton(string $tooltip, string $url = null): void - { - $selector = sprintf('a i[title="%s"]', $tooltip); - if ($url) { - $selector = sprintf('a[href="%s"] i[title="%s"]', $url, $tooltip); - } - $this->dontSeeElement($selector); - } - - public function checkTableRowCheckbox(string $value): void - { - $this->checkOption(sprintf('[value="%s"]', $value)); - } - - public function uncheckTableRowCheckbox(string $value): void - { - $this->uncheckOption(sprintf('[value="%s"]', $value)); - } - - public function seeTableRowCheckboxesAreChecked(array $values = []): void - { - $valueSelector = array_map(fn (string $value) => sprintf('[value="%s"]', $value), $values); - $checkboxSelector = implode(', ', $valueSelector); - - $this->seeCheckboxIsChecked($checkboxSelector); - } - - public function seeTableRowCheckboxesAreUnchecked(string $idPrefix, array $valuesToExclude = []): void - { - $checkboxSelector = sprintf('[id^="%s"]', $idPrefix); - if ($valuesToExclude) { - $valueSelector = array_map(fn (string $value) => sprintf(':not([value^="%s"])', $value), $valuesToExclude); - $checkboxSelector .= implode('', $valueSelector); - } - - $this->dontSeeCheckboxIsChecked($checkboxSelector); - } } diff --git a/tests/_support/Helper/Functional.php b/tests/_support/Helper/Functional.php deleted file mode 100644 index eb527c98..00000000 --- a/tests/_support/Helper/Functional.php +++ /dev/null @@ -1,12 +0,0 @@ -seeElement($selector); + } + + public function dontSeeActionButton(string $tooltip, string $url = null): void + { + $selector = sprintf('a i[title="%s"]', $tooltip); + if ($url) { + $selector = sprintf('a[href="%s"] i[title="%s"]', $url, $tooltip); + } + $this->dontSeeElement($selector); + } + + public function checkTableRowCheckbox(string $value): void + { + $this->checkOption(sprintf('[value="%s"]', $value)); + } + + public function uncheckTableRowCheckbox(string $value): void + { + $this->uncheckOption(sprintf('[value="%s"]', $value)); + } + + public function seeTableRowCheckboxesAreChecked(array $values = []): void + { + $valueSelector = array_map(fn (string $value) => sprintf('[value="%s"]', $value), $values); + $checkboxSelector = implode(', ', $valueSelector); + + $this->seeCheckboxIsChecked($checkboxSelector); + } + + public function seeTableRowCheckboxesAreUnchecked(string $idPrefix, array $valuesToExclude = []): void + { + $checkboxSelector = sprintf('[id^="%s"]', $idPrefix); + if ($valuesToExclude) { + $valueSelector = array_map(fn (string $value) => sprintf(':not([value^="%s"])', $value), $valuesToExclude); + $checkboxSelector .= implode('', $valueSelector); + } + + $this->dontSeeCheckboxIsChecked($checkboxSelector); + } +} diff --git a/tests/_support/Traits/ResponseAssertTrait.php b/tests/_support/Traits/ResponseAssertTrait.php new file mode 100644 index 00000000..c52748d3 --- /dev/null +++ b/tests/_support/Traits/ResponseAssertTrait.php @@ -0,0 +1,73 @@ +seeResponseCodeIsRedirection(); + $this->seeHttpHeader('Location', $url); + } + + public function seeResponseRedirectsToLogInAction(): void + { + $this->seeResponseRedirectsTo('/security/connect/discord'); + } + + public function seeResponseRedirectsToDiscordOauth(): void + { + $this->seeResponseCodeIsRedirection(); + $redirect = $this->grabHttpHeader('Location'); + $this->assertTrue(str_starts_with($redirect, 'https://discord.com/oauth2/authorize')); + } + + public function seeResponseContainsModListPresetWithMods( + string $fileName, + array $expectedDlcs, + array $expectedMods, + ): void { + $extractSteamWorkshopItems = function (Crawler $crawler, string $containerName) { + $containerSelector = sprintf('[data-type="%s"]', $containerName); + $containerCrawler = $crawler->filter($containerSelector); + + return array_map(static function (\DOMNode $steamWorkshopItemNode) { + $steamWorkshopItemNodeCrawler = (new Crawler($steamWorkshopItemNode)); + + return [ + 'name' => $steamWorkshopItemNodeCrawler->filter('[data-type="DisplayName"]')->html(), + 'url' => $steamWorkshopItemNodeCrawler->filter('[data-type="Link"]')->attr('href'), + ]; + }, iterator_to_array($containerCrawler->getIterator())); + }; + + $this->seeHttpHeader('Content-Disposition', sprintf('attachment; filename="%s"', $fileName)); + + $crawler = new Crawler($this->grabResponse()); + $includedDlcs = $extractSteamWorkshopItems($crawler, 'DlcContainer'); + $includedMods = $extractSteamWorkshopItems($crawler, 'ModContainer'); + + $expectedDlcs = array_map(static function (Dlc $dlc) { + return [ + 'name' => $dlc->getName(), + 'url' => "https://store.steampowered.com/app/{$dlc->getAppId()}", + ]; + }, $expectedDlcs); + + $expectedMods = array_map(static function (SteamWorkshopMod $steamWorkshopMod) { + return [ + 'name' => $steamWorkshopMod->getName(), + 'url' => "https://steamcommunity.com/sharedfiles/filedetails/?id={$steamWorkshopMod->getItemId()}", + ]; + }, $expectedMods); + + $this->assertSame($expectedDlcs, $includedDlcs); + $this->assertSame($expectedMods, $includedMods); + } +} diff --git a/tests/_support/Traits/SecurityAssertsTrait.php b/tests/_support/Traits/SecurityAssertsTrait.php new file mode 100644 index 00000000..bb29d326 --- /dev/null +++ b/tests/_support/Traits/SecurityAssertsTrait.php @@ -0,0 +1,40 @@ +grabEntityFromRepository(User::class, ['id' => $id]); + if ($preAuthCallback) { + $preAuthCallback($user); + + // Refresh user entity to avoid permission issues in subsequent requests + $this->haveInRepository($user); + } + $this->amLoggedInAs($user); + + return $user; + } + + public function amApiKeyAuthenticatedAs(string $id, callable $preAuthCallback = null): User + { + /** @var User $user */ + $user = $this->grabEntityFromRepository(User::class, ['id' => $id]); + if ($preAuthCallback) { + $preAuthCallback($user); + + // Refresh user entity to avoid permission issues in subsequent requests + $this->haveInRepository($user); + } + $this->amLoggedInAs($user); + + return $user; + } +} diff --git a/tests/functional.suite.yml b/tests/functional.suite.yml index 519a47a0..743a6895 100644 --- a/tests/functional.suite.yml +++ b/tests/functional.suite.yml @@ -14,4 +14,3 @@ modules: url: / depends: Symfony - Asserts - - \App\Tests\Helper\Functional diff --git a/tests/functional/Web/ModListPublic/CustomizeModListCest.php b/tests/functional/Web/ModListPublic/CustomizeModListCest.php new file mode 100644 index 00000000..8d361d80 --- /dev/null +++ b/tests/functional/Web/ModListPublic/CustomizeModListCest.php @@ -0,0 +1,9 @@ +stopFollowingRedirects(); + $I->freezeTime('2020-01-01T00:00:00+00:00'); + } + + public function downloadModListAsUnauthenticatedUser(FunctionalTester $I): void + { + $optionalMods = [ + AceInteractionMenuExpansionModFixture::ID, + 'invalid', + ]; + $I->amOnPage(sprintf('/mod-list/%s/download/%s', DefaultModListFixture::NAME, json_encode($optionalMods))); + $I->seeResponseContainsModListPresetWithMods('ArmaForces Default 2020_01_01 00_00.html', [ + $I->grabEntityFromRepository(Dlc::class, ['id' => CslaIronCurtainDlcFixture::ID]), + $I->grabEntityFromRepository(Dlc::class, ['id' => GlobalMobilizationDlcFixture::ID]), + $I->grabEntityFromRepository(Dlc::class, ['id' => SogPrairieFireDlcFixture::ID]), + $I->grabEntityFromRepository(Dlc::class, ['id' => Spearhead1944DlcFixture::ID]), + ], [ + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => ArmaForcesAceMedicalModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => ArmaForcesMedicalModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => CupTerrainsCoreModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => CupTerrainsMapsModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => CupUnitsModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => CupVehiclesModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => CupWeaponsModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => RhsAfrfModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => RhsGrefModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => RhsUsafModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => LegacyArmaForcesModsModFixture::ID]), + ]); + } + + public function downloadModListAsAuthenticatedUser(FunctionalTester $I): void + { + $I->amDiscordAuthenticatedAs(User1Fixture::ID); + + $optionalMods = [ + AceInteractionMenuExpansionModFixture::ID, + 'invalid', + ]; + $I->amOnPage(sprintf('/mod-list/%s/download/%s', DefaultModListFixture::NAME, json_encode($optionalMods))); + $I->seeResponseContainsModListPresetWithMods('ArmaForces Default 2020_01_01 00_00.html', [ + $I->grabEntityFromRepository(Dlc::class, ['id' => CslaIronCurtainDlcFixture::ID]), + $I->grabEntityFromRepository(Dlc::class, ['id' => GlobalMobilizationDlcFixture::ID]), + $I->grabEntityFromRepository(Dlc::class, ['id' => SogPrairieFireDlcFixture::ID]), + $I->grabEntityFromRepository(Dlc::class, ['id' => Spearhead1944DlcFixture::ID]), + ], [ + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => ArmaForcesAceMedicalModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => ArmaForcesMedicalModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => CupTerrainsCoreModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => CupTerrainsMapsModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => CupUnitsModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => CupVehiclesModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => CupWeaponsModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => RhsAfrfModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => RhsGrefModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => RhsUsafModFixture::ID]), + $I->grabEntityFromRepository(SteamWorkshopMod::class, ['id' => LegacyArmaForcesModsModFixture::ID]), + ]); + } +} diff --git a/tests/functional/Web/ModListPublic/SelectModListCest.php b/tests/functional/Web/ModListPublic/SelectModListCest.php new file mode 100644 index 00000000..20c0d73f --- /dev/null +++ b/tests/functional/Web/ModListPublic/SelectModListCest.php @@ -0,0 +1,9 @@ +