feat(api): admin manual-session create/edit/stop/discard + users roster
Sub
Phase 3b.1 needs admin-side write paths so an admin can hand-create a
session for a worker, correct any session, and stop/cancel a worker's
stuck active session — duration always derived server-side.
Adds under the existing /api/admin/* admin guard:
- GET /api/admin/users — {id,name,email} roster ordered by name (worker picker)
- POST /api/admin/sessions — manual create -> completed, source=manual,
duration = max(0, round((end-start)/1000) - paused)
- PUT /api/admin/sessions/:id — edit any session (no user reassignment);
recomputes duration when end_time present, else duration null
- POST /api/admin/sessions/:id/stop — fold open pause, end=now, completed
- POST /api/admin/sessions/:id/discard — status=discarded, end=now
Validates end>=start, pair_count>=1, paused<=span, activity/user exist
(400/404); stop/discard on an already-closed session -> 409. Responses
reuse toWorkSession.
Products affected: SoleLog backend (apps/api)
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
import { Hono } from 'hono';
|
||||
import { desc, eq } from 'drizzle-orm';
|
||||
import { asc, desc, eq } from 'drizzle-orm';
|
||||
import { AdminUpdateSessionInput, CreateManualSessionInput } from '@solelog/shared';
|
||||
import { db } from '../db/client';
|
||||
import { activities, user, workSessions } from '../db/schema';
|
||||
import { getSessionUser, isAdmin } from '../lib/require-user';
|
||||
import { toWorkSession } from '../lib/work-session';
|
||||
|
||||
function computeDuration(startMs: number, endMs: number, paused: number): number {
|
||||
return Math.max(0, Math.round((endMs - startMs) / 1000) - paused);
|
||||
}
|
||||
|
||||
export const adminRoutes = new Hono();
|
||||
|
||||
// Gate the whole /api/admin/* surface to admins.
|
||||
@@ -58,3 +63,144 @@ adminRoutes.get('/api/admin/sessions/active', async (c) => {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
// Roster for the create-form worker picker. Direct DB read — no better-auth client dependency.
|
||||
adminRoutes.get('/api/admin/users', async (c) => {
|
||||
const rows = await db
|
||||
.select({ id: user.id, name: user.name, email: user.email })
|
||||
.from(user)
|
||||
.orderBy(asc(user.name));
|
||||
return c.json(rows);
|
||||
});
|
||||
|
||||
// Manual create → always completed, source='manual', duration derived server-side.
|
||||
adminRoutes.post('/api/admin/sessions', async (c) => {
|
||||
const parsed = CreateManualSessionInput.safeParse(await c.req.json().catch(() => null));
|
||||
if (!parsed.success) return c.json({ error: 'Invalid input' }, 400);
|
||||
const d = parsed.data;
|
||||
const start = new Date(d.start_time);
|
||||
const end = new Date(d.end_time);
|
||||
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || end < start)
|
||||
return c.json({ error: 'Invalid input' }, 400);
|
||||
const span = Math.round((end.getTime() - start.getTime()) / 1000);
|
||||
if (d.paused_seconds > span) return c.json({ error: 'Invalid input' }, 400);
|
||||
|
||||
const [u] = await db.select({ id: user.id }).from(user).where(eq(user.id, d.user_id));
|
||||
if (!u) return c.json({ error: 'User not found' }, 404);
|
||||
const [act] = await db.select().from(activities).where(eq(activities.id, d.activity_id));
|
||||
if (!act) return c.json({ error: 'Activity not found' }, 404);
|
||||
|
||||
const [row] = await db
|
||||
.insert(workSessions)
|
||||
.values({
|
||||
userId: d.user_id,
|
||||
activityId: d.activity_id,
|
||||
insoleType: d.insole_type,
|
||||
pairCount: d.pair_count,
|
||||
startTime: start,
|
||||
endTime: end,
|
||||
durationSeconds: computeDuration(start.getTime(), end.getTime(), d.paused_seconds),
|
||||
pausedSeconds: d.paused_seconds,
|
||||
pausedAt: null,
|
||||
status: 'completed',
|
||||
source: 'manual',
|
||||
notes: d.notes ?? null,
|
||||
})
|
||||
.returning();
|
||||
return c.json(toWorkSession(row, { activityName: act.name }));
|
||||
});
|
||||
|
||||
// Edit any session (no user reassignment). Duration recomputed when end present, else null.
|
||||
adminRoutes.put('/api/admin/sessions/:id', async (c) => {
|
||||
const id = Number.parseInt(c.req.param('id'), 10);
|
||||
if (Number.isNaN(id)) return c.json({ error: 'Session not found' }, 404);
|
||||
|
||||
const parsed = AdminUpdateSessionInput.safeParse(await c.req.json().catch(() => null));
|
||||
if (!parsed.success) return c.json({ error: 'Invalid input' }, 400);
|
||||
const d = parsed.data;
|
||||
|
||||
const [row] = await db.select().from(workSessions).where(eq(workSessions.id, id));
|
||||
if (!row) return c.json({ error: 'Session not found' }, 404);
|
||||
|
||||
const start = new Date(d.start_time);
|
||||
if (Number.isNaN(start.getTime())) return c.json({ error: 'Invalid input' }, 400);
|
||||
|
||||
let endTime: Date | null = null;
|
||||
let durationSeconds: number | null = null;
|
||||
if (d.end_time !== null) {
|
||||
const end = new Date(d.end_time);
|
||||
if (Number.isNaN(end.getTime()) || end < start) return c.json({ error: 'Invalid input' }, 400);
|
||||
const span = Math.round((end.getTime() - start.getTime()) / 1000);
|
||||
if (d.paused_seconds > span) return c.json({ error: 'Invalid input' }, 400);
|
||||
endTime = end;
|
||||
durationSeconds = computeDuration(start.getTime(), end.getTime(), d.paused_seconds);
|
||||
}
|
||||
|
||||
const [act] = await db.select().from(activities).where(eq(activities.id, d.activity_id));
|
||||
if (!act) return c.json({ error: 'Activity not found' }, 404);
|
||||
|
||||
const [updated] = await db
|
||||
.update(workSessions)
|
||||
.set({
|
||||
activityId: d.activity_id,
|
||||
insoleType: d.insole_type,
|
||||
pairCount: d.pair_count,
|
||||
startTime: start,
|
||||
endTime,
|
||||
durationSeconds,
|
||||
pausedSeconds: d.paused_seconds,
|
||||
pausedAt: null,
|
||||
status: d.status,
|
||||
notes: d.notes,
|
||||
})
|
||||
.where(eq(workSessions.id, id))
|
||||
.returning();
|
||||
return c.json(toWorkSession(updated, { activityName: act.name }));
|
||||
});
|
||||
|
||||
// Quick stop of an active session: fold any open pause, end=now, completed.
|
||||
adminRoutes.post('/api/admin/sessions/:id/stop', async (c) => {
|
||||
const id = Number.parseInt(c.req.param('id'), 10);
|
||||
if (Number.isNaN(id)) return c.json({ error: 'Session not found' }, 404);
|
||||
|
||||
const [row] = await db.select().from(workSessions).where(eq(workSessions.id, id));
|
||||
if (!row) return c.json({ error: 'Session not found' }, 404);
|
||||
if (row.status !== 'active') return c.json({ error: 'Session already closed' }, 409);
|
||||
|
||||
const now = Date.now();
|
||||
const extraPaused = row.pausedAt
|
||||
? Math.round((now - new Date(row.pausedAt).getTime()) / 1000)
|
||||
: 0;
|
||||
const pausedSeconds = (row.pausedSeconds ?? 0) + extraPaused;
|
||||
const durationSeconds = computeDuration(new Date(row.startTime).getTime(), now, pausedSeconds);
|
||||
|
||||
const [updated] = await db
|
||||
.update(workSessions)
|
||||
.set({
|
||||
endTime: new Date(now),
|
||||
durationSeconds,
|
||||
pausedSeconds,
|
||||
pausedAt: null,
|
||||
status: 'completed',
|
||||
})
|
||||
.where(eq(workSessions.id, id))
|
||||
.returning();
|
||||
return c.json(toWorkSession(updated));
|
||||
});
|
||||
|
||||
// Cancel an active session: status='discarded', end=now (no hard delete).
|
||||
adminRoutes.post('/api/admin/sessions/:id/discard', async (c) => {
|
||||
const id = Number.parseInt(c.req.param('id'), 10);
|
||||
if (Number.isNaN(id)) return c.json({ error: 'Session not found' }, 404);
|
||||
|
||||
const [row] = await db.select().from(workSessions).where(eq(workSessions.id, id));
|
||||
if (!row) return c.json({ error: 'Session not found' }, 404);
|
||||
if (row.status !== 'active') return c.json({ error: 'Session already closed' }, 409);
|
||||
|
||||
const [updated] = await db
|
||||
.update(workSessions)
|
||||
.set({ status: 'discarded', endTime: new Date() })
|
||||
.where(eq(workSessions.id, id))
|
||||
.returning();
|
||||
return c.json(toWorkSession(updated));
|
||||
});
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { createApp } from '../src/app';
|
||||
import { authToken, bearer, seedActivity } from './helpers';
|
||||
import { authToken, bearer, createTestUser, seedActivity } from './helpers';
|
||||
import { db } from '../src/db/client';
|
||||
import { user } from '../src/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
async function userIdByEmail(email: string): Promise<string> {
|
||||
const [row] = await db.select({ id: user.id }).from(user).where(eq(user.email, email));
|
||||
if (!row) throw new Error(`no user for ${email}`);
|
||||
return row.id;
|
||||
}
|
||||
|
||||
describe('admin session views', () => {
|
||||
it('401s without a token', async () => {
|
||||
@@ -44,3 +53,400 @@ describe('admin session views', () => {
|
||||
expect(activeBody.some((s: { id: number }) => s.id === started.id)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin users roster', () => {
|
||||
it('returns id/name/email objects for an admin, ordered by name', async () => {
|
||||
const app = createApp();
|
||||
const adminTok = await authToken(app, 'roster-admin@example.com', 'admin');
|
||||
await createTestUser('roster-w1@example.com');
|
||||
|
||||
const res = await app.request('/api/admin/users', { headers: bearer(adminTok) });
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(Array.isArray(body)).toBe(true);
|
||||
const found = body.find((u: { email: string }) => u.email === 'roster-w1@example.com');
|
||||
expect(found).toBeTruthy();
|
||||
expect(found).toHaveProperty('id');
|
||||
expect(found).toHaveProperty('name');
|
||||
expect(found).toHaveProperty('email');
|
||||
// ordered by name ascending
|
||||
const names = body.map((u: { name: string }) => u.name);
|
||||
const sorted = [...names].sort();
|
||||
expect(names).toEqual(sorted);
|
||||
});
|
||||
|
||||
it('403s for a worker', async () => {
|
||||
const app = createApp();
|
||||
const workerTok = await authToken(app, 'roster-worker@example.com'); // worker
|
||||
expect((await app.request('/api/admin/users', { headers: bearer(workerTok) })).status).toBe(
|
||||
403,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin manual session create', () => {
|
||||
it('creates a completed manual session with duration excluding paused', async () => {
|
||||
const app = createApp();
|
||||
const adminTok = await authToken(app, 'create-admin@example.com', 'admin');
|
||||
await createTestUser('create-target@example.com');
|
||||
const targetId = await userIdByEmail('create-target@example.com');
|
||||
const activityId = await seedActivity('Snijden');
|
||||
|
||||
const start = new Date('2026-06-17T08:00:00.000Z');
|
||||
const end = new Date('2026-06-17T09:00:00.000Z'); // 1h apart = 3600s
|
||||
const res = await app.request('/api/admin/sessions', {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
body: JSON.stringify({
|
||||
user_id: targetId,
|
||||
activity_id: activityId,
|
||||
insole_type: 'Kurk',
|
||||
pair_count: 3,
|
||||
start_time: start.toISOString(),
|
||||
end_time: end.toISOString(),
|
||||
paused_seconds: 600,
|
||||
notes: 'handmatig',
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.source).toBe('manual');
|
||||
expect(body.status).toBe('completed');
|
||||
expect(body.duration_seconds).toBe(3000); // 3600 - 600
|
||||
expect(body.paused_seconds).toBe(600);
|
||||
expect(body.paused_at).toBeNull();
|
||||
expect(body.user_id).toBe(targetId);
|
||||
expect(body.activity_name).toBe('Snijden');
|
||||
});
|
||||
|
||||
it('400s when end < start', async () => {
|
||||
const app = createApp();
|
||||
const adminTok = await authToken(app, 'create-badtime-admin@example.com', 'admin');
|
||||
await createTestUser('create-badtime-target@example.com');
|
||||
const targetId = await userIdByEmail('create-badtime-target@example.com');
|
||||
const activityId = await seedActivity('Frezen');
|
||||
|
||||
const res = await app.request('/api/admin/sessions', {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
body: JSON.stringify({
|
||||
user_id: targetId,
|
||||
activity_id: activityId,
|
||||
insole_type: 'Kurk',
|
||||
pair_count: 1,
|
||||
start_time: '2026-06-17T09:00:00.000Z',
|
||||
end_time: '2026-06-17T08:00:00.000Z',
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it('404s for an unknown user', async () => {
|
||||
const app = createApp();
|
||||
const adminTok = await authToken(app, 'create-nouser-admin@example.com', 'admin');
|
||||
const activityId = await seedActivity('Lijmen');
|
||||
|
||||
const res = await app.request('/api/admin/sessions', {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
body: JSON.stringify({
|
||||
user_id: 'does-not-exist',
|
||||
activity_id: activityId,
|
||||
insole_type: 'Kurk',
|
||||
pair_count: 1,
|
||||
start_time: '2026-06-17T08:00:00.000Z',
|
||||
end_time: '2026-06-17T09:00:00.000Z',
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it('404s for an unknown activity', async () => {
|
||||
const app = createApp();
|
||||
const adminTok = await authToken(app, 'create-noact-admin@example.com', 'admin');
|
||||
await createTestUser('create-noact-target@example.com');
|
||||
const targetId = await userIdByEmail('create-noact-target@example.com');
|
||||
|
||||
const res = await app.request('/api/admin/sessions', {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
body: JSON.stringify({
|
||||
user_id: targetId,
|
||||
activity_id: 999999,
|
||||
insole_type: 'Kurk',
|
||||
pair_count: 1,
|
||||
start_time: '2026-06-17T08:00:00.000Z',
|
||||
end_time: '2026-06-17T09:00:00.000Z',
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it('403s for a worker', async () => {
|
||||
const app = createApp();
|
||||
const workerTok = await authToken(app, 'create-worker@example.com'); // worker
|
||||
const res = await app.request('/api/admin/sessions', {
|
||||
method: 'POST',
|
||||
headers: bearer(workerTok),
|
||||
body: JSON.stringify({
|
||||
user_id: 'x',
|
||||
activity_id: 1,
|
||||
insole_type: 'Kurk',
|
||||
pair_count: 1,
|
||||
start_time: '2026-06-17T08:00:00.000Z',
|
||||
end_time: '2026-06-17T09:00:00.000Z',
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin session edit', () => {
|
||||
it('recomputes duration on edit', async () => {
|
||||
const app = createApp();
|
||||
const adminTok = await authToken(app, 'edit-admin@example.com', 'admin');
|
||||
await createTestUser('edit-target@example.com');
|
||||
const targetId = await userIdByEmail('edit-target@example.com');
|
||||
const activityId = await seedActivity('Polijsten');
|
||||
|
||||
const created = await (
|
||||
await app.request('/api/admin/sessions', {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
body: JSON.stringify({
|
||||
user_id: targetId,
|
||||
activity_id: activityId,
|
||||
insole_type: 'Kurk',
|
||||
pair_count: 2,
|
||||
start_time: '2026-06-17T08:00:00.000Z',
|
||||
end_time: '2026-06-17T09:00:00.000Z',
|
||||
paused_seconds: 0,
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
expect(created.duration_seconds).toBe(3600);
|
||||
|
||||
const res = await app.request(`/api/admin/sessions/${created.id}`, {
|
||||
method: 'PUT',
|
||||
headers: bearer(adminTok),
|
||||
body: JSON.stringify({
|
||||
activity_id: activityId,
|
||||
insole_type: 'Berk',
|
||||
pair_count: 4,
|
||||
start_time: '2026-06-17T08:00:00.000Z',
|
||||
end_time: '2026-06-17T10:00:00.000Z', // now 2h
|
||||
paused_seconds: 300,
|
||||
notes: 'gecorrigeerd',
|
||||
status: 'completed',
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.duration_seconds).toBe(7200 - 300);
|
||||
expect(body.insole_type).toBe('Berk');
|
||||
expect(body.pair_count).toBe(4);
|
||||
expect(body.notes).toBe('gecorrigeerd');
|
||||
});
|
||||
|
||||
it('sets duration null when end_time is null (active)', async () => {
|
||||
const app = createApp();
|
||||
const adminTok = await authToken(app, 'edit-active-admin@example.com', 'admin');
|
||||
await createTestUser('edit-active-target@example.com');
|
||||
const targetId = await userIdByEmail('edit-active-target@example.com');
|
||||
const activityId = await seedActivity('Stikken');
|
||||
|
||||
const created = await (
|
||||
await app.request('/api/admin/sessions', {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
body: JSON.stringify({
|
||||
user_id: targetId,
|
||||
activity_id: activityId,
|
||||
insole_type: 'Kurk',
|
||||
pair_count: 2,
|
||||
start_time: '2026-06-17T08:00:00.000Z',
|
||||
end_time: '2026-06-17T09:00:00.000Z',
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
const res = await app.request(`/api/admin/sessions/${created.id}`, {
|
||||
method: 'PUT',
|
||||
headers: bearer(adminTok),
|
||||
body: JSON.stringify({
|
||||
activity_id: activityId,
|
||||
insole_type: 'Kurk',
|
||||
pair_count: 2,
|
||||
start_time: '2026-06-17T08:00:00.000Z',
|
||||
end_time: null,
|
||||
paused_seconds: 0,
|
||||
notes: null,
|
||||
status: 'active',
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.duration_seconds).toBeNull();
|
||||
expect(body.end_time).toBeNull();
|
||||
expect(body.status).toBe('active');
|
||||
});
|
||||
|
||||
it('400s when end < start on edit', async () => {
|
||||
const app = createApp();
|
||||
const adminTok = await authToken(app, 'edit-badtime-admin@example.com', 'admin');
|
||||
await createTestUser('edit-badtime-target@example.com');
|
||||
const targetId = await userIdByEmail('edit-badtime-target@example.com');
|
||||
const activityId = await seedActivity('Wassen');
|
||||
|
||||
const created = await (
|
||||
await app.request('/api/admin/sessions', {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
body: JSON.stringify({
|
||||
user_id: targetId,
|
||||
activity_id: activityId,
|
||||
insole_type: 'Kurk',
|
||||
pair_count: 2,
|
||||
start_time: '2026-06-17T08:00:00.000Z',
|
||||
end_time: '2026-06-17T09:00:00.000Z',
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
const res = await app.request(`/api/admin/sessions/${created.id}`, {
|
||||
method: 'PUT',
|
||||
headers: bearer(adminTok),
|
||||
body: JSON.stringify({
|
||||
activity_id: activityId,
|
||||
insole_type: 'Kurk',
|
||||
pair_count: 2,
|
||||
start_time: '2026-06-17T09:00:00.000Z',
|
||||
end_time: '2026-06-17T08:00:00.000Z',
|
||||
paused_seconds: 0,
|
||||
notes: null,
|
||||
status: 'completed',
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
it('404s for an unknown session', async () => {
|
||||
const app = createApp();
|
||||
const adminTok = await authToken(app, 'edit-404-admin@example.com', 'admin');
|
||||
const activityId = await seedActivity('Drogen');
|
||||
|
||||
const res = await app.request('/api/admin/sessions/999999', {
|
||||
method: 'PUT',
|
||||
headers: bearer(adminTok),
|
||||
body: JSON.stringify({
|
||||
activity_id: activityId,
|
||||
insole_type: 'Kurk',
|
||||
pair_count: 2,
|
||||
start_time: '2026-06-17T08:00:00.000Z',
|
||||
end_time: '2026-06-17T09:00:00.000Z',
|
||||
paused_seconds: 0,
|
||||
notes: null,
|
||||
status: 'completed',
|
||||
}),
|
||||
});
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin stop/discard another user session', () => {
|
||||
it("stops another worker's active session and computes duration", async () => {
|
||||
const app = createApp();
|
||||
const adminTok = await authToken(app, 'stop-admin@example.com', 'admin');
|
||||
const workerTok = await authToken(app, 'stop-worker@example.com'); // worker
|
||||
const activityId = await seedActivity('Frezen');
|
||||
|
||||
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: 2 }),
|
||||
})
|
||||
).json();
|
||||
|
||||
const res = await app.request(`/api/admin/sessions/${started.id}/stop`, {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.status).toBe('completed');
|
||||
expect(body.end_time).not.toBeNull();
|
||||
expect(body.duration_seconds).toBeGreaterThanOrEqual(0);
|
||||
expect(body.paused_at).toBeNull();
|
||||
});
|
||||
|
||||
it("discards another worker's active session", async () => {
|
||||
const app = createApp();
|
||||
const adminTok = await authToken(app, 'discard-admin@example.com', 'admin');
|
||||
const workerTok = await authToken(app, 'discard-worker@example.com'); // worker
|
||||
const activityId = await seedActivity('Lijmen');
|
||||
|
||||
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: 2 }),
|
||||
})
|
||||
).json();
|
||||
|
||||
const res = await app.request(`/api/admin/sessions/${started.id}/discard`, {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.status).toBe('discarded');
|
||||
expect(body.end_time).not.toBeNull();
|
||||
});
|
||||
|
||||
it('409s when stopping an already-closed session', async () => {
|
||||
const app = createApp();
|
||||
const adminTok = await authToken(app, 'stop-closed-admin@example.com', 'admin');
|
||||
const workerTok = await authToken(app, 'stop-closed-worker@example.com');
|
||||
const activityId = await seedActivity('Snijden');
|
||||
|
||||
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: 2 }),
|
||||
})
|
||||
).json();
|
||||
await app.request(`/api/admin/sessions/${started.id}/discard`, {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
});
|
||||
|
||||
const res = await app.request(`/api/admin/sessions/${started.id}/stop`, {
|
||||
method: 'POST',
|
||||
headers: bearer(adminTok),
|
||||
});
|
||||
expect(res.status).toBe(409);
|
||||
});
|
||||
|
||||
it('403s for a worker hitting stop/discard', async () => {
|
||||
const app = createApp();
|
||||
const workerTok = await authToken(app, 'stop-worker-gate@example.com'); // worker
|
||||
expect(
|
||||
(
|
||||
await app.request('/api/admin/sessions/1/stop', {
|
||||
method: 'POST',
|
||||
headers: bearer(workerTok),
|
||||
})
|
||||
).status,
|
||||
).toBe(403);
|
||||
expect(
|
||||
(
|
||||
await app.request('/api/admin/sessions/1/discard', {
|
||||
method: 'POST',
|
||||
headers: bearer(workerTok),
|
||||
})
|
||||
).status,
|
||||
).toBe(403);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user