import { expect, test, type Page, type Route } from '@playwright/test'; import { gameBasePath, gameProfile, gameTrpcRoute } from './gameTestPaths.js'; const response = (data: unknown) => ({ result: { data } }); const operationNames = (route: Route): string[] => { const url = new URL(route.request().url()); return decodeURIComponent(url.pathname.slice(url.pathname.lastIndexOf('/trpc/') + 6)).split(','); }; const publicResponse = (operation: string): unknown => { if (operation === 'public.getMapLayout') { return response({ mapName: 'che', cityList: [] }); } if (operation === 'public.getCachedMap') { return response({ year: 180, month: 1, cityList: [], nationList: [], history: [] }); } if (operation === 'public.getWorldTrend') { return response({ year: 180, month: 1, turnTerm: 5 }); } if (operation === 'public.getNationList' || operation === 'public.getGeneralList') { return response([]); } throw new Error(`Unhandled public tRPC operation: ${operation}`); }; const seedGameStorage = async (page: Page, gameToken: string): Promise => { await page.addInitScript( ({ token, profile }) => { window.localStorage.setItem('sammo-game-token', token); window.localStorage.setItem('sammo-game-profile', profile); }, { token: gameToken, profile: gameProfile } ); }; test('consumes a same-tab gateway transfer from a clean profile URL', async ({ page }) => { await page.addInitScript((profile) => { window.sessionStorage.setItem( 'sammo-pending-game-session', JSON.stringify({ profile, gatewayToken: 'gateway-transfer-token' }) ); }, gameProfile); const exchangedInputs: unknown[] = []; await page.route(gameTrpcRoute, async (route) => { const operations = operationNames(route); const requestBody = route.request().postDataJSON() as { json?: unknown; 0?: { json?: unknown; gatewayToken?: string } } | undefined; const results = operations.map((operation) => { if (operation === 'auth.exchangeGatewayToken') { exchangedInputs.push(requestBody?.json ?? requestBody?.[0]?.json ?? requestBody?.[0]); return response({ accessToken: 'ga_transferred', profile: gameProfile, expiresAt: '2026-08-11T01:00:00Z', }); } if (operation === 'auth.status') { expect(route.request().headers().authorization).toBe('Bearer ga_transferred'); return response({ userId: 'transfer-user' }); } if (operation === 'lobby.info') { return response({ myGeneral: null }); } if (operation === 'join.getConfig') { return response({}); } return publicResponse(operation); }); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(results), }); }); await page.goto('select-general'); await expect(page).toHaveURL(new RegExp(`${gameBasePath}/select-general$`)); await expect(page.locator('.page-title')).toContainText('장 수 선 택'); expect(exchangedInputs).toEqual([{ gatewayToken: 'gateway-transfer-token' }]); expect(await page.evaluate(() => window.sessionStorage.getItem('sammo-pending-game-session'))).toBeNull(); expect(await page.evaluate(() => window.localStorage.getItem('sammo-game-profile'))).toBe(gameProfile); expect(await page.evaluate(() => window.localStorage.getItem('sammo-game-token'))).toBe('ga_transferred'); }); test('removes an invalid ga_ token and redirects an authenticated route to public', async ({ page }) => { await seedGameStorage(page, 'ga_invalid'); let gatewayRequests = 0; await page.route('http://127.0.0.1:15120/api/trpc/**', async (route) => { gatewayRequests += 1; await route.abort(); }); await page.route(gameTrpcRoute, async (route) => { const operations = operationNames(route); if (operations.includes('auth.status')) { expect(route.request().headers().authorization).toBe('Bearer ga_invalid'); await route.fulfill({ status: 401, contentType: 'application/json', body: JSON.stringify({ error: 'invalid token' }), }); return; } await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(operations.map(publicResponse)), }); }); await page.goto('select-general'); await expect(page).toHaveURL(new RegExp(`${gameBasePath}/public$`)); await expect(page.getByRole('heading', { name: '공개 동향' })).toBeVisible(); expect(await page.evaluate(() => window.localStorage.getItem('sammo-game-token'))).toBeNull(); expect(gatewayRequests).toBe(0); }); test('keeps a valid ga_ token when only lobby.info is unavailable', async ({ page }) => { await seedGameStorage(page, 'ga_valid'); page.on('dialog', (dialog) => dialog.accept()); let gatewayRequests = 0; let statusRequests = 0; let lobbyRequests = 0; await page.route('http://127.0.0.1:15120/api/trpc/**', async (route) => { gatewayRequests += 1; await route.abort(); }); await page.route(gameTrpcRoute, async (route) => { const operations = operationNames(route); if (operations.includes('auth.status')) { statusRequests += 1; expect(route.request().headers().authorization).toBe('Bearer ga_valid'); await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([response({ userId: 'valid-user' })]), }); return; } if (operations.includes('lobby.info')) { lobbyRequests += 1; await route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ error: 'lobby unavailable' }), }); return; } await route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ error: 'fixture page data unavailable' }), }); }); await page.goto('select-general'); await expect(page).toHaveURL(new RegExp(`${gameBasePath}/select-general$`)); await expect(page.locator('.page-title')).toContainText('장 수 선 택'); expect(await page.evaluate(() => window.localStorage.getItem('sammo-game-token'))).toBe('ga_valid'); expect(statusRequests).toBe(1); expect(lobbyRequests).toBe(1); expect(gatewayRequests).toBe(0); });