/** * API integration tests for the authentication endpoints. * Tests login validation, JWT generation, and /me endpoint. * * Uses Hono's built-in test client with a minimal route setup * to test request/response handling without real DB. */ import { describe, it, expect } from 'bun:test'; import { Hono } from 'hono'; import { zValidator } from '@hono/zod-validator'; import { z } from 'zod'; import { SignJWT, jwtVerify } from 'jose'; const TEST_SECRET = new TextEncoder().encode( 'test-secret-for-unit-tests-at-least-16-chars', ); // Minimal auth routes for testing request validation function createAuthTestApp() { const app = new Hono(); const loginSchema = z.object({ email: z.string().email('Invalid email format'), password: z.string().min(6, 'Password must be at least 6 characters'), }); // Simulated login route app.post('/api/auth/login', zValidator('json', loginSchema), async (c) => { const { email, password } = c.req.valid('json'); // Mock: only accept test@test.com / password123 if (email !== 'test@test.com' || password !== 'password123') { return c.json({ code: 40101, data: null, message: 'Invalid email or password' }, 401); } const token = await new SignJWT({ sub: 'user-001', email, role: 'admin', displayName: 'Test Admin', }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('7d') .sign(TEST_SECRET); return c.json({ code: 0, data: { token, user: { id: 'user-001', displayName: 'Test Admin', email, role: 'admin' }, }, message: 'success', }); }); // Simulated /me route app.get('/api/auth/me', async (c) => { const authHeader = c.req.header('Authorization'); if (!authHeader || !authHeader.startsWith('Bearer ')) { return c.json({ code: 40101, data: null, message: 'Authentication required' }, 401); } const token = authHeader.slice(7); try { const { payload } = await jwtVerify(token, TEST_SECRET); return c.json({ code: 0, data: { id: payload.sub, displayName: payload.displayName, email: payload.email, role: payload.role, }, message: 'success', }); } catch { return c.json({ code: 40102, data: null, message: 'Token expired or invalid' }, 401); } }); return app; } describe('POST /api/auth/login', () => { const app = createAuthTestApp(); it('should return 200 with token on valid credentials', async () => { const res = await app.request('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'test@test.com', password: 'password123' }), }); expect(res.status).toBe(200); const body = await res.json(); expect(body.code).toBe(0); expect(body.data.token).toBeDefined(); expect(typeof body.data.token).toBe('string'); expect(body.data.user.email).toBe('test@test.com'); expect(body.data.user.role).toBe('admin'); }); it('should return 401 on invalid credentials', async () => { const res = await app.request('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'wrong@test.com', password: 'wrongpass' }), }); expect(res.status).toBe(401); const body = await res.json(); expect(body.code).toBe(40101); }); it('should return 400 on invalid email format', async () => { const res = await app.request('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'not-an-email', password: 'password123' }), }); expect(res.status).toBe(400); }); it('should return 400 on short password', async () => { const res = await app.request('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'test@test.com', password: '123' }), }); expect(res.status).toBe(400); }); it('should return 400 on missing body', async () => { const res = await app.request('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}', }); expect(res.status).toBe(400); }); }); describe('GET /api/auth/me', () => { const app = createAuthTestApp(); it('should return user info with valid token', async () => { // First login to get a token const loginRes = await app.request('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'test@test.com', password: 'password123' }), }); const loginBody = await loginRes.json(); const token = loginBody.data.token; // Use token for /me const meRes = await app.request('/api/auth/me', { headers: { Authorization: `Bearer ${token}` }, }); expect(meRes.status).toBe(200); const meBody = await meRes.json(); expect(meBody.code).toBe(0); expect(meBody.data.email).toBe('test@test.com'); expect(meBody.data.role).toBe('admin'); }); it('should return 401 without auth header', async () => { const res = await app.request('/api/auth/me'); expect(res.status).toBe(401); }); it('should return 401 with invalid token', async () => { const res = await app.request('/api/auth/me', { headers: { Authorization: 'Bearer invalid.token.here' }, }); expect(res.status).toBe(401); }); it('should return 401 with malformed auth header', async () => { const res = await app.request('/api/auth/me', { headers: { Authorization: 'Basic abc123' }, }); expect(res.status).toBe(401); }); });