test(api): centralize auth helpers on server-side createUser
This commit is contained in:
@@ -1,34 +1,12 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import type { Hono } from 'hono';
|
|
||||||
import { createApp } from '../src/app';
|
import { createApp } from '../src/app';
|
||||||
import { db } from '../src/db/client';
|
import { db } from '../src/db/client';
|
||||||
import { workSessions, user } from '../src/db/schema';
|
import { workSessions, user } from '../src/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { authToken, bearer } from './helpers';
|
||||||
|
|
||||||
const json = { 'content-type': 'application/json' };
|
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', () => {
|
describe('activities routes', () => {
|
||||||
it('401s GET /api/activities without a token', async () => {
|
it('401s GET /api/activities without a token', async () => {
|
||||||
const app = createApp();
|
const app = createApp();
|
||||||
|
|||||||
@@ -5,30 +5,7 @@ import { db } from '../src/db/client';
|
|||||||
import { workSessions } from '../src/db/schema';
|
import { workSessions } from '../src/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { quote, formatDuration } from '../src/lib/csv';
|
import { quote, formatDuration } from '../src/lib/csv';
|
||||||
|
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<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' };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createActivity(app: Hono, token: string, name: string): Promise<number> {
|
async function createActivity(app: Hono, token: string, name: string): Promise<number> {
|
||||||
const res = await app.request('/api/activities', {
|
const res = await app.request('/api/activities', {
|
||||||
|
|||||||
54
apps/api/test/helpers.ts
Normal file
54
apps/api/test/helpers.ts
Normal file
@@ -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<unknown>;
|
||||||
|
};
|
||||||
|
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<string> {
|
||||||
|
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<string, string> {
|
||||||
|
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<number> {
|
||||||
|
const [row] = await db.insert(activities).values({ name, insoleTypes }).returning();
|
||||||
|
return row.id;
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { createApp } from '../src/app';
|
import { createApp } from '../src/app';
|
||||||
|
import { authToken, bearer } from './helpers';
|
||||||
const json = { 'content-type': 'application/json' };
|
|
||||||
|
|
||||||
describe('GET /api/me', () => {
|
describe('GET /api/me', () => {
|
||||||
it('rejects an unauthenticated request', async () => {
|
it('rejects an unauthenticated request', async () => {
|
||||||
@@ -10,27 +9,14 @@ describe('GET /api/me', () => {
|
|||||||
expect(res.status).toBe(401);
|
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 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', {
|
const res = await app.request('/api/me', { headers: bearer(token) });
|
||||||
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}` },
|
|
||||||
});
|
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.user.email).toBe(creds.email);
|
expect(body.user.email).toBe(email);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,45 +1,12 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import type { Hono } from 'hono';
|
|
||||||
import { createApp } from '../src/app';
|
import { createApp } from '../src/app';
|
||||||
import { db } from '../src/db/client';
|
import { db } from '../src/db/client';
|
||||||
import { workSessions } from '../src/db/schema';
|
import { workSessions } from '../src/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { authToken, bearer, seedActivity } from './helpers';
|
||||||
|
|
||||||
const json = { 'content-type': 'application/json' };
|
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' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an activity via the API and return its id.
|
|
||||||
async function createActivity(app: Hono, token: string, name: string): Promise<number> {
|
|
||||||
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', () => {
|
describe('session lifecycle', () => {
|
||||||
it('401s start/stop/discard without a token', async () => {
|
it('401s start/stop/discard without a token', async () => {
|
||||||
const app = createApp();
|
const app = createApp();
|
||||||
@@ -61,7 +28,7 @@ describe('session lifecycle', () => {
|
|||||||
it('starts an active session', async () => {
|
it('starts an active session', async () => {
|
||||||
const app = createApp();
|
const app = createApp();
|
||||||
const token = await authToken(app, 'sess-start@example.com');
|
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', {
|
const res = await app.request('/api/sessions/start', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -109,7 +76,7 @@ describe('session lifecycle', () => {
|
|||||||
it('completes a session and computes duration', async () => {
|
it('completes a session and computes duration', async () => {
|
||||||
const app = createApp();
|
const app = createApp();
|
||||||
const token = await authToken(app, 'sess-complete@example.com');
|
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', {
|
const startRes = await app.request('/api/sessions/start', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -138,7 +105,7 @@ describe('session lifecycle', () => {
|
|||||||
it('409s stopping an already-completed session', async () => {
|
it('409s stopping an already-completed session', async () => {
|
||||||
const app = createApp();
|
const app = createApp();
|
||||||
const token = await authToken(app, 'sess-doublestop@example.com');
|
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', {
|
const startRes = await app.request('/api/sessions/start', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -163,7 +130,7 @@ describe('session lifecycle', () => {
|
|||||||
it('discards an active session', async () => {
|
it('discards an active session', async () => {
|
||||||
const app = createApp();
|
const app = createApp();
|
||||||
const token = await authToken(app, 'sess-discard@example.com');
|
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', {
|
const startRes = await app.request('/api/sessions/start', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -186,7 +153,7 @@ describe('session lifecycle', () => {
|
|||||||
const app = createApp();
|
const app = createApp();
|
||||||
const tokenA = await authToken(app, 'sess-ownerA@example.com');
|
const tokenA = await authToken(app, 'sess-ownerA@example.com');
|
||||||
const tokenB = await authToken(app, 'sess-ownerB@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', {
|
const startRes = await app.request('/api/sessions/start', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -222,8 +189,8 @@ describe('session reads', () => {
|
|||||||
it("returns the user's sessions joined with activity name, newest first", async () => {
|
it("returns the user's sessions joined with activity name, newest first", async () => {
|
||||||
const app = createApp();
|
const app = createApp();
|
||||||
const token = await authToken(app, 'reads-history@example.com');
|
const token = await authToken(app, 'reads-history@example.com');
|
||||||
const frezenId = await createActivity(app, token, 'Frezen');
|
const frezenId = await seedActivity('Frezen');
|
||||||
const slijpenId = await createActivity(app, token, 'Slijpen');
|
const slijpenId = await seedActivity('Slijpen');
|
||||||
|
|
||||||
const firstRes = await app.request('/api/sessions/start', {
|
const firstRes = await app.request('/api/sessions/start', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -263,7 +230,7 @@ describe('session reads', () => {
|
|||||||
const app = createApp();
|
const app = createApp();
|
||||||
const tokenA = await authToken(app, 'reads-scopeA@example.com');
|
const tokenA = await authToken(app, 'reads-scopeA@example.com');
|
||||||
const tokenB = await authToken(app, 'reads-scopeB@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', {
|
const startRes = await app.request('/api/sessions/start', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -282,7 +249,7 @@ describe('session reads', () => {
|
|||||||
it('returns only active sessions from /api/sessions/active', async () => {
|
it('returns only active sessions from /api/sessions/active', async () => {
|
||||||
const app = createApp();
|
const app = createApp();
|
||||||
const token = await authToken(app, 'reads-active@example.com');
|
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.
|
// One session stays active.
|
||||||
const activeRes = await app.request('/api/sessions/start', {
|
const activeRes = await app.request('/api/sessions/start', {
|
||||||
|
|||||||
Reference in New Issue
Block a user