Files
solelog/apps/api/test/export.test.ts
Bas van Rossem 70ac27ec8e
All checks were successful
Build and Push Docker Image / build (push) Successful in 28s
style: align oxfmt to trailing-comma 'all' and normalize code
The repo was authored prettier-style (trailing-comma 'all') but .oxfmtrc.json
was set to 'es5', so every formatted file diverged. Switch the config to 'all'
to match the existing code, ignore docs/** and **/drizzle/** (prose + generated
snapshots the formatter should not own), and reformat the source tree once for
consistency. No behavioural change; all suites green (api 60, worker 28, admin 21).
2026-06-17 21:36:18 +02:00

133 lines
5.0 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import type { Hono } from 'hono';
import { createApp } from '../src/app';
import { db } from '../src/db/client';
import { workSessions } from '../src/db/schema';
import { eq } from 'drizzle-orm';
import { quote, formatDuration } from '../src/lib/csv';
import { authToken, bearer, seedActivity } from './helpers';
// Start a session, backdate its start_time by `durationSeconds`, then stop it,
// producing a completed session with an exact duration.
async function completedSession(
app: Hono,
token: string,
activityId: number,
insoleType: string,
durationSeconds: number,
): Promise<number> {
const startRes = await app.request('/api/sessions/start', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ activity_id: activityId, insole_type: insoleType, pair_count: 2 }),
});
const started = await startRes.json();
await db
.update(workSessions)
.set({ startTime: new Date(Date.now() - durationSeconds * 1000) })
.where(eq(workSessions.id, started.id));
await app.request(`/api/sessions/${started.id}/stop`, { method: 'POST', headers: bearer(token) });
return started.id as number;
}
describe('csv export', () => {
it('401s without a token', async () => {
const app = createApp();
const res = await app.request('/api/export');
expect(res.status).toBe(401);
});
it('exports completed sessions as CSV with the legacy header', async () => {
const app = createApp();
const token = await authToken(app, 'export-basic@example.com');
const activityId = await seedActivity('Frezen');
await completedSession(app, token, activityId, 'Kurk', 90);
const res = await app.request('/api/export', { headers: bearer(token) });
expect(res.status).toBe(200);
expect(res.headers.get('content-type')).toContain('text/csv');
expect(res.headers.get('content-disposition')).toBe(
'attachment; filename="insole-production-report.csv"',
);
const text = await res.text();
const lines = text.split('\n');
expect(lines[0]).toBe(
'"ID","Task","Insole Type","No. of Insoles","Date","Total Duration","Paused Duration","Start Time","End Time"',
);
expect(lines).toHaveLength(2);
expect(lines[1]).toContain('"Frezen"');
expect(lines[1]).toContain('"Kurk"');
expect(lines[1]).toContain('"00:01:30"');
});
it('includes a Paused Duration column carrying the formatted paused value', async () => {
const app = createApp();
const token = await authToken(app, 'export-paused@example.com');
const activityId = await seedActivity('Bekleden');
const id = await completedSession(app, token, activityId, 'Kurk', 90);
// Stamp a known paused total on the completed session.
await db.update(workSessions).set({ pausedSeconds: 75 }).where(eq(workSessions.id, id));
const res = await app.request('/api/export', { headers: bearer(token) });
expect(res.status).toBe(200);
const lines = (await res.text()).split('\n');
const header = lines[0].split(',');
const totalIdx = header.indexOf('"Total Duration"');
const pausedIdx = header.indexOf('"Paused Duration"');
expect(pausedIdx).toBe(totalIdx + 1);
const cells = lines[1].split(',');
expect(cells[pausedIdx]).toBe('"00:01:15"');
});
it('excludes active and discarded sessions and scopes to the user', async () => {
const app = createApp();
const tokenA = await authToken(app, 'export-scopeA@example.com');
const tokenB = await authToken(app, 'export-scopeB@example.com');
const activityId = await seedActivity('Slijpen');
// User A: one completed session.
await completedSession(app, tokenA, activityId, 'Kurk', 30);
// User A: an active session (left running).
await app.request('/api/sessions/start', {
method: 'POST',
headers: bearer(tokenA),
body: JSON.stringify({ activity_id: activityId, insole_type: 'Berk', pair_count: 2 }),
});
// User A: a discarded session.
const discardRes = await app.request('/api/sessions/start', {
method: 'POST',
headers: bearer(tokenA),
body: JSON.stringify({ activity_id: activityId, insole_type: '3D', pair_count: 2 }),
});
const toDiscard = await discardRes.json();
await app.request(`/api/sessions/${toDiscard.id}/discard`, {
method: 'POST',
headers: bearer(tokenA),
});
// User B: a completed session that must not appear in A's export.
await completedSession(app, tokenB, activityId, 'Kurk', 45);
const res = await app.request('/api/export', { headers: bearer(tokenA) });
expect(res.status).toBe(200);
const lines = (await res.text()).split('\n');
expect(lines).toHaveLength(2); // header + 1 completed row
});
});
describe('csv helpers', () => {
it('quote escapes embedded double quotes', () => {
expect(quote('a"b')).toBe('"a""b"');
});
it('formatDuration formats seconds as HH:MM:SS', () => {
expect(formatDuration(3661)).toBe('01:01:01');
expect(formatDuration(0)).toBe('00:00:00');
});
});