Full tests coverage
This commit is contained in:
parent
99134d8556
commit
02d782c3da
157 changed files with 1074 additions and 34670 deletions
82
packages/web/tests/lib/auth.test.ts
Normal file
82
packages/web/tests/lib/auth.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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', () => ({
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue