feat(quick-260917-gyd): Sitzungswaechter erkennt tote Sitzung, Header leitet ab
- auth-actions.ts: fetchSessionState() unterscheidet tote Sitzung (401/403/leere 200-Antwort, Cookie wird geloescht) von API-Ausfall (5xx/Netzwerkfehler/Nicht-JSON, unavailable ohne Redirect); fetchCurrentUser bleibt unveraendert - header.tsx: Waechter im useEffect leitet bei toter Sitzung per Vollnavigation auf /login?next=… um, bleibt bei API-Ausfall still Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
/**
|
||||
* auth-actions.test (fetchSessionState) — Sitzungswaechter (quick-260917-gyd).
|
||||
*
|
||||
* next/headers wird gemockt (cookies() liefert ein Promise auf ein Objekt
|
||||
* mit get/set/delete aus vi.hoisted, Muster: module-access.test.tsx),
|
||||
* next/navigation ebenfalls (redirect ist hier ungenutzt, muss aber
|
||||
* importierbar bleiben), fetch per vi.stubGlobal. Die 'use server'-
|
||||
* Direktive ist unter vitest wirkungslos.
|
||||
*/
|
||||
|
||||
const { cookieGet, cookieSet, cookieDelete } = vi.hoisted(() => ({
|
||||
cookieGet: vi.fn(),
|
||||
cookieSet: vi.fn(),
|
||||
cookieDelete: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('next/headers', () => ({
|
||||
cookies: () =>
|
||||
Promise.resolve({
|
||||
get: cookieGet,
|
||||
set: cookieSet,
|
||||
delete: cookieDelete,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
redirect: vi.fn(),
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe('fetchSessionState', () => {
|
||||
it('Test 1: kein Cookie -> unauthenticated, kein fetch', async () => {
|
||||
cookieGet.mockReturnValue(undefined);
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
const { fetchSessionState } = await import('./auth-actions');
|
||||
const result = await fetchSessionState();
|
||||
|
||||
expect(result).toEqual({ status: 'unauthenticated' });
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Test 2: 200 mit Benutzer -> authenticated, Cookie bleibt, Cookie-Header gesetzt', async () => {
|
||||
cookieGet.mockReturnValue({ value: 'session-abc' });
|
||||
const fetchMock = vi.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: () => Promise.resolve('{"id":"u1","username":"schalli"}'),
|
||||
}),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
const { fetchSessionState } = await import('./auth-actions');
|
||||
const result = await fetchSessionState();
|
||||
|
||||
expect(result.status).toBe('authenticated');
|
||||
if (result.status === 'authenticated') {
|
||||
expect(result.user.username).toBe('schalli');
|
||||
}
|
||||
expect(cookieDelete).not.toHaveBeenCalled();
|
||||
const [, options] = fetchMock.mock.calls[0] as unknown as [
|
||||
string,
|
||||
RequestInit,
|
||||
];
|
||||
expect((options.headers as Record<string, string>).Cookie).toBe(
|
||||
'session=session-abc',
|
||||
);
|
||||
});
|
||||
|
||||
it('Test 3: Status 401 -> unauthenticated, Cookie genau einmal geloescht', async () => {
|
||||
cookieGet.mockReturnValue({ value: 'session-abc' });
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(() => Promise.resolve({ ok: false, status: 401, text: () => Promise.resolve('') })),
|
||||
);
|
||||
|
||||
const { fetchSessionState } = await import('./auth-actions');
|
||||
const result = await fetchSessionState();
|
||||
|
||||
expect(result).toEqual({ status: 'unauthenticated' });
|
||||
expect(cookieDelete).toHaveBeenCalledTimes(1);
|
||||
expect(cookieDelete).toHaveBeenCalledWith('session');
|
||||
});
|
||||
|
||||
it('Test 4: Status 403 -> unauthenticated, Cookie geloescht', async () => {
|
||||
cookieGet.mockReturnValue({ value: 'session-abc' });
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(() => Promise.resolve({ ok: false, status: 403, text: () => Promise.resolve('') })),
|
||||
);
|
||||
|
||||
const { fetchSessionState } = await import('./auth-actions');
|
||||
const result = await fetchSessionState();
|
||||
|
||||
expect(result).toEqual({ status: 'unauthenticated' });
|
||||
expect(cookieDelete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Test 5: Status 200 mit leerem Body (oder "null") -> unauthenticated, Cookie geloescht', async () => {
|
||||
cookieGet.mockReturnValue({ value: 'session-abc' });
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(() => Promise.resolve({ ok: true, status: 200, text: () => Promise.resolve('') })),
|
||||
);
|
||||
|
||||
const { fetchSessionState } = await import('./auth-actions');
|
||||
const result1 = await fetchSessionState();
|
||||
expect(result1).toEqual({ status: 'unauthenticated' });
|
||||
expect(cookieDelete).toHaveBeenCalledTimes(1);
|
||||
|
||||
vi.clearAllMocks();
|
||||
cookieGet.mockReturnValue({ value: 'session-abc' });
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(() => Promise.resolve({ ok: true, status: 200, text: () => Promise.resolve('null') })),
|
||||
);
|
||||
const result2 = await fetchSessionState();
|
||||
expect(result2).toEqual({ status: 'unauthenticated' });
|
||||
expect(cookieDelete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Test 6: Status 500 -> unavailable, Cookie bleibt', async () => {
|
||||
cookieGet.mockReturnValue({ value: 'session-abc' });
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(() => Promise.resolve({ ok: false, status: 500, text: () => Promise.resolve('') })),
|
||||
);
|
||||
|
||||
const { fetchSessionState } = await import('./auth-actions');
|
||||
const result = await fetchSessionState();
|
||||
|
||||
expect(result).toEqual({ status: 'unavailable' });
|
||||
expect(cookieDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Test 7: fetch wirft (Netzwerkfehler) -> unavailable, Cookie bleibt', async () => {
|
||||
cookieGet.mockReturnValue({ value: 'session-abc' });
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(() => Promise.reject(new TypeError('fetch failed'))),
|
||||
);
|
||||
|
||||
const { fetchSessionState } = await import('./auth-actions');
|
||||
const result = await fetchSessionState();
|
||||
|
||||
expect(result).toEqual({ status: 'unavailable' });
|
||||
expect(cookieDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('Test 8: Status 200 mit nicht-JSON-Body -> unavailable, Cookie bleibt', async () => {
|
||||
cookieGet.mockReturnValue({ value: 'session-abc' });
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(() => Promise.resolve({ ok: true, status: 200, text: () => Promise.resolve('<html>') })),
|
||||
);
|
||||
|
||||
const { fetchSessionState } = await import('./auth-actions');
|
||||
const result = await fetchSessionState();
|
||||
|
||||
expect(result).toEqual({ status: 'unavailable' });
|
||||
expect(cookieDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user