- 后端: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>
191 lines
5.7 KiB
TypeScript
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);
|
|
});
|
|
});
|