diff --git a/frontend/src/routes/login/page.test.ts b/frontend/src/routes/login/page.test.ts index d230fca7..fa347c00 100644 --- a/frontend/src/routes/login/page.test.ts +++ b/frontend/src/routes/login/page.test.ts @@ -1,4 +1,4 @@ -import { it, expect, vi, beforeEach } from 'vitest'; +import { it, expect, vi, beforeEach, afterEach } from 'vitest'; import { render, screen, fireEvent, waitFor } from '@testing-library/svelte'; const { goto, pageState, session } = vi.hoisted(() => { @@ -53,6 +53,24 @@ beforeEach(() => { m(auth.getAuthStatus).mockResolvedValue({ initialized: true }); }); +// jsdom's `Location` can't be spied on in place (its setters trigger +// "not implemented" navigation errors), so swap the whole object for a +// stub around each test that needs to observe `window.location.replace`. +const originalLocation = window.location; +let replaceSpy: ReturnType; + +beforeEach(() => { + replaceSpy = vi.fn(); + Object.defineProperty(window, 'location', { + configurable: true, + value: { ...originalLocation, replace: replaceSpy } + }); +}); + +afterEach(() => { + Object.defineProperty(window, 'location', { configurable: true, value: originalLocation }); +}); + it('logs in and redirects', async () => { m(auth.login).mockResolvedValue({ user: { id: '1' } }); render(LoginPage); @@ -179,3 +197,48 @@ it('renders an SSO sign-in link when an OIDC provider is configured', async () = const sso = await screen.findByTestId('login-oidc-btn'); expect(sso.getAttribute('href')).toBe('https://idp.test/auth'); }); + +it('auto-redirects to the IdP when OIDC is the only login method', async () => { + m(auth.getOidcProviders).mockResolvedValue({ + enabled: true, + password_login_enabled: false, + authorize_endpoint: '/api/auth/oidc/authorize' + }); + render(LoginPage); + await waitFor(() => expect(replaceSpy).toHaveBeenCalledWith('/api/auth/oidc/authorize')); +}); + +it('does not auto-redirect when password login is also enabled', async () => { + m(auth.getOidcProviders).mockResolvedValue({ + enabled: true, + password_login_enabled: true, + authorize_endpoint: '/api/auth/oidc/authorize' + }); + render(LoginPage); + await screen.findByTestId('login-form'); + expect(replaceSpy).not.toHaveBeenCalled(); +}); + +it('does not auto-redirect after the IdP already returned an error (loop guard)', async () => { + pageState.url = new URL('http://localhost/login?error=access_denied'); + m(auth.getOidcProviders).mockResolvedValue({ + enabled: true, + password_login_enabled: false, + authorize_endpoint: '/api/auth/oidc/authorize' + }); + render(LoginPage); + await screen.findByTestId('login-form'); + expect(replaceSpy).not.toHaveBeenCalled(); +}); + +it('does not auto-redirect during first-run setup', async () => { + m(auth.getAuthStatus).mockResolvedValue({ initialized: false }); + m(auth.getOidcProviders).mockResolvedValue({ + enabled: true, + password_login_enabled: false, + authorize_endpoint: '/api/auth/oidc/authorize' + }); + render(LoginPage); + await screen.findByTestId('login-setup-form'); + expect(replaceSpy).not.toHaveBeenCalled(); +});