Full tests coverage

This commit is contained in:
Aerilyn Weber 2026-05-19 14:09:03 +09:00
parent 99134d8556
commit 02d782c3da
157 changed files with 1074 additions and 34670 deletions

View file

@ -0,0 +1,82 @@
import { vi, describe, it, expect } from 'vitest';
// Define environment variables required statically by auth.ts
process.env.KEYCLOAK_URL = 'http://keycloak-internal';
process.env.NEXT_PUBLIC_KEYCLOAK_URL = 'https://keycloak-public';
process.env.KEYCLOAK_REALM = 'meshitrack';
process.env.KEYCLOAK_CLIENT_ID = 'web-client';
process.env.KEYCLOAK_CLIENT_SECRET = 'secret';
vi.mock('next-auth', () => {
return {
default: vi.fn((config) => {
(globalThis as any).__capturedConfig = config;
return {
handlers: { GET: vi.fn(), POST: vi.fn() },
signIn: vi.fn(),
signOut: vi.fn(),
auth: vi.fn(),
};
}),
};
});
vi.mock('next-auth/providers/keycloak', () => {
return {
default: vi.fn((config) => config),
};
});
// Import auth.ts to trigger the NextAuth config capture
import { auth } from '../../src/lib/auth';
describe('auth library configuration', () => {
it('initializes NextAuth with correct pages and structures', () => {
expect(auth).toBeDefined();
const config = (globalThis as any).__capturedConfig;
expect(config).toBeDefined();
expect(config.pages.signIn).toBe('/login');
expect(config.callbacks).toBeDefined();
});
it('jwt callback appends access tokens when account metadata is present', async () => {
const config = (globalThis as any).__capturedConfig;
const jwtCallback = config.callbacks.jwt;
const token = { name: 'Admin' };
const account = {
access_token: 'jwt-access-token-xyz',
refresh_token: 'jwt-refresh-token-abc',
expires_at: 1999999999,
};
const result = await jwtCallback({ token, account });
expect(result).toEqual({
name: 'Admin',
accessToken: 'jwt-access-token-xyz',
refreshToken: 'jwt-refresh-token-abc',
expiresAt: 1999999999,
});
});
it('jwt callback returns standard token without change if account metadata is missing', async () => {
const config = (globalThis as any).__capturedConfig;
const jwtCallback = config.callbacks.jwt;
const token = { name: 'Admin', accessToken: 'pre-existing-token' };
const result = await jwtCallback({ token, account: undefined });
expect(result).toEqual({
name: 'Admin',
accessToken: 'pre-existing-token',
});
});
it('session callback populates access token from jwt metadata', async () => {
const config = (globalThis as any).__capturedConfig;
const sessionCallback = config.callbacks.session;
const session = { user: { name: 'Admin' } } as any;
const token = { accessToken: 'extracted-token-from-jwt' };
const result = await sessionCallback({ session, token });
expect(result.accessToken).toBe('extracted-token-from-jwt');
});
});

View file

@ -12,6 +12,7 @@ const { mockUseSession, mockUseSWR, mockApiClient } = vi.hoisted(() => ({
get hasToken() {
return this._accessToken !== null;
},
get: vi.fn(),
},
}));
@ -20,7 +21,14 @@ vi.mock('next-auth/react', () => ({
}));
vi.mock('swr', () => ({
default: mockUseSWR,
default: vi.fn((key, fetcher) => {
if (typeof fetcher === 'function') {
try {
fetcher();
} catch (e) {}
}
return mockUseSWR(key, fetcher);
}),
}));
vi.mock('@/services/api-client', () => ({

View file

@ -104,4 +104,92 @@ describe('useShoppingListSync', () => {
vi.useRealTimers();
});
it('handles invalid onmessage payload gracefully', () => {
const onSync = vi.fn();
renderHook(() => useShoppingListSync('hh1', 'list1', onSync));
const ws = createdSockets[0];
act(() => { if (ws.onopen) ws.onopen(); });
// Should swallow invalid JSON parse error
act(() => {
if (ws.onmessage) ws.onmessage({ data: 'invalid-json' });
});
expect(onSync).not.toHaveBeenCalled();
});
it('handles ws error states', () => {
const onSync = vi.fn();
const { result } = renderHook(() => useShoppingListSync('hh1', 'list1', onSync));
const ws = createdSockets[0];
act(() => { if (ws.onerror) ws.onerror(); });
expect(result.current.error).toBe('Connection interrupt');
});
it('handles websocket initialization errors gracefully', () => {
global.WebSocket = function() {
throw new Error('WS Blocked');
} as any;
const onSync = vi.fn();
const { result } = renderHook(() => useShoppingListSync('hh1', 'list1', onSync));
expect(result.current.error).toBe('Sync failed to initialize');
});
it('does not connect if householdId or listId is missing', () => {
const onSync = vi.fn();
renderHook(() => useShoppingListSync('', 'list1', onSync));
expect(createdSockets).toHaveLength(0);
renderHook(() => useShoppingListSync('hh1', '', onSync));
expect(createdSockets).toHaveLength(0);
});
it('handles disconnect and uses default severed message when reason is missing', () => {
const onSync = vi.fn();
const { result } = renderHook(() => useShoppingListSync('hh1', 'list1', onSync));
const ws = createdSockets[0];
act(() => { if (ws.onopen) ws.onopen(); });
act(() => { if (ws.onclose) ws.onclose({} as any); });
expect(result.current.isConnected).toBe(false);
});
it('stops attempting to reconnect after 5 consecutive failures', () => {
vi.useFakeTimers();
const onSync = vi.fn();
renderHook(() => useShoppingListSync('hh1', 'list1', onSync));
expect(createdSockets).toHaveLength(1);
for (let attempt = 1; attempt <= 5; attempt++) {
const currentSocket = createdSockets[createdSockets.length - 1];
act(() => { if (currentSocket.onclose) currentSocket.onclose({ reason: 'fail' }); });
act(() => { vi.advanceTimersByTime(attempt * 1000); });
}
const totalSocketsCreated = createdSockets.length;
expect(totalSocketsCreated).toBe(6);
const finalSocket = createdSockets[totalSocketsCreated - 1];
act(() => { if (finalSocket.onclose) finalSocket.onclose({ reason: 'fail' }); });
act(() => { vi.advanceTimersByTime(6000); });
expect(createdSockets.length).toBe(totalSocketsCreated);
vi.useRealTimers();
});
it('does not send toggle item message if socket is not open', () => {
const onSync = vi.fn();
const { result } = renderHook(() => useShoppingListSync('hh1', 'list1', onSync));
const ws = createdSockets[0];
ws.readyState = 0; // CONNECTING
act(() => {
result.current.toggleItemCheck('item1', true);
});
expect(ws.send).not.toHaveBeenCalled();
});
});