test(api): Admin sessions — fill gaps + lifecycle test
Fills the session-part coverage gaps in src/routes/admin.ts (cross-user list, active list, manual create/edit, admin stop/discard) and adds the admin↔worker lifecycle test. New route-level tests (apps/api/test/admin.test.ts): - manual create: 400 on schema-invalid body; 400 when paused_seconds > span. - edit: 404 non-numeric id; 400 schema-invalid body; 400 unparseable start_time; 400 paused_seconds > span; 404 unknown activity. - stop: 404 non-numeric/unknown id; folds an open pause into paused_seconds. - discard: 404 non-numeric/unknown id; 409 when already closed. Lifecycle (apps/api/test/integration/admin-worker-session.test.ts): worker starts → admin stops via the admin endpoint → session lands in /api/admin/report and /api/admin/export with matching totals (uses resetDb for exact counts). admin.ts session parts now at 100% stmts/lines; every session-part branch covered. The one remaining session-part branch (row.pausedSeconds ?? 0 in the stop route) is c8-ignored — paused_seconds is NOT NULL DEFAULT 0, so the null branch is DB-impossible. The remaining uncovered branches in admin.ts are the report/export parts, owned by the Reports domain (B-A6). No behavior-changing bugs found; all session-part code was already correct.
This commit is contained in:
68
apps/api/test/integration/admin-worker-session.test.ts
Normal file
68
apps/api/test/integration/admin-worker-session.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
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';
|
||||
|
||||
const WIDE = `from=${new Date('2000-01-01').toISOString()}&to=${new Date('2100-01-01').toISOString()}`;
|
||||
|
||||
describe('admin↔worker session lifecycle (worker starts → admin stops → report + export)', () => {
|
||||
beforeEach(resetDb);
|
||||
|
||||
it('an admin-stopped worker session lands in the report and export with correct totals', async () => {
|
||||
const app = createApp();
|
||||
const adminTok = await authToken(app, 'admin-ws-admin@example.com', 'admin');
|
||||
const workerTok = await authToken(app, 'admin-ws-worker@example.com'); // worker
|
||||
const activityId = await seedActivity('Frezen', ['Kurk']);
|
||||
|
||||
// Worker starts a session.
|
||||
const started = await (
|
||||
await app.request('/api/sessions/start', {
|
||||
method: 'POST',
|
||||
headers: bearer(workerTok),
|
||||
body: JSON.stringify({ activity_id: activityId, insole_type: 'Kurk', pair_count: 3 }),
|
||||
})
|
||||
).json();
|
||||
|
||||
// Back-date the start so the admin stop yields a deterministic-ish worked span.
|
||||
await db
|
||||
.update(workSessions)
|
||||
.set({ startTime: new Date(Date.now() - 120000) })
|
||||
.where(eq(workSessions.id, started.id));
|
||||
|
||||
// Admin stops it via the admin endpoint.
|
||||
const stopRes = await app.request(`/api/admin/sessions/${started.id}/stop`, {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
});
|
||||
expect(stopRes.status).toBe(200);
|
||||
const stopped = await stopRes.json();
|
||||
expect(stopped.status).toBe('completed');
|
||||
expect(stopped.duration_seconds).toBeGreaterThanOrEqual(120);
|
||||
|
||||
// The session shows up in the admin report with totals matching the stored row.
|
||||
const report = await (
|
||||
await app.request(`/api/admin/report?${WIDE}`, { headers: bearer(adminTok) })
|
||||
).json();
|
||||
expect(report.totals.sessions).toBe(1);
|
||||
expect(report.totals.worked_seconds).toBe(stopped.duration_seconds);
|
||||
expect(report.totals.pairs).toBe(3);
|
||||
expect(report.by_worker).toHaveLength(1);
|
||||
expect(report.by_worker[0].user_name).toBe('admin-ws-worker');
|
||||
expect(report.by_worker[0].worked_seconds).toBe(stopped.duration_seconds);
|
||||
expect(report.by_activity).toHaveLength(1);
|
||||
expect(report.by_activity[0].activity_name).toBe('Frezen');
|
||||
expect(report.by_type).toHaveLength(1);
|
||||
expect(report.by_type[0].insole_type).toBe('Kurk');
|
||||
|
||||
// And in the all-users export CSV (header + the one row, with the Worker column).
|
||||
const csvRes = await app.request(`/api/admin/export?${WIDE}`, { headers: bearer(adminTok) });
|
||||
expect(csvRes.status).toBe(200);
|
||||
const lines = (await csvRes.text()).trim().split('\n');
|
||||
expect(lines).toHaveLength(2);
|
||||
expect(lines[0]).toContain('"Worker"');
|
||||
expect(lines[1]).toContain('admin-ws-worker');
|
||||
expect(lines[1]).toContain('Frezen');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user