diff --git a/apps/api/test/activities.test.ts b/apps/api/test/activities.test.ts index acb2161..ed4a7f9 100644 --- a/apps/api/test/activities.test.ts +++ b/apps/api/test/activities.test.ts @@ -1,34 +1,12 @@ 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'; +import { authToken, bearer } from './helpers'; const json = { 'content-type': 'application/json' }; -// Sign up + sign in a user, returning the bearer token. -async function authToken(app: Hono, email: string): Promise { - 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 { - return { authorization: `Bearer ${token}`, 'content-type': 'application/json' }; -} - describe('activities routes', () => { it('401s GET /api/activities without a token', async () => { const app = createApp(); diff --git a/apps/api/test/export.test.ts b/apps/api/test/export.test.ts index d866f6c..73973b9 100644 --- a/apps/api/test/export.test.ts +++ b/apps/api/test/export.test.ts @@ -5,30 +5,7 @@ import { db } from '../src/db/client'; import { workSessions } from '../src/db/schema'; import { eq } from 'drizzle-orm'; import { quote, formatDuration } from '../src/lib/csv'; - -const json = { 'content-type': 'application/json' }; - -// Sign up + sign in a user, returning the bearer token. -async function authToken(app: Hono, email: string): Promise { - 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 { - return { authorization: `Bearer ${token}`, 'content-type': 'application/json' }; -} +import { authToken, bearer } from './helpers'; async function createActivity(app: Hono, token: string, name: string): Promise { const res = await app.request('/api/activities', { diff --git a/apps/api/test/helpers.ts b/apps/api/test/helpers.ts new file mode 100644 index 0000000..1cf46ec --- /dev/null +++ b/apps/api/test/helpers.ts @@ -0,0 +1,54 @@ +import type { Hono } from 'hono'; +import { auth } from '../src/auth'; +import { db } from '../src/db/client'; +import { activities } from '../src/db/schema'; + +const PASSWORD = 'sterk-wachtwoord-123'; +const json = { 'content-type': 'application/json' }; + +// Create a user server-side. Prefer the admin plugin's createUser (bypasses +// `disableSignUp` and sets the role); fall back to signUpEmail while the admin +// plugin isn't wired yet (sign-up is still open at that point). Either way the +// test no longer depends on the public sign-up *route*. +export async function createTestUser(email: string, role: 'worker' | 'admin' = 'worker') { + const api = auth.api as { + createUser?: (args: { + body: { email: string; password: string; name: string; role: 'worker' | 'admin' }; + }) => Promise; + }; + const name = email.split('@')[0] || 'User'; + if (typeof api.createUser === 'function') { + await api.createUser({ body: { email, password: PASSWORD, name, role } }); + } else { + await auth.api.signUpEmail({ body: { email, password: PASSWORD, name } }); + } +} + +export async function authToken( + app: Hono, + email: string, + role: 'worker' | 'admin' = 'worker' +): Promise { + await createTestUser(email, role); + const signin = await app.request('/api/auth/sign-in/email', { + method: 'POST', + headers: json, + body: JSON.stringify({ email, password: PASSWORD }), + }); + const token = signin.headers.get('set-auth-token'); + if (!token) throw new Error('no token'); + return token; +} + +export function bearer(token: string): Record { + return { authorization: `Bearer ${token}`, 'content-type': 'application/json' }; +} + +// Insert an activity straight into the DB (test setup that should not depend on authz). +export async function seedActivity( + name: string, + insoleTypes: string[] = ['Kurk', 'Berk', '3D'] +): Promise { + const [row] = await db.insert(activities).values({ name, insoleTypes }).returning(); + return row.id; +} diff --git a/apps/api/test/me.test.ts b/apps/api/test/me.test.ts index 248be8b..f773936 100644 --- a/apps/api/test/me.test.ts +++ b/apps/api/test/me.test.ts @@ -1,7 +1,6 @@ import { describe, it, expect } from 'vitest'; import { createApp } from '../src/app'; - -const json = { 'content-type': 'application/json' }; +import { authToken, bearer } from './helpers'; describe('GET /api/me', () => { it('rejects an unauthenticated request', async () => { @@ -10,27 +9,14 @@ describe('GET /api/me', () => { expect(res.status).toBe(401); }); - it('returns the user for a valid bearer token (sign-up -> sign-in -> me)', async () => { + it('returns the user for a valid bearer token (create -> sign-in -> me)', async () => { const app = createApp(); - const creds = { email: 'me@example.com', password: 'sterk-wachtwoord-123', name: 'Me' }; + const email = 'me@example.com'; + const token = await authToken(app, email); - await app.request('/api/auth/sign-up/email', { - method: 'POST', - headers: json, - body: JSON.stringify(creds), - }); - const signin = await app.request('/api/auth/sign-in/email', { - method: 'POST', - headers: json, - body: JSON.stringify({ email: creds.email, password: creds.password }), - }); - const token = signin.headers.get('set-auth-token'); - - const res = await app.request('/api/me', { - headers: { authorization: `Bearer ${token}` }, - }); + const res = await app.request('/api/me', { headers: bearer(token) }); expect(res.status).toBe(200); const body = await res.json(); - expect(body.user.email).toBe(creds.email); + expect(body.user.email).toBe(email); }); }); diff --git a/apps/api/test/sessions.test.ts b/apps/api/test/sessions.test.ts index 9303ddd..6955d71 100644 --- a/apps/api/test/sessions.test.ts +++ b/apps/api/test/sessions.test.ts @@ -1,45 +1,12 @@ import { describe, it, expect } from 'vitest'; -import type { Hono } from 'hono'; import { createApp } from '../src/app'; import { db } from '../src/db/client'; import { workSessions } from '../src/db/schema'; import { eq } from 'drizzle-orm'; +import { authToken, bearer, seedActivity } from './helpers'; const json = { 'content-type': 'application/json' }; -// Sign up + sign in a user, returning the bearer token. -async function authToken(app: Hono, email: string): Promise { - 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 { - return { authorization: `Bearer ${token}`, 'content-type': 'application/json' }; -} - -// Create an activity via the API and return its id. -async function createActivity(app: Hono, token: string, name: string): Promise { - const res = await app.request('/api/activities', { - method: 'POST', - headers: bearer(token), - body: JSON.stringify({ name, insole_types: ['Kurk', 'Berk', '3D'] }), - }); - const body = await res.json(); - return body.id as number; -} - describe('session lifecycle', () => { it('401s start/stop/discard without a token', async () => { const app = createApp(); @@ -61,7 +28,7 @@ describe('session lifecycle', () => { it('starts an active session', async () => { const app = createApp(); const token = await authToken(app, 'sess-start@example.com'); - const activityId = await createActivity(app, token, 'Frezen'); + const activityId = await seedActivity('Frezen'); const res = await app.request('/api/sessions/start', { method: 'POST', @@ -109,7 +76,7 @@ describe('session lifecycle', () => { it('completes a session and computes duration', async () => { const app = createApp(); const token = await authToken(app, 'sess-complete@example.com'); - const activityId = await createActivity(app, token, 'Slijpen'); + const activityId = await seedActivity('Slijpen'); const startRes = await app.request('/api/sessions/start', { method: 'POST', @@ -138,7 +105,7 @@ describe('session lifecycle', () => { it('409s stopping an already-completed session', async () => { const app = createApp(); const token = await authToken(app, 'sess-doublestop@example.com'); - const activityId = await createActivity(app, token, 'Bekleden'); + const activityId = await seedActivity('Bekleden'); const startRes = await app.request('/api/sessions/start', { method: 'POST', @@ -163,7 +130,7 @@ describe('session lifecycle', () => { it('discards an active session', async () => { const app = createApp(); const token = await authToken(app, 'sess-discard@example.com'); - const activityId = await createActivity(app, token, 'Afwerken'); + const activityId = await seedActivity('Afwerken'); const startRes = await app.request('/api/sessions/start', { method: 'POST', @@ -186,7 +153,7 @@ describe('session lifecycle', () => { const app = createApp(); const tokenA = await authToken(app, 'sess-ownerA@example.com'); const tokenB = await authToken(app, 'sess-ownerB@example.com'); - const activityId = await createActivity(app, tokenA, 'Printen'); + const activityId = await seedActivity('Printen'); const startRes = await app.request('/api/sessions/start', { method: 'POST', @@ -222,8 +189,8 @@ describe('session reads', () => { it("returns the user's sessions joined with activity name, newest first", async () => { const app = createApp(); const token = await authToken(app, 'reads-history@example.com'); - const frezenId = await createActivity(app, token, 'Frezen'); - const slijpenId = await createActivity(app, token, 'Slijpen'); + const frezenId = await seedActivity('Frezen'); + const slijpenId = await seedActivity('Slijpen'); const firstRes = await app.request('/api/sessions/start', { method: 'POST', @@ -263,7 +230,7 @@ describe('session reads', () => { const app = createApp(); const tokenA = await authToken(app, 'reads-scopeA@example.com'); const tokenB = await authToken(app, 'reads-scopeB@example.com'); - const activityId = await createActivity(app, tokenA, 'Bekleden'); + const activityId = await seedActivity('Bekleden'); const startRes = await app.request('/api/sessions/start', { method: 'POST', @@ -282,7 +249,7 @@ describe('session reads', () => { it('returns only active sessions from /api/sessions/active', async () => { const app = createApp(); const token = await authToken(app, 'reads-active@example.com'); - const activityId = await createActivity(app, token, 'Afwerken'); + const activityId = await seedActivity('Afwerken'); // One session stays active. const activeRes = await app.request('/api/sessions/start', {