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.
717 lines
24 KiB
TypeScript
717 lines
24 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import type { Hono } from 'hono';
|
|
import { createApp } from '../src/app';
|
|
import { authToken, bearer, createTestUser, seedActivity } from './helpers';
|
|
import { db } from '../src/db/client';
|
|
import { user, workSessions } 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;
|
|
}
|
|
|
|
// Create a 1h completed manual session and return its row (used to exercise the edit path).
|
|
async function seedManualSession(
|
|
app: Hono,
|
|
adminTok: string,
|
|
targetId: string,
|
|
activityId: number,
|
|
): Promise<{ id: number }> {
|
|
return (
|
|
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();
|
|
}
|
|
|
|
describe('admin session views', () => {
|
|
it('401s without a token', async () => {
|
|
const app = createApp();
|
|
expect((await app.request('/api/admin/sessions')).status).toBe(401);
|
|
expect((await app.request('/api/admin/sessions/active')).status).toBe(401);
|
|
});
|
|
|
|
it('403s for a worker', async () => {
|
|
const app = createApp();
|
|
const token = await authToken(app, 'admin-view-worker@example.com'); // worker
|
|
expect((await app.request('/api/admin/sessions', { headers: bearer(token) })).status).toBe(403);
|
|
});
|
|
|
|
it("returns ALL users' sessions for an admin, with user info", async () => {
|
|
const app = createApp();
|
|
const adminTok = await authToken(app, 'admin-view-admin@example.com', 'admin');
|
|
const workerTok = await authToken(app, 'admin-view-w2@example.com'); // worker
|
|
const activityId = await seedActivity('Frezen');
|
|
|
|
// 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: 2 }),
|
|
})
|
|
).json();
|
|
|
|
const res = await app.request('/api/admin/sessions', { headers: bearer(adminTok) });
|
|
expect(res.status).toBe(200);
|
|
const body = await res.json();
|
|
const found = body.find((s: { id: number }) => s.id === started.id);
|
|
expect(found).toBeTruthy();
|
|
expect(found.user_email).toBe('admin-view-w2@example.com');
|
|
expect(found.activity_name).toBe('Frezen');
|
|
|
|
const active = await app.request('/api/admin/sessions/active', { headers: bearer(adminTok) });
|
|
expect(active.status).toBe(200);
|
|
const activeBody = await active.json();
|
|
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('400s when the body fails schema validation', async () => {
|
|
const app = createApp();
|
|
const adminTok = await authToken(app, 'create-badbody-admin@example.com', 'admin');
|
|
const res = await app.request('/api/admin/sessions', {
|
|
method: 'POST',
|
|
headers: bearer(adminTok),
|
|
body: JSON.stringify({ pair_count: 0 }), // missing required fields + invalid pair_count
|
|
});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it('400s when paused_seconds exceeds the worked span', async () => {
|
|
const app = createApp();
|
|
const adminTok = await authToken(app, 'create-pausedbig-admin@example.com', 'admin');
|
|
await createTestUser('create-pausedbig-target@example.com');
|
|
const targetId = await userIdByEmail('create-pausedbig-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-17T08:00:00.000Z',
|
|
end_time: '2026-06-17T09:00:00.000Z', // 3600s span
|
|
paused_seconds: 4000, // > span
|
|
}),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
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 a non-numeric session id', async () => {
|
|
const app = createApp();
|
|
const adminTok = await authToken(app, 'edit-nan-admin@example.com', 'admin');
|
|
const res = await app.request('/api/admin/sessions/not-a-number', {
|
|
method: 'PUT',
|
|
headers: bearer(adminTok),
|
|
body: JSON.stringify({
|
|
activity_id: 1,
|
|
insole_type: 'Kurk',
|
|
pair_count: 1,
|
|
start_time: '2026-06-17T08:00:00.000Z',
|
|
end_time: null,
|
|
paused_seconds: 0,
|
|
notes: null,
|
|
status: 'active',
|
|
}),
|
|
});
|
|
expect(res.status).toBe(404);
|
|
});
|
|
|
|
it('400s when the body fails schema validation', async () => {
|
|
const app = createApp();
|
|
const adminTok = await authToken(app, 'edit-badbody-admin@example.com', 'admin');
|
|
const res = await app.request('/api/admin/sessions/1', {
|
|
method: 'PUT',
|
|
headers: bearer(adminTok),
|
|
body: JSON.stringify({ pair_count: 0 }),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it('400s when start_time is not a valid date', async () => {
|
|
const app = createApp();
|
|
const adminTok = await authToken(app, 'edit-badstart-admin@example.com', 'admin');
|
|
await createTestUser('edit-badstart-target@example.com');
|
|
const targetId = await userIdByEmail('edit-badstart-target@example.com');
|
|
const activityId = await seedActivity('Frezen');
|
|
const created = await seedManualSession(app, adminTok, targetId, activityId);
|
|
|
|
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: 'not-a-date',
|
|
end_time: null,
|
|
paused_seconds: 0,
|
|
notes: null,
|
|
status: 'active',
|
|
}),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it('400s when paused_seconds exceeds the span on edit', async () => {
|
|
const app = createApp();
|
|
const adminTok = await authToken(app, 'edit-pausedbig-admin@example.com', 'admin');
|
|
await createTestUser('edit-pausedbig-target@example.com');
|
|
const targetId = await userIdByEmail('edit-pausedbig-target@example.com');
|
|
const activityId = await seedActivity('Lijmen');
|
|
const created = await seedManualSession(app, adminTok, targetId, activityId);
|
|
|
|
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: '2026-06-17T09:00:00.000Z', // 3600s span
|
|
paused_seconds: 5000, // > span
|
|
notes: null,
|
|
status: 'completed',
|
|
}),
|
|
});
|
|
expect(res.status).toBe(400);
|
|
});
|
|
|
|
it('404s for an unknown activity on edit', async () => {
|
|
const app = createApp();
|
|
const adminTok = await authToken(app, 'edit-noact-admin@example.com', 'admin');
|
|
await createTestUser('edit-noact-target@example.com');
|
|
const targetId = await userIdByEmail('edit-noact-target@example.com');
|
|
const activityId = await seedActivity('Snijden');
|
|
const created = await seedManualSession(app, adminTok, targetId, activityId);
|
|
|
|
const res = await app.request(`/api/admin/sessions/${created.id}`, {
|
|
method: 'PUT',
|
|
headers: bearer(adminTok),
|
|
body: JSON.stringify({
|
|
activity_id: 999999,
|
|
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);
|
|
});
|
|
|
|
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('404s for a non-numeric or unknown stop id', async () => {
|
|
const app = createApp();
|
|
const adminTok = await authToken(app, 'stop-404-admin@example.com', 'admin');
|
|
expect(
|
|
(
|
|
await app.request('/api/admin/sessions/not-a-number/stop', {
|
|
method: 'POST',
|
|
headers: bearer(adminTok),
|
|
})
|
|
).status,
|
|
).toBe(404);
|
|
expect(
|
|
(
|
|
await app.request('/api/admin/sessions/999999/stop', {
|
|
method: 'POST',
|
|
headers: bearer(adminTok),
|
|
})
|
|
).status,
|
|
).toBe(404);
|
|
});
|
|
|
|
it('folds an open pause into paused_seconds when stopping', async () => {
|
|
const app = createApp();
|
|
const adminTok = await authToken(app, 'stop-paused-admin@example.com', 'admin');
|
|
const workerTok = await authToken(app, 'stop-paused-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();
|
|
|
|
// Back-date the start (20s) and leave an open pause (6s ago) so the admin stop
|
|
// folds the running pause into paused_seconds and excludes it from worked time.
|
|
await db
|
|
.update(workSessions)
|
|
.set({ startTime: new Date(Date.now() - 20000), pausedAt: new Date(Date.now() - 6000) })
|
|
.where(eq(workSessions.id, started.id));
|
|
|
|
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.paused_at).toBeNull();
|
|
expect(body.paused_seconds).toBeGreaterThanOrEqual(6); // the open pause was folded in
|
|
expect(body.duration_seconds).toBeLessThan(20); // paused time excluded from worked
|
|
});
|
|
|
|
it('404s for a non-numeric or unknown discard id', async () => {
|
|
const app = createApp();
|
|
const adminTok = await authToken(app, 'discard-404-admin@example.com', 'admin');
|
|
expect(
|
|
(
|
|
await app.request('/api/admin/sessions/not-a-number/discard', {
|
|
method: 'POST',
|
|
headers: bearer(adminTok),
|
|
})
|
|
).status,
|
|
).toBe(404);
|
|
expect(
|
|
(
|
|
await app.request('/api/admin/sessions/999999/discard', {
|
|
method: 'POST',
|
|
headers: bearer(adminTok),
|
|
})
|
|
).status,
|
|
).toBe(404);
|
|
});
|
|
|
|
it('409s when discarding an already-closed session', async () => {
|
|
const app = createApp();
|
|
const adminTok = await authToken(app, 'discard-closed-admin@example.com', 'admin');
|
|
const workerTok = await authToken(app, 'discard-closed-worker@example.com'); // worker
|
|
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}/stop`, {
|
|
method: 'POST',
|
|
headers: bearer(adminTok),
|
|
});
|
|
|
|
const res = await app.request(`/api/admin/sessions/${started.id}/discard`, {
|
|
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);
|
|
});
|
|
});
|