Files
solelog/apps/api/test/activities.test.ts

199 lines
6.6 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, user } from '../src/db/schema';
import { eq } from 'drizzle-orm';
const json = { 'content-type': 'application/json' };
// Sign up + sign in a user, returning the bearer token.
async function authToken(app: Hono, email: string): Promise<string> {
const password = 'sterk-wachtwoord-123';
await app.request('/api/auth/sign-up/email', {
method: 'POST',
headers: json,
body: JSON.stringify({ email, password, name: email.split('@')[0] }),
});
const signin = await app.request('/api/auth/sign-in/email', {
method: 'POST',
headers: json,
body: JSON.stringify({ email, password }),
});
const token = signin.headers.get('set-auth-token');
if (!token) throw new Error('no token');
return token;
}
function bearer(token: string): Record<string, string> {
return { authorization: `Bearer ${token}`, 'content-type': 'application/json' };
}
describe('activities routes', () => {
it('401s GET /api/activities without a token', async () => {
const app = createApp();
const res = await app.request('/api/activities');
expect(res.status).toBe(401);
});
it('401s POST /api/activities without a token', async () => {
const app = createApp();
const res = await app.request('/api/activities', {
method: 'POST',
headers: json,
body: JSON.stringify({ name: 'Frezen', insole_types: ['Kurk'] }),
});
expect(res.status).toBe(401);
});
it('creates an activity and lists it', async () => {
const app = createApp();
const token = await authToken(app, 'act-create@example.com');
const createRes = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Frezen', insole_types: ['Kurk', 'Berk'] }),
});
expect(createRes.status).toBe(200);
const created = await createRes.json();
expect(created.name).toBe('Frezen');
expect(created.insole_types).toEqual(['Kurk', 'Berk']);
expect(typeof created.id).toBe('number');
expect(typeof created.created_at).toBe('string');
const listRes = await app.request('/api/activities', { headers: bearer(token) });
expect(listRes.status).toBe(200);
const list = await listRes.json();
expect(Array.isArray(list)).toBe(true);
expect(list.some((a: { id: number }) => a.id === created.id)).toBe(true);
});
it('defaults insole_types to all three when omitted', async () => {
const app = createApp();
const token = await authToken(app, 'act-default@example.com');
const res = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Slijpen' }),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.insole_types).toEqual(['Kurk', 'Berk', '3D']);
});
it('filters by ?insole_type', async () => {
const app = createApp();
const token = await authToken(app, 'act-filter@example.com');
const printRes = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Printen-only-3D', insole_types: ['3D'] }),
});
const print = await printRes.json();
const corkRes = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Kurk-only', insole_types: ['Kurk'] }),
});
const cork = await corkRes.json();
const res = await app.request('/api/activities?insole_type=3D', { headers: bearer(token) });
expect(res.status).toBe(200);
const list: Array<{ id: number }> = await res.json();
expect(list.some((a) => a.id === print.id)).toBe(true);
expect(list.some((a) => a.id === cork.id)).toBe(false);
});
it('400s POST with an empty name', async () => {
const app = createApp();
const token = await authToken(app, 'act-emptyname@example.com');
const res = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: ' ', insole_types: ['Kurk'] }),
});
expect(res.status).toBe(400);
});
it('updates an activity', async () => {
const app = createApp();
const token = await authToken(app, 'act-update@example.com');
const createRes = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'Oud', insole_types: ['Kurk'] }),
});
const created = await createRes.json();
const res = await app.request(`/api/activities/${created.id}`, {
method: 'PUT',
headers: bearer(token),
body: JSON.stringify({ name: 'Nieuw', insole_types: ['Berk', '3D'] }),
});
expect(res.status).toBe(200);
const updated = await res.json();
expect(updated.id).toBe(created.id);
expect(updated.name).toBe('Nieuw');
expect(updated.insole_types).toEqual(['Berk', '3D']);
});
it('404s PUT for a missing id', async () => {
const app = createApp();
const token = await authToken(app, 'act-put404@example.com');
const res = await app.request('/api/activities/999999', {
method: 'PUT',
headers: bearer(token),
body: JSON.stringify({ name: 'X', insole_types: ['Kurk'] }),
});
expect(res.status).toBe(404);
});
it('deletes an activity and its sessions', async () => {
const app = createApp();
const token = await authToken(app, 'act-delete@example.com');
const createRes = await app.request('/api/activities', {
method: 'POST',
headers: bearer(token),
body: JSON.stringify({ name: 'TeVerwijderen', insole_types: ['Kurk'] }),
});
const created = await createRes.json();
// Find the test user's id to attach a work_sessions row directly.
const [u] = await db.select().from(user).where(eq(user.email, 'act-delete@example.com'));
expect(u).toBeTruthy();
await db.insert(workSessions).values({
userId: u.id,
activityId: created.id,
startTime: new Date(),
});
const before = await db
.select()
.from(workSessions)
.where(eq(workSessions.activityId, created.id));
expect(before.length).toBe(1);
const res = await app.request(`/api/activities/${created.id}`, {
method: 'DELETE',
headers: bearer(token),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body).toEqual({ success: true });
const after = await db
.select()
.from(workSessions)
.where(eq(workSessions.activityId, created.id));
expect(after.length).toBe(0);
});
});