// @vitest-environment jsdom import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; import {cleanup, fireEvent, screen, waitFor} from '@testing-library/react'; import {toast} from 'sonner'; import SettingsPage from './SettingsPage.jsx'; import {fetchRoutes, json} from '../test/fetchRoutes.js'; import {renderWithProviders} from '../test/renderWithProviders.jsx'; vi.mock('react-i18next', async () => { const mock = await import('../test/i18n-mock.js'); const i18n = {language: 'de', changeLanguage: vi.fn()}; return {...mock, useTranslation: () => ({t: mock.useTranslation().t, i18n})}; }); vi.mock('sonner', () => ({toast: {success: vi.fn(), error: vi.fn(), info: vi.fn()}})); const STEAM_URL = 'https://steamcommunity.com/openid/login?openid.mode=checkid_setup'; const profile = (overrides = {}) => ({ id: 1, email: 'a@b.de', role: 'USER', displayName: 'Lobo', steamId: '76561198005679788', steamIdVerified: false, settings: {useDarkTheme: true, language: 'de', publicProfile: false}, ...overrides, }); const calls = (method) => fetch.mock.calls.filter(([, init]) => init?.method === method); const noBody = {ok: true, status: 204, json: async () => { throw new SyntaxError('no body'); }}; function renderPage(me, {redirectUrl = STEAM_URL, entry = '/settings'} = {}) { fetchRoutes({ 'GET /api/users/me': json(200, me), 'POST /api/users/steam-link': json(200, {redirectUrl}), 'DELETE /api/users/steam-link': noBody, 'PUT /api/users/settings': noBody, 'POST /api/auth/refresh': noBody, 'GET /api/version': json(404, {}), 'GET /api/follows': json(200, {following: [], followers: []}), 'GET /api/library/psn-account': json(200, {enabled: false, linked: false}), }); renderWithProviders(, {path: '/settings', entries: [entry]}); } describe('SettingsPage Steam account', () => { const assign = vi.fn(); const realLocation = window.location; beforeEach(() => { localStorage.setItem('tbp_session', '1'); Object.defineProperty(window, 'location', {configurable: true, value: {...realLocation, assign}}); }); afterEach(() => { cleanup(); localStorage.clear(); vi.clearAllMocks(); Object.defineProperty(window, 'location', {configurable: true, value: realLocation}); }); it('shows a typed-in ID as not verified and sends the user to Steam to verify it', async () => { renderPage(profile()); expect(await screen.findByText('SETTINGS.STEAM_LINK.NOT_VERIFIED')).toBeTruthy(); fireEvent.click(screen.getByRole('button', {name: /SETTINGS.STEAM_LINK.VERIFY/})); await waitFor(() => expect(assign).toHaveBeenCalledWith(STEAM_URL)); expect(calls('POST').map(([url]) => url).some((url) => url.endsWith('/api/users/steam-link'))).toBe(true); }); it('does not follow a link that is not Steam', async () => { renderPage(profile({steamId: null}), {redirectUrl: 'https://evil.example/openid/login?x'}); fireEvent.click(await screen.findByRole('button', {name: /SETTINGS.STEAM_LINK.LINK/})); await waitFor(() => expect(toast.error).toHaveBeenCalledWith('SETTINGS.STEAM_LINK.ERROR')); expect(assign).not.toHaveBeenCalled(); }); it('shows a verified ID with the option to link another account', async () => { renderPage(profile({steamIdVerified: true})); expect(await screen.findByText('SETTINGS.STEAM_LINK.VERIFIED')).toBeTruthy(); expect(screen.getByRole('button', {name: /SETTINGS.STEAM_LINK.CHANGE/})).toBeTruthy(); }); it('back from Steam: refreshes the session so Core gets the new Steam ID, and says so', async () => { renderPage(profile({steamIdVerified: true}), {entry: '/settings?steam=linked'}); await screen.findByText('SETTINGS.STEAM_LINK.VERIFIED'); expect(calls('POST').some(([url]) => url.endsWith('/api/auth/refresh'))).toBe(true); expect(toast.success).toHaveBeenCalledWith('SETTINGS.STEAM_LINK.LINKED'); }); it.each([['in-use', 'error', 'SETTINGS.STEAM_LINK.IN_USE'], ['failed', 'error', 'SETTINGS.STEAM_LINK.FAILED'], ['cancelled', 'info', 'SETTINGS.STEAM_LINK.CANCELLED']])('back from Steam with %s', async (outcome, kind, key) => { renderPage(profile(), {entry: `/settings?steam=${outcome}`}); await screen.findByText('SETTINGS.STEAM_LINK.NOT_VERIFIED'); expect(toast[kind]).toHaveBeenCalledWith(key); expect(calls('POST').some(([url]) => url.endsWith('/api/auth/refresh'))).toBe(false); }); it('unlinks the Steam account', async () => { renderPage(profile({steamIdVerified: true})); fireEvent.click(await screen.findByRole('button', {name: /SETTINGS.STEAM_LINK.UNLINK/})); expect(await screen.findByText('SETTINGS.STEAM_LINK.NONE')).toBeTruthy(); expect(calls('DELETE')[0][0]).toMatch(/\/api\/users\/steam-link$/); }); it('never sends a Steam ID with the settings', async () => { renderPage(profile()); fireEvent.click(await screen.findByRole('switch', {name: 'SETTINGS.DARK_THEME'})); await waitFor(() => expect(calls('PUT')).toHaveLength(1)); expect(JSON.parse(calls('PUT')[0][1].body)).not.toHaveProperty('steamId'); }); });