Files
Toju/toju-app/src/app/domains/authentication/domain/logic/auth-navigation.rules.spec.ts
T
myxeliumandCursor d3d22846e7 fix: Bug - User login status showing as both logged in and logged out
Stop treating transient auth_required as home-session expiry, keep auth scope
consistent across login redirect and server-rail joins, and leave /login when
the in-memory user is still authenticated.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 01:54:01 +02:00

144 lines
4.9 KiB
TypeScript

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.'
});
});
});