diff --git a/app/Platform/Actions/BuildGameInterestedDevelopersDataAction.php b/app/Platform/Actions/BuildGameInterestedDevelopersDataAction.php new file mode 100644 index 0000000000..e3b2866e69 --- /dev/null +++ b/app/Platform/Actions/BuildGameInterestedDevelopersDataAction.php @@ -0,0 +1,35 @@ + + */ + public function execute(Game $game): Collection + { + $users = UserGameListEntry::whereType(UserGameListType::Develop) + ->where('GameID', $game->id) + ->with(['user' => function ($query) { + $query->orderBy('User'); + }]) + ->get() + ->filter(fn (UserGameListEntry $entry) => $entry->user + && ($entry->user->hasRole(Role::DEVELOPER) || $entry->user->hasRole(Role::DEVELOPER_JUNIOR)) + ) + ->values() + ->map(fn (UserGameListEntry $entry) => UserData::fromUser($entry->user)); + + return $users; + } +} diff --git a/app/Platform/Controllers/GameController.php b/app/Platform/Controllers/GameController.php index c744bb5905..b0d6701daa 100644 --- a/app/Platform/Controllers/GameController.php +++ b/app/Platform/Controllers/GameController.php @@ -10,8 +10,11 @@ use App\Models\Game; use App\Models\System; use App\Models\User; +use App\Platform\Actions\BuildGameInterestedDevelopersDataAction; use App\Platform\Actions\BuildGameListAction; use App\Platform\Actions\GetRandomGameAction; +use App\Platform\Data\DeveloperInterestPagePropsData; +use App\Platform\Data\GameData; use App\Platform\Data\GameListPagePropsData; use App\Platform\Data\SystemData; use App\Platform\Enums\GameListSortField; @@ -158,6 +161,18 @@ public function destroy(Game $game): void $this->authorize('delete', $game); } + public function devInterest(Game $game): InertiaResponse + { + $this->authorize('viewDeveloperInterest', $game); + + $props = new DeveloperInterestPagePropsData( + game: GameData::fromGame($game)->include('badgeUrl', 'system'), + developers: (new BuildGameInterestedDevelopersDataAction())->execute($game) + ); + + return Inertia::render('game/[game]/dev-interest', $props); + } + public function random(GameListRequest $request): RedirectResponse { $this->authorize('viewAny', Game::class); diff --git a/app/Platform/Data/DeveloperInterestPagePropsData.php b/app/Platform/Data/DeveloperInterestPagePropsData.php new file mode 100644 index 0000000000..a45631eb39 --- /dev/null +++ b/app/Platform/Data/DeveloperInterestPagePropsData.php @@ -0,0 +1,21 @@ + */ + public Collection $developers, + ) { + } +} diff --git a/app/Platform/RouteServiceProvider.php b/app/Platform/RouteServiceProvider.php index 34eccb98e4..ae204393e9 100755 --- a/app/Platform/RouteServiceProvider.php +++ b/app/Platform/RouteServiceProvider.php @@ -64,6 +64,7 @@ protected function mapWebRoutes(): void }); Route::middleware(['web', 'inertia'])->group(function () { + Route::get('game/{game}/dev-interest', [GameController::class, 'devInterest'])->name('game.dev-interest'); Route::get('game/{game}/hashes', [GameHashController::class, 'index'])->name('game.hashes.index'); Route::get('game/{game}/top-achievers', [GameTopAchieversController::class, 'index'])->name('game.top-achievers.index'); diff --git a/app/Platform/Services/GameDevInterestPageService.php b/app/Platform/Services/GameDevInterestPageService.php deleted file mode 100644 index 3ba186498f..0000000000 --- a/app/Platform/Services/GameDevInterestPageService.php +++ /dev/null @@ -1,27 +0,0 @@ -where('GameID', $game->id) - ->join('UserAccounts', 'UserAccounts.ID', '=', 'SetRequest.user_id') - ->orderBy('UserAccounts.User') - ->pluck('UserAccounts.User'); - - return [ - 'pageDescription' => "Developers interested in working on {$game->title}", - 'pageTitle' => "{$game->title} - Developer Interest", - 'users' => $listUsers, - ]; - } -} diff --git a/app/Policies/GamePolicy.php b/app/Policies/GamePolicy.php index be5655476f..609ff0c0f6 100644 --- a/app/Policies/GamePolicy.php +++ b/app/Policies/GamePolicy.php @@ -142,6 +142,29 @@ public function manageContributionCredit(User $user, Game $game): bool ]); } + public function viewDeveloperInterest(User $user, Game $game): bool + { + $hasActivePrimaryClaim = $user->loadMissing('achievementSetClaims') + ->achievementSetClaims() + ->whereGameId($game->id) + ->primaryClaim() + ->active() + ->exists(); + + // Devs and JrDevs can see the page, but they need to have an + // active primary claim first. Collaborators for the game + // cannot open the page. + if ($hasActivePrimaryClaim) { + return true; + } + + // Mods and admins can see everything. + return $user->hasAnyRole([ + Role::ADMINISTRATOR, + Role::MODERATOR, + ]); + } + private function canDeveloperJuniorUpdateGame(User $user, Game $game): bool { // If the user has a DEVELOPER_JUNIOR role, they need to have a claim diff --git a/lang/en_US.json b/lang/en_US.json index b6858338cd..7d4cd3b1be 100644 --- a/lang/en_US.json +++ b/lang/en_US.json @@ -512,6 +512,10 @@ "Moderation Comments - {{user}}": "Moderation Comments - {{user}}", "Moderation Comments": "Moderation Comments", "Columns": "Columns", + "Developer Interest - {{gameTitle}}": "Developer Interest - {{gameTitle}}", + "Developer Interest": "Developer Interest", + "The following users have added this game to their Want to Develop list:": "The following users have added this game to their Want to Develop list:", + "No users have added this game to their Want to Develop list.": "No users have added this game to their Want to Develop list.", "Developer Feed": "Developer Feed", "Unlocks Contributed": "Unlocks Contributed", "Points Contributed": "Points Contributed", diff --git a/resources/js/features/achievements/components/CreateAchievementTicketMainRoot/CreateAchievementTicketMainRoot.test.tsx b/resources/js/features/achievements/components/CreateAchievementTicketMainRoot/CreateAchievementTicketMainRoot.test.tsx index ab09760e53..64fc3ab10e 100644 --- a/resources/js/features/achievements/components/CreateAchievementTicketMainRoot/CreateAchievementTicketMainRoot.test.tsx +++ b/resources/js/features/achievements/components/CreateAchievementTicketMainRoot/CreateAchievementTicketMainRoot.test.tsx @@ -807,82 +807,86 @@ describe('Component: CreateAchievementTicketMainRoot', () => { }, ); - it('sends along data from the ?extra query param if that data is provided', async () => { - // ARRANGE - const postSpy = vi.spyOn(axios, 'post').mockResolvedValueOnce({ data: { ticketId: 123 } }); + it( + 'sends along data from the ?extra query param if that data is provided', + { timeout: 10_000 }, + async () => { + // ARRANGE + const postSpy = vi.spyOn(axios, 'post').mockResolvedValueOnce({ data: { ticketId: 123 } }); - const achievement = createAchievement(); - const gameHashes = [createGameHash({ name: 'Hash A' }), createGameHash({ name: 'Hash B' })]; - const emulators = [ - createEmulator({ name: 'Bizhawk' }), - createEmulator({ name: 'RALibRetro' }), - createEmulator({ name: 'RetroArch' }), - ]; + const achievement = createAchievement(); + const gameHashes = [createGameHash({ name: 'Hash A' }), createGameHash({ name: 'Hash B' })]; + const emulators = [ + createEmulator({ name: 'Bizhawk' }), + createEmulator({ name: 'RALibRetro' }), + createEmulator({ name: 'RetroArch' }), + ]; - render( - , - { - pageProps: { - achievement, - emulators, - gameHashes, - auth: { user: createAuthenticatedUser({ points: 500 }) }, - ziggy: createZiggyProps({ - query: { - // !!!!! - extra: - 'eyJ0cmlnZ2VyUmljaFByZXNlbmNlIjoi8J+Qukxpbmsg8J+Xuu+4j0RlYXRoIE1vdW50YWluIOKdpO+4jzMvMyDwn5GlMS80IPCfp78wLzQg8J+RuzAvNjAg8J+QnDAvMjQg8J+SgDUg8J+VmTEyOjAwIEFN8J+MmSJ9', - }, - }), + render( + , + { + pageProps: { + achievement, + emulators, + gameHashes, + auth: { user: createAuthenticatedUser({ points: 500 }) }, + ziggy: createZiggyProps({ + query: { + // !!!!! + extra: + 'eyJ0cmlnZ2VyUmljaFByZXNlbmNlIjoi8J+Qukxpbmsg8J+Xuu+4j0RlYXRoIE1vdW50YWluIOKdpO+4jzMvMyDwn5GlMS80IPCfp78wLzQg8J+RuzAvNjAg8J+QnDAvMjQg8J+SgDUg8J+VmTEyOjAwIEFN8J+MmSJ9', + }, + }), + }, }, - }, - ); + ); - // ACT - await userEvent.click(screen.getByRole('combobox', { name: /issue/i })); - await userEvent.click(screen.getByRole('option', { name: /did not trigger/i })); + // ACT + await userEvent.click(screen.getByRole('combobox', { name: /issue/i })); + await userEvent.click(screen.getByRole('option', { name: /did not trigger/i })); - await userEvent.click(screen.getByRole('combobox', { name: /emulator/i })); - await userEvent.click(screen.getByRole('option', { name: /retroarch/i })); + await userEvent.click(screen.getByRole('combobox', { name: /emulator/i })); + await userEvent.click(screen.getByRole('option', { name: /retroarch/i })); - await userEvent.type(screen.getByRole('textbox', { name: /emulator core/i }), 'gambatte'); + await userEvent.type(screen.getByRole('textbox', { name: /emulator core/i }), 'gambatte'); - await userEvent.click(screen.getByRole('radio', { name: /softcore/i })); - await userEvent.click(screen.getByText(/softcore/i)); + await userEvent.click(screen.getByRole('radio', { name: /softcore/i })); + await userEvent.click(screen.getByText(/softcore/i)); - await userEvent.click(screen.getByRole('combobox', { name: /supported game file/i })); - await userEvent.click(screen.getByRole('option', { name: /hash a/i })); + await userEvent.click(screen.getByRole('combobox', { name: /supported game file/i })); + await userEvent.click(screen.getByRole('option', { name: /hash a/i })); - await userEvent.type( - screen.getByRole('textbox', { name: /description/i }), - 'Something is very wrong with this achievement. I tried many things and it just wont unlock. Help.', - ); + await userEvent.type( + screen.getByRole('textbox', { name: /description/i }), + 'Something is very wrong with this achievement. I tried many things and it just wont unlock. Help.', + ); - await userEvent.click(screen.getByRole('button', { name: /submit/i })); + await userEvent.click(screen.getByRole('button', { name: /submit/i })); - // ASSERT - await waitFor( - () => { - expect(postSpy).toHaveBeenCalledOnce(); - }, - { timeout: 6000 }, - ); + // ASSERT + await waitFor( + () => { + expect(postSpy).toHaveBeenCalledOnce(); + }, + { timeout: 6000 }, + ); - expect(postSpy).toHaveBeenCalledWith(['api.ticket.store'], { - core: 'gambatte', - description: - 'Something is very wrong with this achievement. I tried many things and it just wont unlock. Help.', - emulator: 'RetroArch', - emulatorVersion: null, - extra: - 'eyJ0cmlnZ2VyUmljaFByZXNlbmNlIjoi8J+Qukxpbmsg8J+Xuu+4j0RlYXRoIE1vdW50YWluIOKdpO+4jzMvMyDwn5GlMS80IPCfp78wLzQg8J+RuzAvNjAg8J+QnDAvMjQg8J+SgDUg8J+VmTEyOjAwIEFN8J+MmSJ9', - gameHashId: gameHashes[0].id, - issue: 2, - mode: 'softcore', - ticketableId: achievement.id, - ticketableModel: 'achievement', - }); - }); + expect(postSpy).toHaveBeenCalledWith(['api.ticket.store'], { + core: 'gambatte', + description: + 'Something is very wrong with this achievement. I tried many things and it just wont unlock. Help.', + emulator: 'RetroArch', + emulatorVersion: null, + extra: + 'eyJ0cmlnZ2VyUmljaFByZXNlbmNlIjoi8J+Qukxpbmsg8J+Xuu+4j0RlYXRoIE1vdW50YWluIOKdpO+4jzMvMyDwn5GlMS80IPCfp78wLzQg8J+RuzAvNjAg8J+QnDAvMjQg8J+SgDUg8J+VmTEyOjAwIEFN8J+MmSJ9', + gameHashId: gameHashes[0].id, + issue: 2, + mode: 'softcore', + ticketableId: achievement.id, + ticketableModel: 'achievement', + }); + }, + ); it('given the user is using a non-English locale, shows a warning about their ticket description', () => { // ARRANGE diff --git a/resources/js/features/games/components/DevInterestMainRoot/DevInterestMainRoot.test.tsx b/resources/js/features/games/components/DevInterestMainRoot/DevInterestMainRoot.test.tsx new file mode 100644 index 0000000000..b7acfffb30 --- /dev/null +++ b/resources/js/features/games/components/DevInterestMainRoot/DevInterestMainRoot.test.tsx @@ -0,0 +1,91 @@ +import { render, screen } from '@/test'; +import { createGame, createSystem, createUser } from '@/test/factories'; + +import { DevInterestMainRoot } from './DevInterestMainRoot'; + +describe('Component: DevInterestMainRoot', () => { + it('renders without crashing', () => { + // ARRANGE + const { container } = render( + , + { + pageProps: { + developers: [], + game: createGame(), + }, + }, + ); + + // ASSERT + expect(container).toBeTruthy(); + }); + + it('displays game breadcrumbs', () => { + // ARRANGE + const system = createSystem({ name: 'Nintendo 64' }); + const game = createGame({ system }); + + render(, { + pageProps: { + game, + developers: [], + }, + }); + + // ASSERT + expect(screen.getByRole('listitem', { name: /all games/i })).toBeVisible(); + expect(screen.getByRole('listitem', { name: system.name })).toBeVisible(); + expect(screen.getByRole('listitem', { name: game.title })).toBeVisible(); + }); + + it('displays an accessible heading', () => { + // ARRANGE + const system = createSystem({ name: 'Nintendo 64' }); + const game = createGame({ system }); + + render(, { + pageProps: { + game, + developers: [], + }, + }); + + // ASSERT + expect(screen.getByRole('heading', { name: /developer interest/i })).toBeVisible(); + }); + + it('given there are no developers interested, displays an empty state message', () => { + // ARRANGE + render(, { + pageProps: { + developers: [], + game: createGame(), + }, + }); + + // ASSERT + expect(screen.getByText(/no users have added this game/i)); + expect(screen.queryByRole('table')).not.toBeInTheDocument(); + }); + + it('given there are developers interested, shows the list of developers', () => { + // ARRANGE + render(, { + pageProps: { + developers: [ + createUser({ displayName: 'Scott' }), + createUser({ displayName: 'Jamiras' }), + createUser({ displayName: 'luchaos' }), + ], + game: createGame(), + }, + }); + + // ASSERT + expect(screen.getByRole('table')).toBeVisible(); + + expect(screen.getByRole('link', { name: /scott/i })).toBeVisible(); + expect(screen.getByRole('link', { name: /jamiras/i })).toBeVisible(); + expect(screen.getByRole('link', { name: /luchaos/i })).toBeVisible(); + }); +}); diff --git a/resources/js/features/games/components/DevInterestMainRoot/DevInterestMainRoot.tsx b/resources/js/features/games/components/DevInterestMainRoot/DevInterestMainRoot.tsx new file mode 100644 index 0000000000..2f60513e6c --- /dev/null +++ b/resources/js/features/games/components/DevInterestMainRoot/DevInterestMainRoot.tsx @@ -0,0 +1,58 @@ +import type { FC } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { + BaseTable, + BaseTableBody, + BaseTableCell, + BaseTableHead, + BaseTableHeader, + BaseTableRow, +} from '@/common/components/+vendor/BaseTable'; +import { GameBreadcrumbs } from '@/common/components/GameBreadcrumbs'; +import { GameHeading } from '@/common/components/GameHeading'; +import { UserAvatar } from '@/common/components/UserAvatar'; +import { usePageProps } from '@/common/hooks/usePageProps'; + +export const DevInterestMainRoot: FC = () => { + const { developers, game } = usePageProps(); + + const { t } = useTranslation(); + + return ( +
+ + {t('Developer Interest')} + + {developers.length ? ( +
+

{t('The following users have added this game to their Want to Develop list:')}

+ + + + + {t('Dev')} + + + + + {developers.map((developer) => ( + + + + + + ))} + + +
+ ) : ( +

{t('No users have added this game to their Want to Develop list.')}

+ )} +
+ ); +}; diff --git a/resources/js/features/games/components/DevInterestMainRoot/index.ts b/resources/js/features/games/components/DevInterestMainRoot/index.ts new file mode 100644 index 0000000000..960713dcad --- /dev/null +++ b/resources/js/features/games/components/DevInterestMainRoot/index.ts @@ -0,0 +1 @@ +export * from './DevInterestMainRoot'; diff --git a/resources/js/pages/game/[game]/dev-interest.tsx b/resources/js/pages/game/[game]/dev-interest.tsx new file mode 100644 index 0000000000..d87e1e30f1 --- /dev/null +++ b/resources/js/pages/game/[game]/dev-interest.tsx @@ -0,0 +1,31 @@ +import { useTranslation } from 'react-i18next'; + +import { SEO } from '@/common/components/SEO'; +import { usePageProps } from '@/common/hooks/usePageProps'; +import { AppLayout } from '@/common/layouts/AppLayout'; +import type { AppPage } from '@/common/models'; +import { DevInterestMainRoot } from '@/features/games/components/DevInterestMainRoot'; + +const GameDevInterest: AppPage = () => { + const { game } = usePageProps(); + + const { t } = useTranslation(); + + return ( + <> + + + + + + + ); +}; + +GameDevInterest.layout = (page) => {page}; + +export default GameDevInterest; diff --git a/resources/js/types/generated.d.ts b/resources/js/types/generated.d.ts index ad7a245bd4..c6ce6159b2 100644 --- a/resources/js/types/generated.d.ts +++ b/resources/js/types/generated.d.ts @@ -346,6 +346,10 @@ declare namespace App.Platform.Data { emulatorCore: string | null; selectedMode: number | null; }; + export type DeveloperInterestPageProps = { + game: App.Platform.Data.Game; + developers: Array; + }; export type Emulator = { id: number; name: string; diff --git a/resources/views/pages/game/[game]/dev-interest.blade.php b/resources/views/pages/game/[game]/dev-interest.blade.php deleted file mode 100644 index 435e9f67a4..0000000000 --- a/resources/views/pages/game/[game]/dev-interest.blade.php +++ /dev/null @@ -1,73 +0,0 @@ -getAttribute('Permissions'); - if ($permissions < Permissions::JuniorDeveloper) { - abort(403); - } - - if ($permissions < Permissions::Moderator && !hasSetClaimed($user, $game->id, true)) { - abort(403); - } - // END TODO use a policy -- "can:viewDeveloperInterest,game"? - - return $view->with($pageService->buildViewData($game)); -}); - -?> - -@props([ - 'pageDescription' => '', - 'pageTitle' => '', - 'users' => [], -]) - - - - - - - - -

The following users have added this game to their Want to Develop list:

- - - @if (count($users) < 1) - - @else - @foreach ($users as $user) - - @endforeach - @endif -
None
{!! userAvatar($user) !!}
-
diff --git a/tests/Feature/Platform/Controllers/GameControllerTest.php b/tests/Feature/Platform/Controllers/GameControllerTest.php index a6a0707495..f4e15d794b 100644 --- a/tests/Feature/Platform/Controllers/GameControllerTest.php +++ b/tests/Feature/Platform/Controllers/GameControllerTest.php @@ -4,8 +4,14 @@ namespace Tests\Feature\Platform\Controllers; +use App\Community\Enums\ClaimStatus; +use App\Community\Enums\ClaimType; +use App\Models\AchievementSetClaim; use App\Models\Game; +use App\Models\Role; use App\Models\System; +use App\Models\User; +use Database\Seeders\RolesTableSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; use Inertia\Testing\AssertableInertia as Assert; use Tests\TestCase; @@ -42,4 +48,148 @@ public function testIndexReturnsCorrectInertiaResponse(): void ->where('paginatedGameListEntries.items.0.game.system.id', $gameOne->system->id) ); } + + public function testDevInterestDeniesAccessToRegularUsers(): void + { + // Arrange + $this->seed(RolesTableSeeder::class); + + $system = System::factory()->create(['name' => 'Nintendo 64', 'active' => true]); + $game = Game::factory()->create(['title' => 'StarCraft 64', 'ConsoleID' => $system->id]); + + /** @var User $user */ + $user = User::factory()->create(['websitePrefs' => 63, 'UnreadMessageCount' => 0]); + $this->actingAs($user); + + // Act + $response = $this->get(route('game.dev-interest', ['game' => $game])); + + // Assert + $response->assertForbidden(); + } + + public function testDevInterestDeniesAccessToJuniorDevelopers(): void + { + // Arrange + $this->seed(RolesTableSeeder::class); + + $system = System::factory()->create(['name' => 'Nintendo 64', 'active' => true]); + $game = Game::factory()->create(['title' => 'StarCraft 64', 'ConsoleID' => $system->id]); + + /** @var User $user */ + $user = User::factory()->create(['websitePrefs' => 63, 'UnreadMessageCount' => 0]); + $user->assignRole(Role::DEVELOPER_JUNIOR); + $this->actingAs($user); + + // Act + $response = $this->get(route('game.dev-interest', ['game' => $game])); + + // Assert + $response->assertForbidden(); + } + + public function testDevInterestDeniesAccessToFullDevelopers(): void + { + // Arrange + $this->seed(RolesTableSeeder::class); + + $system = System::factory()->create(['name' => 'Nintendo 64', 'active' => true]); + $game = Game::factory()->create(['title' => 'StarCraft 64', 'ConsoleID' => $system->id]); + + /** @var User $user */ + $user = User::factory()->create(['websitePrefs' => 63, 'UnreadMessageCount' => 0]); + $user->assignRole(Role::DEVELOPER); + $this->actingAs($user); + + // Act + $response = $this->get(route('game.dev-interest', ['game' => $game])); + + // Assert + $response->assertForbidden(); + } + + public function testDevInterestIsAuthorizedForFullDevelopersWithPrimaryActiveClaims(): void + { + // Arrange + $this->seed(RolesTableSeeder::class); + + $system = System::factory()->create(['name' => 'Nintendo 64', 'active' => true]); + $game = Game::factory()->create(['title' => 'StarCraft 64', 'ConsoleID' => $system->id]); + + /** @var User $user */ + $user = User::factory()->create(['websitePrefs' => 63, 'UnreadMessageCount' => 0]); + $user->assignRole(Role::DEVELOPER); + $this->actingAs($user); + + AchievementSetClaim::factory()->create([ + 'user_id' => $user->id, + 'game_id' => $game->id, + 'ClaimType' => ClaimType::Primary, + 'Status' => ClaimStatus::Active, + ]); + + // Act + $response = $this->get(route('game.dev-interest', ['game' => $game])); + + // Assert + $response->assertOk(); + } + + public function testDevInterestDeniesAccessForDevelopersWithCollaborationClaims(): void + { + // Arrange + $this->seed(RolesTableSeeder::class); + + $system = System::factory()->create(['name' => 'Nintendo 64', 'active' => true]); + $game = Game::factory()->create(['title' => 'StarCraft 64', 'ConsoleID' => $system->id]); + + /** @var User $user */ + $user = User::factory()->create(['websitePrefs' => 63, 'UnreadMessageCount' => 0]); + $user->assignRole(Role::DEVELOPER); + $this->actingAs($user); + + AchievementSetClaim::factory()->create([ + 'user_id' => $user->id, + 'game_id' => $game->id, + 'ClaimType' => ClaimType::Collaboration, + 'Status' => ClaimStatus::Active, + ]); + + // Act + $response = $this->get(route('game.dev-interest', ['game' => $game])); + + // Assert + $response->assertForbidden(); + } + + public function testDevInterestReturnsCorrectInertiaResponse(): void + { + // Arrange + $this->seed(RolesTableSeeder::class); + + $system = System::factory()->create(['name' => 'Nintendo 64', 'active' => true]); + $game = Game::factory()->create(['title' => 'StarCraft 64', 'ConsoleID' => $system->id]); + + /** @var User $user */ + $user = User::factory()->create(['websitePrefs' => 63, 'UnreadMessageCount' => 0]); + $user->assignRole(Role::DEVELOPER); + $this->actingAs($user); + + AchievementSetClaim::factory()->create([ + 'user_id' => $user->id, + 'game_id' => $game->id, + 'Status' => ClaimStatus::Active, + ]); + + // Act + $response = $this->get(route('game.dev-interest', ['game' => $game])); + + // Assert + $response->assertInertia(fn ($page) => $page + ->has('game') + ->where('game.id', $game->id) + ->has('developers') + ->etc() + ); + } }