import { firstValueFrom, of } from 'rxjs'; import { describe, expect, it } from 'vitest'; import { UsersActions } from '../../../../store/users/users.actions'; import { buildLoginReturnQueryParams, resolveSafeReturnUrl, resolveSessionExpiredNavigation, resolveUnauthenticatedStartupRedirect, waitForAuthenticationOutcome } from './auth-navigation.rules'; describe('resolveSafeReturnUrl', () => { it('returns the requested in-app path unchanged', () => { expect(resolveSafeReturnUrl('/servers')).toBe('/servers'); expect(resolveSafeReturnUrl('/room/abc')).toBe('/room/abc'); }); it('unwraps nested login returnUrl chains to the original destination', () => { const nested = '/login?returnUrl=%2Flogin%3FreturnUrl%3D%252Fservers'; expect(resolveSafeReturnUrl(nested)).toBe('/servers'); expect(resolveSafeReturnUrl(`/login?returnUrl=${encodeURIComponent(nested)}`)).toBe('/servers'); }); it('falls back to dashboard for auth-only return targets', () => { expect(resolveSafeReturnUrl('/login')).toBe('/dashboard'); expect(resolveSafeReturnUrl('/register')).toBe('/dashboard'); expect(resolveSafeReturnUrl(null)).toBe('/dashboard'); }); it('rejects open redirects and protocol-relative paths', () => { expect(resolveSafeReturnUrl('//evil.example/phish')).toBe('/dashboard'); expect(resolveSafeReturnUrl('https://evil.example/phish')).toBe('/dashboard'); }); }); describe('buildLoginReturnQueryParams', () => { it('preserves a safe destination when redirecting from protected routes', () => { expect(buildLoginReturnQueryParams('/servers')).toEqual({ returnUrl: '/servers' }); }); it('does not nest login returnUrl values', () => { expect(buildLoginReturnQueryParams('/login?returnUrl=%2Fservers')).toEqual({ returnUrl: '/servers' }); expect(buildLoginReturnQueryParams('/login?returnUrl=%2Flogin%3FreturnUrl%3D%252Fservers')).toEqual({ returnUrl: '/servers' }); }); it('omits returnUrl when there is no meaningful destination', () => { expect(buildLoginReturnQueryParams('/login')).toEqual({}); expect(buildLoginReturnQueryParams('/register')).toEqual({}); }); }); describe('resolveUnauthenticatedStartupRedirect', () => { it('sends signed-out visitors on the dashboard/root to login (no mobile exception)', () => { expect(resolveUnauthenticatedStartupRedirect('/dashboard')).toEqual({ path: '/login', queryParams: {} }); expect(resolveUnauthenticatedStartupRedirect('/')).toEqual({ path: '/login', queryParams: { returnUrl: '/' } }); }); it('preserves a safe returnUrl when redirecting from a protected route', () => { expect(resolveUnauthenticatedStartupRedirect('/servers')).toEqual({ path: '/login', queryParams: { returnUrl: '/servers' } }); expect(resolveUnauthenticatedStartupRedirect('/room/abc')).toEqual({ path: '/login', queryParams: { returnUrl: '/room/abc' } }); }); it('leaves public auth/invite routes untouched', () => { expect(resolveUnauthenticatedStartupRedirect('/login')).toBeNull(); expect(resolveUnauthenticatedStartupRedirect('/login?returnUrl=%2Fservers')).toBeNull(); expect(resolveUnauthenticatedStartupRedirect('/register')).toBeNull(); expect(resolveUnauthenticatedStartupRedirect('/invite/abc123')).toBeNull(); }); }); describe('resolveSessionExpiredNavigation', () => { const currentUser = { id: 'user-1' }; it('keeps an authenticated user on protected routes', () => { expect(resolveSessionExpiredNavigation(currentUser, '/room/abc')).toEqual({ kind: 'stay' }); }); it('leaves the login page when the in-memory user is still authenticated', () => { expect(resolveSessionExpiredNavigation(currentUser, '/login?returnUrl=%2Fservers')).toEqual({ kind: 'leave-auth-route', returnUrl: '/servers' }); }); it('sends fully signed-out users to login with a safe returnUrl', () => { expect(resolveSessionExpiredNavigation(null, '/room/abc')).toEqual({ kind: 'navigate-login', queryParams: { returnUrl: '/room/abc' } }); }); }); describe('waitForAuthenticationOutcome', () => { it('resolves when authentication storage preparation succeeds', async () => { const user = { id: 'user-1', oderId: 'user-1', username: 'alice', displayName: 'Alice', status: 'online' as const, role: 'member' as const, joinedAt: 1 }; const outcome = await firstValueFrom(waitForAuthenticationOutcome(of( UsersActions.setCurrentUser({ user }) ))); expect(outcome).toEqual({ kind: 'success', user }); }); it('resolves with a failure when authentication storage preparation fails', async () => { const outcome = await firstValueFrom(waitForAuthenticationOutcome(of( UsersActions.loadCurrentUserFailure({ error: 'Failed to prepare local user state.' }) ))); expect(outcome).toEqual({ kind: 'failure', error: 'Failed to prepare local user state.' }); }); });