test(front): add test suite frontend (WIP)

This commit is contained in:
jubnl
2026-04-07 12:31:09 +02:00
parent 96080e8a03
commit 3c31902885
97 changed files with 16973 additions and 4 deletions
@@ -0,0 +1,12 @@
import { http, HttpResponse } from 'msw';
export const addonHandlers = [
http.get('/api/addons', () => {
return HttpResponse.json({
addons: [
{ id: 'vacay', name: 'Vacay', type: 'feature', icon: 'calendar', enabled: true },
{ id: 'atlas', name: 'Atlas', type: 'feature', icon: 'map', enabled: true },
],
});
}),
];
+125
View File
@@ -0,0 +1,125 @@
import { http, HttpResponse } from 'msw';
import { buildUser, buildAdmin } from '../../factories';
export const adminHandlers = [
http.get('/api/admin/users', () => {
const user1 = buildUser({ username: 'alice', email: 'alice@example.com' });
const admin1 = buildAdmin({ username: 'admin', email: 'admin@example.com' });
return HttpResponse.json({ users: [admin1, user1] });
}),
http.post('/api/admin/users', async ({ request }) => {
const body = await request.json() as Record<string, unknown>;
const user = buildUser({ ...body });
return HttpResponse.json({ user });
}),
http.put('/api/admin/users/:id', async ({ params, request }) => {
const body = await request.json() as Record<string, unknown>;
const user = buildUser({ id: Number(params.id), ...body });
return HttpResponse.json({ user });
}),
http.delete('/api/admin/users/:id', () => {
return HttpResponse.json({ success: true });
}),
http.get('/api/admin/stats', () => {
return HttpResponse.json({
totalUsers: 2,
totalTrips: 5,
totalPlaces: 42,
totalFiles: 8,
});
}),
http.get('/api/admin/invites', () => {
return HttpResponse.json({ invites: [] });
}),
http.post('/api/admin/invites', async ({ request }) => {
const body = await request.json() as Record<string, unknown>;
return HttpResponse.json({ invite: { id: 1, token: 'test-invite-token', ...body } });
}),
http.delete('/api/admin/invites/:id', () => {
return HttpResponse.json({ success: true });
}),
http.get('/api/admin/oidc', () => {
return HttpResponse.json({
issuer: '',
client_id: '',
client_secret: '',
client_secret_set: false,
display_name: '',
oidc_only: false,
discovery_url: '',
});
}),
http.put('/api/admin/oidc', async ({ request }) => {
const body = await request.json() as Record<string, unknown>;
return HttpResponse.json({ ...body });
}),
http.get('/api/admin/version-check', () => {
return HttpResponse.json({ update_available: false, latest: '1.0.0', current: '1.0.0' });
}),
http.get('/api/admin/bag-tracking', () => {
return HttpResponse.json({ enabled: false });
}),
http.put('/api/admin/bag-tracking', async ({ request }) => {
const body = await request.json() as Record<string, unknown>;
return HttpResponse.json({ enabled: body.enabled });
}),
http.get('/api/admin/addons', () => {
return HttpResponse.json({ addons: [] });
}),
http.get('/api/admin/packing-templates', () => {
return HttpResponse.json({ templates: [] });
}),
http.get('/api/admin/audit-log', () => {
return HttpResponse.json({ logs: [], total: 0 });
}),
http.get('/api/admin/mcp-tokens', () => {
return HttpResponse.json({ tokens: [] });
}),
http.get('/api/admin/permissions', () => {
return HttpResponse.json({ permissions: {} });
}),
http.get('/api/admin/notification-preferences', () => {
return HttpResponse.json({
event_types: [],
available_channels: {},
implemented_combos: {},
preferences: {},
});
}),
// Auth settings endpoints used by AdminPage
http.get('/api/auth/app-settings', () => {
return HttpResponse.json({});
}),
http.put('/api/auth/app-settings', async ({ request }) => {
const body = await request.json() as Record<string, unknown>;
return HttpResponse.json({ ...body });
}),
http.get('/api/auth/me/settings', () => {
return HttpResponse.json({ settings: { maps_api_key: '', openweather_api_key: '' } });
}),
http.get('/api/auth/validate-keys', () => {
return HttpResponse.json({ maps: true, weather: true });
}),
];
@@ -0,0 +1,28 @@
import { http, HttpResponse } from 'msw';
import { buildAssignment, buildPlace } from '../../factories';
export const assignmentsHandlers = [
http.post('/api/trips/:id/days/:dayId/assignments', async ({ params, request }) => {
const body = await request.json() as { place_id: number };
const place = buildPlace({ id: body.place_id, trip_id: Number(params.id) });
const assignment = buildAssignment({
day_id: Number(params.dayId),
place_id: body.place_id,
place,
order_index: 0,
});
return HttpResponse.json({ assignment });
}),
http.delete('/api/trips/:id/days/:dayId/assignments/:assignmentId', () => {
return HttpResponse.json({ success: true });
}),
http.put('/api/trips/:id/days/:dayId/assignments/reorder', () => {
return HttpResponse.json({ success: true });
}),
http.put('/api/trips/:id/assignments/:assignmentId/move', () => {
return HttpResponse.json({ success: true });
}),
];
+31
View File
@@ -0,0 +1,31 @@
import { http, HttpResponse } from 'msw';
import { buildUser, buildAppConfig } from '../../factories';
export const authHandlers = [
http.post('/api/auth/login', () => {
const user = buildUser();
return HttpResponse.json({ user, token: 'mock-token' });
}),
http.get('/api/auth/me', () => {
const user = buildUser();
return HttpResponse.json({ user });
}),
http.post('/api/auth/register', () => {
const user = buildUser();
return HttpResponse.json({ user, token: 'mock-token' });
}),
http.get('/api/auth/app-config', () => {
return HttpResponse.json(buildAppConfig());
}),
http.post('/api/auth/ws-token', () => {
return HttpResponse.json({ token: 'mock-ws-token' });
}),
http.post('/api/auth/logout', () => {
return HttpResponse.json({ success: true });
}),
];
@@ -0,0 +1,38 @@
import { http, HttpResponse } from 'msw';
import { buildBudgetItem } from '../../factories';
export const budgetHandlers = [
http.get('/api/trips/:id/budget', ({ params }) => {
return HttpResponse.json({
items: [buildBudgetItem({ trip_id: Number(params.id) })],
});
}),
http.post('/api/trips/:id/budget', async ({ params, request }) => {
const body = await request.json() as Record<string, unknown>;
const item = buildBudgetItem({ trip_id: Number(params.id), ...body });
return HttpResponse.json({ item });
}),
http.put('/api/trips/:id/budget/:itemId', async ({ params, request }) => {
const body = await request.json() as Record<string, unknown>;
const item = buildBudgetItem({ id: Number(params.itemId), trip_id: Number(params.id), ...body });
return HttpResponse.json({ item });
}),
http.delete('/api/trips/:id/budget/:itemId', () => {
return HttpResponse.json({ success: true });
}),
http.put('/api/trips/:id/budget/:itemId/members', async ({ params, request }) => {
const body = await request.json() as { user_ids: number[] };
const members = body.user_ids.map(uid => ({ user_id: uid, paid: false }));
const item = buildBudgetItem({ id: Number(params.itemId), trip_id: Number(params.id), persons: body.user_ids.length, members });
return HttpResponse.json({ members, item });
}),
http.put('/api/trips/:id/budget/:itemId/members/:userId/paid', async ({ params, request }) => {
const body = await request.json() as { paid: boolean };
return HttpResponse.json({ success: true, paid: body.paid });
}),
];
@@ -0,0 +1,31 @@
import { http, HttpResponse } from 'msw';
import { buildDayNote } from '../../factories';
export const dayNotesHandlers = [
http.get('/api/trips/:id/days/:dayId/notes', ({ params }) => {
return HttpResponse.json({
notes: [buildDayNote({ day_id: Number(params.dayId) })],
});
}),
http.post('/api/trips/:id/days/:dayId/notes', async ({ params, request }) => {
const body = await request.json() as Record<string, unknown>;
const note = buildDayNote({ day_id: Number(params.dayId), ...body });
return HttpResponse.json({ note });
}),
http.put('/api/trips/:id/days/:dayId/notes/:noteId', async ({ params, request }) => {
const body = await request.json() as Record<string, unknown>;
const note = buildDayNote({ id: Number(params.noteId), day_id: Number(params.dayId), ...body });
return HttpResponse.json({ note });
}),
http.delete('/api/trips/:id/days/:dayId/notes/:noteId', () => {
return HttpResponse.json({ success: true });
}),
http.put('/api/trips/:id/days/:dayId', async ({ params, request }) => {
const body = await request.json() as Record<string, unknown>;
return HttpResponse.json({ day: { id: Number(params.dayId), trip_id: Number(params.id), ...body } });
}),
];
@@ -0,0 +1,19 @@
import { http, HttpResponse } from 'msw';
import { buildTripFile } from '../../factories';
export const filesHandlers = [
http.get('/api/trips/:id/files', ({ params }) => {
return HttpResponse.json({
files: [buildTripFile({ trip_id: Number(params.id) })],
});
}),
http.post('/api/trips/:id/files', ({ params }) => {
const file = buildTripFile({ trip_id: Number(params.id) });
return HttpResponse.json({ file });
}),
http.delete('/api/trips/:id/files/:fileId', () => {
return HttpResponse.json({ success: true });
}),
];
@@ -0,0 +1,37 @@
import { authHandlers } from './auth';
import { settingsHandlers } from './settings';
import { addonHandlers } from './addons';
import { notificationHandlers } from './notifications';
import { vacayHandlers } from './vacay';
import { tripsHandlers } from './trips';
import { placesHandlers } from './places';
import { assignmentsHandlers } from './assignments';
import { packingHandlers } from './packing';
import { todoHandlers } from './todo';
import { budgetHandlers } from './budget';
import { reservationsHandlers } from './reservations';
import { filesHandlers } from './files';
import { tagsHandlers } from './tags';
import { dayNotesHandlers } from './dayNotes';
import { adminHandlers } from './admin';
import { sharedHandlers } from './shared';
export const defaultHandlers = [
...authHandlers,
...settingsHandlers,
...addonHandlers,
...notificationHandlers,
...vacayHandlers,
...tripsHandlers,
...placesHandlers,
...assignmentsHandlers,
...packingHandlers,
...todoHandlers,
...budgetHandlers,
...reservationsHandlers,
...filesHandlers,
...tagsHandlers,
...dayNotesHandlers,
...adminHandlers,
...sharedHandlers,
];
@@ -0,0 +1,90 @@
import { http, HttpResponse } from 'msw';
export const notificationHandlers = [
http.get('/api/notifications/in-app', ({ request }) => {
const url = new URL(request.url);
const offset = parseInt(url.searchParams.get('offset') || '0', 10);
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
const allNotifications = Array.from({ length: 25 }, (_, i) => ({
id: i + 1,
type: 'simple',
scope: 'trip',
target: 1,
sender_id: 2,
sender_username: 'alice',
sender_avatar: null,
recipient_id: 1,
title_key: 'notif.title',
title_params: '{}',
text_key: 'notif.text',
text_params: '{}',
positive_text_key: null,
negative_text_key: null,
response: null,
navigate_text_key: null,
navigate_target: null,
is_read: i < 5 ? 0 : 1,
created_at: '2025-01-01T00:00:00.000Z',
}));
const page = allNotifications.slice(offset, offset + limit);
return HttpResponse.json({
notifications: page,
total: allNotifications.length,
unread_count: 5,
});
}),
http.get('/api/notifications/in-app/unread-count', () => {
return HttpResponse.json({ count: 5 });
}),
http.put('/api/notifications/in-app/:id/read', () => {
return HttpResponse.json({ success: true });
}),
http.put('/api/notifications/in-app/:id/unread', () => {
return HttpResponse.json({ success: true });
}),
http.put('/api/notifications/in-app/read-all', () => {
return HttpResponse.json({ success: true });
}),
http.delete('/api/notifications/in-app/:id', () => {
return HttpResponse.json({ success: true });
}),
http.delete('/api/notifications/in-app/all', () => {
return HttpResponse.json({ success: true });
}),
http.post('/api/notifications/in-app/:id/respond', async ({ request, params }) => {
const body = await request.json() as { response: string };
return HttpResponse.json({
notification: {
id: Number(params.id),
type: 'boolean',
scope: 'trip',
target: 1,
sender_id: 2,
sender_username: 'alice',
sender_avatar: null,
recipient_id: 1,
title_key: 'notif.title',
title_params: '{}',
text_key: 'notif.text',
text_params: '{}',
positive_text_key: 'accept',
negative_text_key: 'decline',
response: body.response,
navigate_text_key: null,
navigate_target: null,
is_read: 1,
created_at: '2025-01-01T00:00:00.000Z',
},
});
}),
];
@@ -0,0 +1,26 @@
import { http, HttpResponse } from 'msw';
import { buildPackingItem } from '../../factories';
export const packingHandlers = [
http.get('/api/trips/:id/packing', ({ params }) => {
return HttpResponse.json({
items: [buildPackingItem({ trip_id: Number(params.id) })],
});
}),
http.post('/api/trips/:id/packing', async ({ params, request }) => {
const body = await request.json() as Record<string, unknown>;
const item = buildPackingItem({ trip_id: Number(params.id), ...body });
return HttpResponse.json({ item });
}),
http.put('/api/trips/:id/packing/:itemId', async ({ params, request }) => {
const body = await request.json() as Record<string, unknown>;
const item = buildPackingItem({ id: Number(params.itemId), trip_id: Number(params.id), ...body });
return HttpResponse.json({ item });
}),
http.delete('/api/trips/:id/packing/:itemId', () => {
return HttpResponse.json({ success: true });
}),
];
@@ -0,0 +1,25 @@
import { http, HttpResponse } from 'msw';
import { buildPlace } from '../../factories';
export const placesHandlers = [
http.get('/api/trips/:id/places', ({ params }) => {
const tripId = Number(params.id);
return HttpResponse.json({ places: [buildPlace({ trip_id: tripId }), buildPlace({ trip_id: tripId })] });
}),
http.post('/api/trips/:id/places', async ({ params, request }) => {
const body = await request.json() as Record<string, unknown>;
const place = buildPlace({ trip_id: Number(params.id), ...body });
return HttpResponse.json({ place });
}),
http.put('/api/trips/:id/places/:placeId', async ({ params, request }) => {
const body = await request.json() as Record<string, unknown>;
const place = buildPlace({ id: Number(params.placeId), trip_id: Number(params.id), ...body });
return HttpResponse.json({ place });
}),
http.delete('/api/trips/:id/places/:placeId', () => {
return HttpResponse.json({ success: true });
}),
];
@@ -0,0 +1,30 @@
import { http, HttpResponse } from 'msw';
import { buildReservation } from '../../factories';
export const reservationsHandlers = [
http.get('/api/trips/:id/reservations', ({ params }) => {
return HttpResponse.json({
reservations: [buildReservation({ trip_id: Number(params.id) })],
});
}),
http.post('/api/trips/:id/reservations', async ({ params, request }) => {
const body = await request.json() as Record<string, unknown>;
const reservation = buildReservation({ trip_id: Number(params.id), ...body });
return HttpResponse.json({ reservation });
}),
http.put('/api/trips/:id/reservations/:reservationId', async ({ params, request }) => {
const body = await request.json() as Record<string, unknown>;
const reservation = buildReservation({
id: Number(params.reservationId),
trip_id: Number(params.id),
...body,
});
return HttpResponse.json({ reservation });
}),
http.delete('/api/trips/:id/reservations/:reservationId', () => {
return HttpResponse.json({ success: true });
}),
];
@@ -0,0 +1,16 @@
import { http, HttpResponse } from 'msw';
import { buildSettings } from '../../factories';
export const settingsHandlers = [
http.get('/api/settings', () => {
return HttpResponse.json({ settings: buildSettings() });
}),
http.put('/api/settings', () => {
return HttpResponse.json({ success: true });
}),
http.post('/api/settings/bulk', () => {
return HttpResponse.json({ success: true });
}),
];
@@ -0,0 +1,36 @@
import { http, HttpResponse } from 'msw';
import { buildTrip, buildDay, buildPlace } from '../../factories';
export const sharedHandlers = [
http.get('/api/shared/:token', ({ params }) => {
const { token } = params;
if (token === 'invalid-token' || token === 'expired-token') {
return new HttpResponse(null, { status: 404 });
}
const trip = { ...buildTrip({ start_date: '2026-07-01', end_date: '2026-07-05' }), title: 'Shared Paris Trip' };
const day1 = buildDay({ trip_id: trip.id, date: '2026-07-01' });
const place1 = buildPlace({ trip_id: trip.id, name: 'Eiffel Tower', lat: 48.8584, lng: 2.2945 });
return HttpResponse.json({
trip,
days: [day1],
assignments: {},
dayNotes: {},
places: [place1],
reservations: [],
accommodations: [],
packing: [],
budget: [],
categories: [],
permissions: {
share_bookings: true,
share_packing: false,
share_budget: false,
share_collab: false,
},
collab: [],
});
}),
];
+24
View File
@@ -0,0 +1,24 @@
import { http, HttpResponse } from 'msw';
import { buildTag, buildCategory } from '../../factories';
export const tagsHandlers = [
http.get('/api/tags', () => {
return HttpResponse.json({ tags: [buildTag(), buildTag()] });
}),
http.post('/api/tags', async ({ request }) => {
const body = await request.json() as Record<string, unknown>;
const tag = buildTag(body);
return HttpResponse.json({ tag });
}),
http.get('/api/categories', () => {
return HttpResponse.json({ categories: [buildCategory(), buildCategory()] });
}),
http.post('/api/categories', async ({ request }) => {
const body = await request.json() as Record<string, unknown>;
const category = buildCategory(body);
return HttpResponse.json({ category });
}),
];
+26
View File
@@ -0,0 +1,26 @@
import { http, HttpResponse } from 'msw';
import { buildTodoItem } from '../../factories';
export const todoHandlers = [
http.get('/api/trips/:id/todo', ({ params }) => {
return HttpResponse.json({
items: [buildTodoItem({ trip_id: Number(params.id) })],
});
}),
http.post('/api/trips/:id/todo', async ({ params, request }) => {
const body = await request.json() as Record<string, unknown>;
const item = buildTodoItem({ trip_id: Number(params.id), ...body });
return HttpResponse.json({ item });
}),
http.put('/api/trips/:id/todo/:itemId', async ({ params, request }) => {
const body = await request.json() as Record<string, unknown>;
const item = buildTodoItem({ id: Number(params.itemId), trip_id: Number(params.id), ...body });
return HttpResponse.json({ item });
}),
http.delete('/api/trips/:id/todo/:itemId', () => {
return HttpResponse.json({ success: true });
}),
];
@@ -0,0 +1,49 @@
import { http, HttpResponse } from 'msw';
import { buildTrip, buildDay, buildUser } from '../../factories';
export const tripsHandlers = [
// List all trips (active or archived)
http.get('/api/trips', ({ request }) => {
const url = new URL(request.url);
const archived = url.searchParams.get('archived');
if (archived) {
return HttpResponse.json({ trips: [] });
}
const trip1 = buildTrip({ title: 'Paris Adventure', start_date: '2026-07-01', end_date: '2026-07-10' });
const trip2 = buildTrip({ title: 'Tokyo Trip', start_date: '2026-09-01', end_date: '2026-09-15' });
return HttpResponse.json({ trips: [trip1, trip2] });
}),
http.get('/api/trips/:id', ({ params }) => {
const trip = buildTrip({ id: Number(params.id) });
return HttpResponse.json({ trip });
}),
http.get('/api/trips/:id/days', ({ params }) => {
const tripId = Number(params.id);
const day1 = buildDay({ trip_id: tripId, assignments: [], notes_items: [] });
const day2 = buildDay({ trip_id: tripId, assignments: [], notes_items: [] });
return HttpResponse.json({ days: [day1, day2] });
}),
http.put('/api/trips/:id', async ({ params, request }) => {
const body = await request.json() as Record<string, unknown>;
const trip = buildTrip({ id: Number(params.id), ...body });
return HttpResponse.json({ trip });
}),
http.post('/api/trips', async ({ request }) => {
const body = await request.json() as Record<string, unknown>;
const trip = buildTrip({ ...body });
return HttpResponse.json({ trip });
}),
http.get('/api/trips/:id/members', ({ params }) => {
const owner = buildUser();
return HttpResponse.json({ owner, members: [] });
}),
http.get('/api/trips/:id/accommodations', () => {
return HttpResponse.json({ accommodations: [] });
}),
];
+127
View File
@@ -0,0 +1,127 @@
import { http, HttpResponse } from 'msw';
export const vacayHandlers = [
http.get('/api/addons/vacay/plan', () => {
return HttpResponse.json({
plan: {
id: 1,
holidays_enabled: false,
holidays_region: null,
holiday_calendars: [],
block_weekends: true,
carry_over_enabled: false,
company_holidays_enabled: false,
},
users: [{ id: 1, username: 'user1', color: '#3b82f6' }],
pendingInvites: [],
incomingInvites: [],
isOwner: true,
isFused: false,
});
}),
http.put('/api/addons/vacay/plan', () => {
return HttpResponse.json({
plan: {
id: 1,
holidays_enabled: true,
holidays_region: null,
holiday_calendars: [],
block_weekends: true,
carry_over_enabled: false,
company_holidays_enabled: false,
},
});
}),
http.get('/api/addons/vacay/years', () => {
return HttpResponse.json({ years: [2025, 2026] });
}),
http.post('/api/addons/vacay/years', () => {
return HttpResponse.json({ years: [2025, 2026, 2027] });
}),
http.delete('/api/addons/vacay/years/:year', () => {
return HttpResponse.json({ years: [2025] });
}),
http.get('/api/addons/vacay/entries/:year', () => {
return HttpResponse.json({
entries: [
{ date: '2025-06-15', user_id: 1 },
{ date: '2025-06-16', user_id: 1 },
],
companyHolidays: [],
});
}),
http.post('/api/addons/vacay/entries/toggle', () => {
return HttpResponse.json({ success: true });
}),
http.post('/api/addons/vacay/entries/company-holiday', () => {
return HttpResponse.json({ success: true });
}),
http.get('/api/addons/vacay/stats/:year', () => {
return HttpResponse.json({
stats: [{ user_id: 1, vacation_days: 30, used: 2 }],
});
}),
http.put('/api/addons/vacay/stats/:year', () => {
return HttpResponse.json({ success: true });
}),
http.get('/api/addons/vacay/holidays/countries', () => {
return HttpResponse.json({ countries: ['DE', 'US', 'FR'] });
}),
http.get('/api/addons/vacay/holidays/:year/:country', () => {
return HttpResponse.json([
{ date: '2025-12-25', name: 'Christmas', localName: 'Weihnachten', global: true, counties: null },
{ date: '2025-01-01', name: 'New Year', localName: 'Neujahr', global: true, counties: null },
]);
}),
http.put('/api/addons/vacay/color', () => {
return HttpResponse.json({ success: true });
}),
http.post('/api/addons/vacay/invite', () => {
return HttpResponse.json({ success: true });
}),
http.post('/api/addons/vacay/invite/accept', () => {
return HttpResponse.json({ success: true });
}),
http.post('/api/addons/vacay/invite/decline', () => {
return HttpResponse.json({ success: true });
}),
http.post('/api/addons/vacay/invite/cancel', () => {
return HttpResponse.json({ success: true });
}),
http.post('/api/addons/vacay/dissolve', () => {
return HttpResponse.json({ success: true });
}),
http.post('/api/addons/vacay/plan/holiday-calendars', () => {
return HttpResponse.json({
calendar: { id: 1, plan_id: 1, region: 'DE', label: null, color: '#ef4444', sort_order: 0 },
});
}),
http.put('/api/addons/vacay/plan/holiday-calendars/:id', () => {
return HttpResponse.json({
calendar: { id: 1, plan_id: 1, region: 'US', label: 'US Holidays', color: '#3b82f6', sort_order: 0 },
});
}),
http.delete('/api/addons/vacay/plan/holiday-calendars/:id', () => {
return HttpResponse.json({ success: true });
}),
];
+4
View File
@@ -0,0 +1,4 @@
import { setupServer } from 'msw/node';
import { defaultHandlers } from './handlers';
export const server = setupServer(...defaultHandlers);