- 后端: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>
108 lines
3.3 KiB
TypeScript
108 lines
3.3 KiB
TypeScript
/**
|
|
* Unit tests for JWT token generation and verification logic.
|
|
* Tests the jose-based JWT flow independent of the database.
|
|
*/
|
|
import { describe, it, expect } from 'bun:test';
|
|
import { SignJWT, jwtVerify } from 'jose';
|
|
|
|
const TEST_SECRET = new TextEncoder().encode(
|
|
'test-secret-for-unit-tests-at-least-16-chars',
|
|
);
|
|
|
|
describe('JWT Token Generation', () => {
|
|
it('should generate a valid JWT with expected claims', async () => {
|
|
const token = await new SignJWT({
|
|
sub: 'user-001',
|
|
email: 'test@example.com',
|
|
role: 'admin',
|
|
displayName: 'Test User',
|
|
})
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setIssuedAt()
|
|
.setExpirationTime('7d')
|
|
.sign(TEST_SECRET);
|
|
|
|
expect(typeof token).toBe('string');
|
|
expect(token.split('.').length).toBe(3); // JWT has 3 parts
|
|
|
|
// Verify the token
|
|
const { payload } = await jwtVerify(token, TEST_SECRET);
|
|
expect(payload.sub).toBe('user-001');
|
|
expect(payload.email).toBe('test@example.com');
|
|
expect(payload.role).toBe('admin');
|
|
expect(payload.displayName).toBe('Test User');
|
|
expect(payload.exp).toBeDefined();
|
|
expect(payload.iat).toBeDefined();
|
|
});
|
|
|
|
it('should reject tampered tokens', async () => {
|
|
const token = await new SignJWT({ sub: 'user-001' })
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setExpirationTime('1h')
|
|
.sign(TEST_SECRET);
|
|
|
|
// Tamper with the token by changing a character
|
|
const parts = token.split('.');
|
|
parts[1] = parts[1].slice(0, -1) + (parts[1].slice(-1) === 'A' ? 'B' : 'A');
|
|
const tampered = parts.join('.');
|
|
|
|
try {
|
|
await jwtVerify(tampered, TEST_SECRET);
|
|
expect(true).toBe(false); // Should not reach here
|
|
} catch (err: any) {
|
|
expect(err.code).toBe('ERR_JWS_SIGNATURE_VERIFICATION_FAILED');
|
|
}
|
|
});
|
|
|
|
it('should reject tokens signed with wrong secret', async () => {
|
|
const wrongSecret = new TextEncoder().encode('wrong-secret-that-is-long-enough');
|
|
const token = await new SignJWT({ sub: 'user-001' })
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setExpirationTime('1h')
|
|
.sign(wrongSecret);
|
|
|
|
try {
|
|
await jwtVerify(token, TEST_SECRET);
|
|
expect(true).toBe(false);
|
|
} catch (err: any) {
|
|
expect(err.code).toBe('ERR_JWS_SIGNATURE_VERIFICATION_FAILED');
|
|
}
|
|
});
|
|
|
|
it('should reject expired tokens', async () => {
|
|
const token = await new SignJWT({ sub: 'user-001' })
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setExpirationTime('0s') // Already expired
|
|
.sign(TEST_SECRET);
|
|
|
|
// Wait a tiny bit to ensure expiration
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
|
|
try {
|
|
await jwtVerify(token, TEST_SECRET);
|
|
expect(true).toBe(false);
|
|
} catch (err: any) {
|
|
expect(err.code).toBe('ERR_JWT_EXPIRED');
|
|
}
|
|
});
|
|
|
|
it('should include all role types in valid token payloads', async () => {
|
|
const roles = ['admin', 'manager', 'developer', 'viewer'];
|
|
|
|
for (const role of roles) {
|
|
const token = await new SignJWT({
|
|
sub: `user-${role}`,
|
|
email: `${role}@test.com`,
|
|
role,
|
|
displayName: `Test ${role}`,
|
|
})
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setExpirationTime('1h')
|
|
.sign(TEST_SECRET);
|
|
|
|
const { payload } = await jwtVerify(token, TEST_SECRET);
|
|
expect(payload.role).toBe(role);
|
|
}
|
|
});
|
|
});
|