Files
Juno ChowandClaude Fable 5.1 f665082dad chore: 建立产品仓库,完成开源前的敏感信息清理
前端(Vite + React)、API/worker(Fastify + BullMQ)、api-contracts 契约包、
迁移脚本、测试与自托管 docker-compose 示例。

开源准备:运维脚本与机器信息已拆到私有仓库;公司名、备案号、隐私邮箱改读
publicCompliance.local.json(仓库只保留占位模板);站点地址与客服二维码改为
VITE_ 环境变量;fixtures 中的真实用户标识已匿名化;三个 package.json 标注
Apache-2.0。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-16 02:10:46 +08:00

108 lines
3.0 KiB
TypeScript

import type { FastifyRequest } from 'fastify';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { AuthToken } from '../src/domain/types.js';
const mocks = vi.hoisted(() => ({
poolQuery: vi.fn(),
}));
vi.mock('../src/db/pool.js', () => ({
pool: { query: mocks.poolQuery },
}));
import { requireAdmin, requireAuth } from '../src/auth/session.js';
describe('后端会话鉴权', () => {
beforeEach(() => {
mocks.poolQuery.mockReset();
});
it('Cookie 声明与数据库状态、角色和版本一致时通过鉴权', async () => {
const admin: AuthToken = {
id: 'admin-1',
email: 'admin@lindyson.ai',
role: 'admin',
authVersion: 3,
};
mocks.poolQuery.mockResolvedValue(queryResult([{
email: admin.email,
role: admin.role,
status: 'active',
auth_version: admin.authVersion,
}]));
const { request, verify } = requestFor(admin);
await expect(requireAdmin(request)).resolves.toEqual(admin);
expect(verify).toHaveBeenCalledWith('signed-jwt');
expect(mocks.poolQuery).toHaveBeenCalledWith(expect.stringContaining('auth_version'), [admin.id]);
});
it.each([
['frozen', 3, 'admin'],
['active', 4, 'admin'],
['active', 3, 'user'],
] as const)('数据库状态为 %s、版本为 %s、角色为 %s 时撤销旧会话', async (status, authVersion, role) => {
const token: AuthToken = {
id: 'admin-1',
email: 'admin@lindyson.ai',
role: 'admin',
authVersion: 3,
};
mocks.poolQuery.mockResolvedValue(queryResult([{
email: token.email,
role,
status,
auth_version: authVersion,
}]));
await expect(requireAuth(requestFor(token).request)).rejects.toMatchObject({
statusCode: 401,
code: 'AUTH_SESSION_INVALID',
});
});
it('普通用户访问后台接口返回 403', async () => {
const user: AuthToken = {
id: 'user-1',
email: 'member@lindyson.ai',
role: 'user',
authVersion: 1,
};
mocks.poolQuery.mockResolvedValue(queryResult([{
email: user.email,
role: user.role,
status: 'trial',
auth_version: user.authVersion,
}]));
await expect(requireAdmin(requestFor(user).request)).rejects.toMatchObject({
message: '需要管理员权限',
statusCode: 403,
});
});
it('不再接受缺少 HttpOnly Cookie 的 Bearer 会话', async () => {
const request = {
headers: { authorization: 'Bearer legacy-token' },
server: { jwt: { verify: vi.fn() } },
} as unknown as FastifyRequest;
await expect(requireAuth(request)).rejects.toMatchObject({ statusCode: 401 });
expect(mocks.poolQuery).not.toHaveBeenCalled();
});
});
function requestFor(user: AuthToken) {
const verify = vi.fn(() => user);
const request = {
headers: { cookie: 'other=value; lindyson_session=signed-jwt' },
server: { jwt: { verify } },
} as unknown as FastifyRequest;
return { request, verify };
}
function queryResult(rows: unknown[]) {
return { rows, rowCount: rows.length };
}