test(api): Work-sessions — fill gaps + lifecycle test
Drives src/routes/sessions.ts and src/lib/work-session.ts to 100% stmts/branch/funcs/lines (branch was 84.12% / 83.33%). - sessions.test.ts: added error/edge branch tests — malformed-JSON start (400), non-numeric id on pause/resume/stop/discard (404), pause/resume of a no-longer-active session (409), discard missing (404) + already-closed (409), stop missing (404), and discard ownership scoping (user B → 404). - work-session.test.ts: mapper unit tests — null insoleType, nullish pausedSeconds → 0, null durationSeconds/notes, full-field shape with activity/user opts, and opts defaulting to undefined. - test/integration/worker-session-lifecycle.test.ts: the anchor lifecycle test (start→pause→resume→stop→history→export). No bugs found — the code was correct. The two remaining uncovered branches were the defensive `row.pausedSeconds ?? 0` guards in resume/stop; pausedSeconds is NOT NULL DEFAULT 0 in the schema, so the null branch is unreachable via any route. Annotated both with `/* c8 ignore next -- reason */` (comment only, no behavior change).
This commit is contained in:
69
apps/api/test/integration/worker-session-lifecycle.test.ts
Normal file
69
apps/api/test/integration/worker-session-lifecycle.test.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { createApp } from '../../src/app';
|
||||
import { db } from '../../src/db/client';
|
||||
import { workSessions } from '../../src/db/schema';
|
||||
import { authToken, bearer, seedActivity, resetDb } from '../helpers';
|
||||
|
||||
describe('worker session lifecycle (start→pause→resume→stop→history→export)', () => {
|
||||
beforeEach(resetDb);
|
||||
|
||||
it('excludes paused time, shows in history, and exports a CSV row', async () => {
|
||||
const app = createApp();
|
||||
const token = await authToken(app, 'lifecycle@example.com');
|
||||
const activityId = await seedActivity('Frezen', ['Kurk']);
|
||||
|
||||
// start
|
||||
const startRes = await app.request('/api/sessions/start', {
|
||||
method: 'POST',
|
||||
headers: bearer(token),
|
||||
body: JSON.stringify({ activity_id: activityId, insole_type: 'Kurk', pair_count: 3 }),
|
||||
});
|
||||
expect(startRes.status).toBe(200);
|
||||
const started = await startRes.json();
|
||||
|
||||
// pause, then backdate the pause start by 4s so resume accumulates a real span
|
||||
await app.request(`/api/sessions/${started.id}/pause`, {
|
||||
method: 'POST',
|
||||
headers: bearer(token),
|
||||
});
|
||||
await db
|
||||
.update(workSessions)
|
||||
.set({ pausedAt: new Date(Date.now() - 4000) })
|
||||
.where(eq(workSessions.id, started.id));
|
||||
const resumeRes = await app.request(`/api/sessions/${started.id}/resume`, {
|
||||
method: 'POST',
|
||||
headers: bearer(token),
|
||||
});
|
||||
expect((await resumeRes.json()).paused_seconds).toBeGreaterThan(0);
|
||||
|
||||
// backdate the start by 10s so worked-time is deterministic-ish, then stop
|
||||
await db
|
||||
.update(workSessions)
|
||||
.set({ startTime: new Date(Date.now() - 10000) })
|
||||
.where(eq(workSessions.id, started.id));
|
||||
const stopRes = await app.request(`/api/sessions/${started.id}/stop`, {
|
||||
method: 'POST',
|
||||
headers: bearer(token),
|
||||
});
|
||||
const stopped = await stopRes.json();
|
||||
expect(stopped.status).toBe('completed');
|
||||
expect(stopped.paused_seconds).toBeGreaterThan(0);
|
||||
expect(stopped.duration_seconds).toBeLessThan(10); // paused time excluded
|
||||
const wall = Math.round((Date.parse(stopped.end_time) - Date.parse(stopped.start_time)) / 1000);
|
||||
expect(stopped.duration_seconds + stopped.paused_seconds).toBe(wall);
|
||||
|
||||
// history shows it
|
||||
const hist = await (await app.request('/api/sessions', { headers: bearer(token) })).json();
|
||||
expect(hist).toHaveLength(1);
|
||||
expect(hist[0].id).toBe(started.id);
|
||||
expect(hist[0].activity_name).toBe('Frezen');
|
||||
|
||||
// export contains the completed row
|
||||
const csvRes = await app.request('/api/export', { headers: bearer(token) });
|
||||
expect(csvRes.status).toBe(200);
|
||||
const csv = await csvRes.text();
|
||||
expect(csv).toContain('Frezen');
|
||||
expect(csv.trim().split('\n').length).toBe(2); // header + 1 row
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user