mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 22:31:46 +00:00
merge: resolve conflicts with dev, fix 7 Snyk security issues
- Resolve translation conflicts (keep both journey + OAuth scope keys) - Resolve migrations.ts (dev OAuth migrations + journey migrations) - Fix hono directory traversal, response splitting, input validation (CVE-2026-39407/08/09/10) - Fix @hono/node-server directory traversal (CVE-2026-39406) - Fix nodemailer CRLF injection (upgrade to 8.0.5)
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
// FE-STORE-BUDGET-001 to FE-STORE-BUDGET-011
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { server } from '../../../tests/helpers/msw/server';
|
||||
import { resetAllStores, seedStore } from '../../../tests/helpers/store';
|
||||
import { buildBudgetItem } from '../../../tests/helpers/factories';
|
||||
import { useTripStore } from '../tripStore';
|
||||
|
||||
beforeEach(() => {
|
||||
resetAllStores();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
describe('budgetSlice', () => {
|
||||
it('FE-STORE-BUDGET-001: loadBudgetItems populates store', async () => {
|
||||
const item = buildBudgetItem({ trip_id: 1 });
|
||||
server.use(
|
||||
http.get('/api/trips/1/budget', () =>
|
||||
HttpResponse.json({ items: [item] })
|
||||
)
|
||||
);
|
||||
await useTripStore.getState().loadBudgetItems(1);
|
||||
expect(useTripStore.getState().budgetItems).toHaveLength(1);
|
||||
expect(useTripStore.getState().budgetItems[0].id).toBe(item.id);
|
||||
});
|
||||
|
||||
it('FE-STORE-BUDGET-002: loadBudgetItems swallows errors silently', async () => {
|
||||
server.use(
|
||||
http.get('/api/trips/1/budget', () =>
|
||||
HttpResponse.json({ error: 'server error' }, { status: 500 })
|
||||
)
|
||||
);
|
||||
// Should NOT throw
|
||||
await expect(useTripStore.getState().loadBudgetItems(1)).resolves.toBeUndefined();
|
||||
expect(useTripStore.getState().budgetItems).toEqual([]);
|
||||
});
|
||||
|
||||
it('FE-STORE-BUDGET-003: addBudgetItem appends to store and returns item', async () => {
|
||||
const newItem = buildBudgetItem({ name: 'Hotel', trip_id: 1 });
|
||||
server.use(
|
||||
http.post('/api/trips/1/budget', () =>
|
||||
HttpResponse.json({ item: newItem })
|
||||
)
|
||||
);
|
||||
const result = await useTripStore.getState().addBudgetItem(1, { name: 'Hotel' });
|
||||
expect(result.id).toBe(newItem.id);
|
||||
expect(useTripStore.getState().budgetItems).toContainEqual(newItem);
|
||||
});
|
||||
|
||||
it('FE-STORE-BUDGET-004: addBudgetItem throws on API error', async () => {
|
||||
server.use(
|
||||
http.post('/api/trips/1/budget', () =>
|
||||
HttpResponse.json({ error: 'Validation failed' }, { status: 422 })
|
||||
)
|
||||
);
|
||||
await expect(useTripStore.getState().addBudgetItem(1, {})).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('FE-STORE-BUDGET-005: updateBudgetItem replaces item in store', async () => {
|
||||
const existing = buildBudgetItem({ id: 10, trip_id: 1, name: 'Old' });
|
||||
seedStore(useTripStore, { budgetItems: [existing] });
|
||||
|
||||
const updated = { ...existing, name: 'New' };
|
||||
server.use(
|
||||
http.put('/api/trips/1/budget/10', () =>
|
||||
HttpResponse.json({ item: updated })
|
||||
)
|
||||
);
|
||||
await useTripStore.getState().updateBudgetItem(1, 10, { name: 'New' });
|
||||
const items = useTripStore.getState().budgetItems;
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0].name).toBe('New');
|
||||
});
|
||||
|
||||
it('FE-STORE-BUDGET-006: updateBudgetItem calls loadReservations when reservation_id + total_price provided', async () => {
|
||||
const existing = buildBudgetItem({ id: 20, trip_id: 1 });
|
||||
seedStore(useTripStore, { budgetItems: [existing] });
|
||||
|
||||
const loadReservations = vi.fn().mockResolvedValue(undefined);
|
||||
seedStore(useTripStore, { loadReservations });
|
||||
|
||||
const itemWithReservation = { ...existing, reservation_id: 99 };
|
||||
server.use(
|
||||
http.put('/api/trips/1/budget/20', () =>
|
||||
HttpResponse.json({ item: itemWithReservation })
|
||||
)
|
||||
);
|
||||
await useTripStore.getState().updateBudgetItem(1, 20, { total_price: 50 });
|
||||
expect(loadReservations).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it('FE-STORE-BUDGET-007: deleteBudgetItem optimistically removes and rolls back on error', async () => {
|
||||
const item = buildBudgetItem({ id: 5, trip_id: 1 });
|
||||
seedStore(useTripStore, { budgetItems: [item] });
|
||||
|
||||
server.use(
|
||||
http.delete('/api/trips/1/budget/5', () =>
|
||||
HttpResponse.json({ error: 'forbidden' }, { status: 403 })
|
||||
)
|
||||
);
|
||||
// The item is removed immediately (optimistic), then restored on error
|
||||
const deletePromise = useTripStore.getState().deleteBudgetItem(1, 5);
|
||||
await expect(deletePromise).rejects.toThrow();
|
||||
// After rollback, item is back
|
||||
expect(useTripStore.getState().budgetItems).toContainEqual(item);
|
||||
});
|
||||
|
||||
it('FE-STORE-BUDGET-008: setBudgetItemMembers updates members on matching item', async () => {
|
||||
const item = buildBudgetItem({ id: 7, trip_id: 1, members: [] });
|
||||
seedStore(useTripStore, { budgetItems: [item] });
|
||||
|
||||
const members = [{ user_id: 1, paid: false }, { user_id: 2, paid: false }];
|
||||
const updatedItem = { ...item, persons: 2, members };
|
||||
server.use(
|
||||
http.put('/api/trips/1/budget/7/members', () =>
|
||||
HttpResponse.json({ members, item: updatedItem })
|
||||
)
|
||||
);
|
||||
await useTripStore.getState().setBudgetItemMembers(1, 7, [1, 2]);
|
||||
const stored = useTripStore.getState().budgetItems.find(i => i.id === 7);
|
||||
expect(stored?.members).toHaveLength(2);
|
||||
expect(stored?.persons).toBe(2);
|
||||
});
|
||||
|
||||
it('FE-STORE-BUDGET-009: toggleBudgetMemberPaid updates paid flag on matching member', async () => {
|
||||
const item = buildBudgetItem({
|
||||
id: 8,
|
||||
trip_id: 1,
|
||||
members: [{ user_id: 3, paid: false }],
|
||||
});
|
||||
seedStore(useTripStore, { budgetItems: [item] });
|
||||
|
||||
server.use(
|
||||
http.put('/api/trips/1/budget/8/members/3/paid', () =>
|
||||
HttpResponse.json({ success: true, paid: true })
|
||||
)
|
||||
);
|
||||
await useTripStore.getState().toggleBudgetMemberPaid(1, 8, 3, true);
|
||||
const stored = useTripStore.getState().budgetItems.find(i => i.id === 8);
|
||||
expect(stored?.members?.[0]?.paid).toBe(true);
|
||||
});
|
||||
|
||||
it('FE-STORE-BUDGET-010: reorderBudgetItems reorders optimistically and reloads on error', async () => {
|
||||
const a = buildBudgetItem({ id: 1, trip_id: 1 });
|
||||
const b = buildBudgetItem({ id: 2, trip_id: 1 });
|
||||
seedStore(useTripStore, { budgetItems: [a, b] });
|
||||
|
||||
// Reorder succeeds
|
||||
server.use(
|
||||
http.put('/api/trips/1/budget/reorder/items', () =>
|
||||
HttpResponse.json({ success: true })
|
||||
)
|
||||
);
|
||||
await useTripStore.getState().reorderBudgetItems(1, [2, 1]);
|
||||
const items = useTripStore.getState().budgetItems;
|
||||
expect(items[0].id).toBe(2);
|
||||
expect(items[1].id).toBe(1);
|
||||
});
|
||||
|
||||
it('FE-STORE-BUDGET-011: reorderBudgetItems reloads list on API error', async () => {
|
||||
const a = buildBudgetItem({ id: 1, trip_id: 1 });
|
||||
const b = buildBudgetItem({ id: 2, trip_id: 1 });
|
||||
seedStore(useTripStore, { budgetItems: [a, b] });
|
||||
|
||||
const freshItem = buildBudgetItem({ id: 99, trip_id: 1 });
|
||||
server.use(
|
||||
http.put('/api/trips/1/budget/reorder/items', () =>
|
||||
HttpResponse.json({ error: 'error' }, { status: 500 })
|
||||
),
|
||||
http.get('/api/trips/1/budget', () =>
|
||||
HttpResponse.json({ items: [freshItem] })
|
||||
)
|
||||
);
|
||||
await useTripStore.getState().reorderBudgetItems(1, [2, 1]);
|
||||
// After failure, fresh list from server
|
||||
expect(useTripStore.getState().budgetItems[0].id).toBe(freshItem.id);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user