Files
TREK/client/src/components/Admin/AuditLogPanel.test.tsx
T
jubnl fd48169219 test(client): expand frontend test suite to 69.1% coverage
Add and extend tests across 32 files (+10 595 lines) covering Admin
panels (AuditLog, Backup, DevNotifications, GitHub), Collab (Chat,
Notes, Panel, Polls), Planner (DayDetailPanel, DayPlanSidebar),
Settings (DisplaySettings, Integrations, MapSettings), Files
(FileManager, FilesPage), Map, Layout (DemoBanner,
InAppNotificationBell), shared pickers (CustomDateTimePicker,
CustomTimePicker), Vacay holidays, pages (Dashboard, Login), unit
stores (authStore, inAppNotificationStore), API (authUrl, client
integration), and i18n. Also updates sonar-project.properties and
MSW trip handlers to support the new cases.
2026-04-07 21:56:08 +02:00

224 lines
8.0 KiB
TypeScript

// FE-ADMIN-AUDIT-001 to FE-ADMIN-AUDIT-010
import { render, screen, waitFor } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { server } from '../../../tests/helpers/msw/server';
import { resetAllStores } from '../../../tests/helpers/store';
import AuditLogPanel from './AuditLogPanel';
const ENTRY_1 = {
id: 1,
created_at: '2025-06-01T10:30:00Z',
user_id: 5,
username: 'alice',
user_email: 'alice@example.com',
action: 'trip.create',
resource: '/trips/42',
details: { title: 'Test' },
ip: '127.0.0.1',
};
const ENTRY_2 = {
id: 2,
created_at: '2025-06-02T11:00:00Z',
user_id: 6,
username: 'bob',
user_email: 'bob@example.com',
action: 'trip.delete',
resource: '/trips/43',
details: null,
ip: '10.0.0.1',
};
beforeEach(() => {
resetAllStores();
});
afterEach(() => {
server.resetHandlers();
});
describe('AuditLogPanel', () => {
it('FE-ADMIN-AUDIT-001: loading state shown on mount', async () => {
server.use(
http.get('/api/admin/audit-log', async () => {
await new Promise(() => {}); // never resolves
return HttpResponse.json({ entries: [], total: 0 });
}),
);
render(<AuditLogPanel serverTimezone="UTC" />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
expect(document.querySelector('table')).not.toBeInTheDocument();
});
it('FE-ADMIN-AUDIT-002: empty state shown when no entries', async () => {
server.use(
http.get('/api/admin/audit-log', () =>
HttpResponse.json({ entries: [], total: 0 }),
),
);
render(<AuditLogPanel serverTimezone="UTC" />);
await screen.findByText('No audit entries yet.');
expect(document.querySelector('table')).not.toBeInTheDocument();
});
it('FE-ADMIN-AUDIT-003: table renders all columns with data', async () => {
server.use(
http.get('/api/admin/audit-log', () =>
HttpResponse.json({ entries: [ENTRY_1], total: 1 }),
),
);
render(<AuditLogPanel serverTimezone="UTC" />);
await screen.findByText('trip.create');
expect(screen.getByText('Time')).toBeInTheDocument();
expect(screen.getByText('User')).toBeInTheDocument();
expect(screen.getByText('Action')).toBeInTheDocument();
expect(screen.getByText('Resource')).toBeInTheDocument();
expect(screen.getByText('IP')).toBeInTheDocument();
expect(screen.getByText('Details')).toBeInTheDocument();
expect(screen.getByText('alice')).toBeInTheDocument();
expect(screen.getByText('/trips/42')).toBeInTheDocument();
expect(screen.getByText('127.0.0.1')).toBeInTheDocument();
expect(screen.getByText('{"title":"Test"}')).toBeInTheDocument();
});
it('FE-ADMIN-AUDIT-004: userLabel fallback chain', async () => {
const entries = [
{ ...ENTRY_1, id: 10, username: 'alice', user_email: null, user_id: 5, action: 'a.username' },
{ ...ENTRY_1, id: 11, username: null, user_email: 'bob@example.com', user_id: 6, action: 'a.email' },
{ ...ENTRY_1, id: 12, username: null, user_email: null, user_id: 7, action: 'a.id' },
{ ...ENTRY_1, id: 13, username: null, user_email: null, user_id: null, action: 'a.none' },
];
server.use(
http.get('/api/admin/audit-log', () =>
HttpResponse.json({ entries, total: 4 }),
),
);
render(<AuditLogPanel serverTimezone="UTC" />);
await screen.findByText('a.username');
expect(screen.getByText('alice')).toBeInTheDocument();
expect(screen.getByText('bob@example.com')).toBeInTheDocument();
expect(screen.getByText('#7')).toBeInTheDocument();
// '—' appears multiple times (null resource, null ip for some, null user) — just check it exists
expect(screen.getAllByText('—').length).toBeGreaterThan(0);
});
it('FE-ADMIN-AUDIT-005: dash shown for null resource, ip, and details', async () => {
const entry = {
...ENTRY_1,
id: 20,
action: 'a.nulls',
resource: null,
ip: null,
details: null,
};
const entryEmptyDetails = {
...ENTRY_1,
id: 21,
action: 'a.emptyobj',
resource: '/ok',
ip: '1.2.3.4',
details: {},
};
server.use(
http.get('/api/admin/audit-log', () =>
HttpResponse.json({ entries: [entry, entryEmptyDetails], total: 2 }),
),
);
render(<AuditLogPanel serverTimezone="UTC" />);
await screen.findByText('a.nulls');
// null resource, null ip, null details → three '—' for entry; empty obj details → another '—'
const dashes = screen.getAllByText('—');
expect(dashes.length).toBeGreaterThanOrEqual(4);
});
it('FE-ADMIN-AUDIT-006: showing count text reflects count and total', async () => {
server.use(
http.get('/api/admin/audit-log', () =>
HttpResponse.json({ entries: [ENTRY_1], total: 50 }),
),
);
render(<AuditLogPanel serverTimezone="UTC" />);
await screen.findByText('trip.create');
expect(screen.getByText('1 loaded · 50 total')).toBeInTheDocument();
});
it('FE-ADMIN-AUDIT-007: "Load more" appends entries', async () => {
let callCount = 0;
server.use(
http.get('/api/admin/audit-log', () => {
callCount++;
if (callCount === 1) {
return HttpResponse.json({ entries: [ENTRY_1], total: 2 });
}
return HttpResponse.json({ entries: [ENTRY_2], total: 2 });
}),
);
const user = userEvent.setup();
render(<AuditLogPanel serverTimezone="UTC" />);
await screen.findByText('trip.create');
const loadMoreBtn = screen.getByText('Load more');
expect(loadMoreBtn).toBeInTheDocument();
await user.click(loadMoreBtn);
await screen.findByText('trip.delete');
expect(screen.getByText('trip.create')).toBeInTheDocument();
expect(screen.queryByText('Load more')).not.toBeInTheDocument();
});
it('FE-ADMIN-AUDIT-008: "Load more" hidden when all entries loaded', async () => {
server.use(
http.get('/api/admin/audit-log', () =>
HttpResponse.json({ entries: [ENTRY_1, ENTRY_2], total: 2 }),
),
);
render(<AuditLogPanel serverTimezone="UTC" />);
await screen.findByText('trip.create');
expect(screen.queryByText('Load more')).not.toBeInTheDocument();
});
it('FE-ADMIN-AUDIT-009: Refresh resets list to page 1', async () => {
const PAGE1_ENTRY = { ...ENTRY_1, id: 100, action: 'phase1.action' };
const PAGE2_ENTRY = { ...ENTRY_2, id: 101, action: 'phase2.action' };
const REFRESH_ENTRY = { ...ENTRY_2, id: 102, action: 'phase3.refresh' };
let callCount = 0;
server.use(
http.get('/api/admin/audit-log', () => {
callCount++;
if (callCount === 1) {
return HttpResponse.json({ entries: [PAGE1_ENTRY], total: 2 });
}
if (callCount === 2) {
return HttpResponse.json({ entries: [PAGE2_ENTRY], total: 2 });
}
return HttpResponse.json({ entries: [REFRESH_ENTRY], total: 1 });
}),
);
const user = userEvent.setup();
render(<AuditLogPanel serverTimezone="UTC" />);
// Initial load: PAGE1_ENTRY visible, load more
await screen.findByText('phase1.action');
const loadMoreBtn = screen.getByText('Load more');
await user.click(loadMoreBtn);
await screen.findByText('phase2.action');
// Now refresh
const refreshBtn = screen.getByText('Refresh');
await user.click(refreshBtn);
// After refresh, only REFRESH_ENTRY should be visible
await screen.findByText('phase3.refresh');
await waitFor(() => expect(screen.queryByText('phase1.action')).not.toBeInTheDocument());
expect(screen.queryByText('phase2.action')).not.toBeInTheDocument();
});
it('FE-ADMIN-AUDIT-010: Refresh button is disabled while loading', async () => {
server.use(
http.get('/api/admin/audit-log', async () => {
await new Promise(() => {}); // never resolves
return HttpResponse.json({ entries: [], total: 0 });
}),
);
render(<AuditLogPanel serverTimezone="UTC" />);
const refreshBtn = screen.getByText('Refresh');
expect(refreshBtn.closest('button')).toBeDisabled();
});
});