devperf/backend/tests/api/auth.test.ts
zyc 44464dd334 feat: DevPerf Dashboard 研发人效看板 v1.0
- 后端:Bun + Hono + Drizzle ORM + SQLite
- 前端:Vue 3 + Naive UI + ECharts
- 项目管理:创建项目 + 绑定 Git 仓库
- OKR 系统:目标/关键结果 CRUD + 进度追踪
- Git 同步:Gitea API 自动同步 commit/PR + 作者关联
- 数据看板:项目 OKR 进度 + KR 状态分布 + 代码活动
- 权限体系:admin/manager/developer/viewer 四级
- Docker 部署:docker-compose + nginx

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 17:57:14 +08:00

191 lines
5.7 KiB
TypeScript

/**
* 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);
});
});