安全/鉴权:登出清服务端 httpOnly 令牌、登录 IP 限流前置 bcrypt、停用账号 令牌在 6 个内联 role handler 失效、admin/items 存在性 oracle 认证前置、 同意偏好 sanitize fail-closed、api-crypto 按 HTTP 方法判体。 CMS:workflow 状态更新+审计收进交互式 $transaction(失败即回滚)、编辑器 richtext 走 textarea 防换行损毁、array/object 子字段按作用域读写防污染、 about/contact/erp-upgrade 补 ISR revalidate 与路由映射。 UI/a11y:AnimatedCounter 入视口前显起点、CLS 累计上报、MetricCard 动画入 effect 并卸载取消 rAF、后台状态四态映射/搜索防抖+序号守卫/媒体单 input/ 吞错改横幅/表单 label-aria 关联。 新增对应判别性单测与 pagination/rate-limit/client-ip/sanitize/validate-content-data 等模块及 tests-integration 真库集成层。
159 lines
5.3 KiB
TypeScript
159 lines
5.3 KiB
TypeScript
import { describe, it, expect, jest, beforeEach } from '@jest/globals';
|
|
import type { NextRequest } from 'next/server';
|
|
|
|
const mockUserFindUnique = jest.fn<(args: unknown) => Promise<unknown>>();
|
|
const mockUserRoleFindMany = jest.fn<(args: unknown) => Promise<unknown[]>>();
|
|
|
|
jest.mock('@/lib/db', () => ({
|
|
prisma: {
|
|
user: { findUnique: mockUserFindUnique },
|
|
userRole: { findMany: mockUserRoleFindMany },
|
|
},
|
|
}));
|
|
|
|
const mockVerifyRefreshToken = jest.fn<(token: string) => unknown>();
|
|
const mockGenerateTokens = jest.fn<(payload: unknown) => { accessToken: string; refreshToken: string }>();
|
|
const mockSetTokenCookie = jest.fn();
|
|
|
|
jest.mock('@/lib/auth', () => ({
|
|
verifyRefreshToken: (token: string) => mockVerifyRefreshToken(token),
|
|
generateTokens: (payload: unknown) => mockGenerateTokens(payload),
|
|
setTokenCookie: (r: unknown, a: string, b: string, s: boolean) => mockSetTokenCookie(r, a, b, s),
|
|
}));
|
|
|
|
jest.unmock('./route');
|
|
|
|
import { POST } from './route';
|
|
|
|
const legacyPayload = {
|
|
userId: 'user-1',
|
|
username: 'editor',
|
|
role: 'super_admin',
|
|
};
|
|
|
|
function createRequest(body: Record<string, unknown>): NextRequest {
|
|
return {
|
|
url: 'http://localhost:3000/api/auth/refresh',
|
|
nextUrl: { protocol: 'http:' },
|
|
headers: new Headers(),
|
|
json: async () => body,
|
|
} as unknown as NextRequest;
|
|
}
|
|
|
|
const request = () => createRequest({ refreshToken: 'signed-refresh-token' });
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
mockVerifyRefreshToken.mockReturnValue(legacyPayload);
|
|
mockUserFindUnique.mockResolvedValue({ id: 'user-1', username: 'editor', role: 'admin', status: 1 });
|
|
mockUserRoleFindMany.mockResolvedValue([{ userId: 'user-1', roleCode: 'super_admin' }]);
|
|
mockGenerateTokens.mockReturnValue({ accessToken: 'new-access', refreshToken: 'new-refresh' });
|
|
});
|
|
|
|
describe('POST /api/auth/refresh', () => {
|
|
it('returns 401 when the refresh token is missing', async () => {
|
|
const response = await POST(createRequest({}));
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(mockGenerateTokens).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns 401 when the refresh token signature is invalid', async () => {
|
|
mockVerifyRefreshToken.mockImplementation(() => {
|
|
throw new Error('jwt expired');
|
|
});
|
|
|
|
const response = await POST(request());
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(mockUserFindUnique).not.toHaveBeenCalled();
|
|
expect(mockGenerateTokens).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('mints no access token for a disabled user', async () => {
|
|
mockUserFindUnique.mockResolvedValue({ id: 'user-1', username: 'editor', status: 0 });
|
|
|
|
const response = await POST(request());
|
|
const body = await response.json();
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(body.code).toBe('UNAUTHORIZED');
|
|
expect(mockGenerateTokens).not.toHaveBeenCalled();
|
|
expect(mockSetTokenCookie).not.toHaveBeenCalled();
|
|
expect(mockUserRoleFindMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('mints no access token for a deleted user', async () => {
|
|
mockUserFindUnique.mockResolvedValue(null);
|
|
|
|
const response = await POST(request());
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(mockGenerateTokens).not.toHaveBeenCalled();
|
|
expect(mockSetTokenCookie).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('looks the account up by the token subject id, not by username', async () => {
|
|
await POST(request());
|
|
|
|
expect(mockUserFindUnique).toHaveBeenCalledWith({ where: { id: 'user-1' } });
|
|
expect(mockUserRoleFindMany).toHaveBeenCalledWith({ where: { userId: 'user-1' } });
|
|
});
|
|
|
|
it('re-signs with the demoted role from the database instead of the stale claim', async () => {
|
|
mockUserRoleFindMany.mockResolvedValue([{ userId: 'user-1', roleCode: 'content_editor' }]);
|
|
|
|
const response = await POST(request());
|
|
const body = await response.json();
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockGenerateTokens).toHaveBeenCalledWith({
|
|
userId: 'user-1',
|
|
username: 'editor',
|
|
role: 'content_editor',
|
|
});
|
|
expect(mockGenerateTokens.mock.calls[0]![0]).not.toMatchObject({ role: 'super_admin' });
|
|
expect(body).toEqual({ accessToken: 'new-access', refreshToken: 'new-refresh' });
|
|
});
|
|
|
|
it('falls back to the stored role column when the account carries no role grants', async () => {
|
|
mockUserRoleFindMany.mockResolvedValue([]);
|
|
|
|
const response = await POST(request());
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(mockGenerateTokens).toHaveBeenCalledWith({
|
|
userId: 'user-1',
|
|
username: 'editor',
|
|
role: 'admin',
|
|
});
|
|
});
|
|
|
|
it('re-issues from the current account row rather than the token payload', async () => {
|
|
mockUserFindUnique.mockResolvedValue({
|
|
id: 'user-1',
|
|
username: 'renamed-editor',
|
|
role: 'content_editor',
|
|
status: 1,
|
|
});
|
|
mockUserRoleFindMany.mockResolvedValue([{ userId: 'user-1', roleCode: 'reviewer' }]);
|
|
|
|
await POST(request());
|
|
|
|
expect(mockGenerateTokens).toHaveBeenCalledWith({
|
|
userId: 'user-1',
|
|
username: 'renamed-editor',
|
|
role: 'reviewer',
|
|
});
|
|
});
|
|
|
|
it('returns 500 when the account lookup fails instead of handing out a token', async () => {
|
|
mockUserFindUnique.mockRejectedValue(new Error('SQLITE_BUSY'));
|
|
|
|
const response = await POST(request());
|
|
|
|
expect(response.status).toBe(500);
|
|
expect(mockGenerateTokens).not.toHaveBeenCalled();
|
|
});
|
|
});
|