test: add API route tests
This commit is contained in:
+188
@@ -0,0 +1,188 @@
|
|||||||
|
jest.mock('next-auth', () => {
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn(() => ({
|
||||||
|
handlers: {
|
||||||
|
authOptions: {
|
||||||
|
providers: [],
|
||||||
|
callbacks: {},
|
||||||
|
pages: {},
|
||||||
|
session: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
signIn: jest.fn(),
|
||||||
|
signOut: jest.fn(),
|
||||||
|
auth: jest.fn(),
|
||||||
|
})),
|
||||||
|
getServerSession: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('next-auth/providers/credentials', () =>
|
||||||
|
jest.fn(() => ({
|
||||||
|
name: '邮箱密码',
|
||||||
|
credentials: {
|
||||||
|
email: { label: '邮箱', type: 'email' },
|
||||||
|
password: { label: '密码', type: 'password' },
|
||||||
|
},
|
||||||
|
authorize: jest.fn(),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.mock('nanoid', () => ({
|
||||||
|
nanoid: jest.fn(() => 'test-id-123'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('next/dynamic', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
default: (importFn, options) => {
|
||||||
|
const MockComponent = (props) => null;
|
||||||
|
MockComponent.displayName = 'DynamicComponent';
|
||||||
|
MockComponent.preload = () => Promise.resolve();
|
||||||
|
return MockComponent;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('next/server', () => ({
|
||||||
|
NextRequest: class MockNextRequest {
|
||||||
|
constructor(input, init = {}) {
|
||||||
|
this.url = typeof input === 'string' ? input : input.url;
|
||||||
|
this.method = init.method || 'GET';
|
||||||
|
this.headers = new Headers(init.headers);
|
||||||
|
this.body = init.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
async json() {
|
||||||
|
return typeof this.body === 'string' ? JSON.parse(this.body) : this.body;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
NextResponse: {
|
||||||
|
json: (body, init = {}) => ({
|
||||||
|
status: init.status || 200,
|
||||||
|
json: async () => body,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
global.console = {
|
||||||
|
...console,
|
||||||
|
error: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
log: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
class MockIntersectionObserver {
|
||||||
|
constructor(callback, options = {}) {
|
||||||
|
this.callback = callback;
|
||||||
|
this.options = options;
|
||||||
|
this.elements = new Set();
|
||||||
|
this.observationEntries = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
observe(element) {
|
||||||
|
this.elements.add(element);
|
||||||
|
const entry = {
|
||||||
|
isIntersecting: true,
|
||||||
|
target: element,
|
||||||
|
boundingClientRect: element.getBoundingClientRect ? element.getBoundingClientRect() : {},
|
||||||
|
intersectionRatio: 1,
|
||||||
|
intersectionRect: {},
|
||||||
|
rootBounds: {},
|
||||||
|
time: Date.now(),
|
||||||
|
};
|
||||||
|
this.observationEntries.push(entry);
|
||||||
|
this.callback(this.observationEntries, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
unobserve(element) {
|
||||||
|
this.elements.delete(element);
|
||||||
|
this.observationEntries = this.observationEntries.filter(
|
||||||
|
entry => entry.target !== element
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.elements.clear();
|
||||||
|
this.observationEntries = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
takeRecords() {
|
||||||
|
return this.observationEntries;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
global.IntersectionObserver = MockIntersectionObserver;
|
||||||
|
global.IntersectionObserverEntry = class IntersectionObserverEntry {
|
||||||
|
constructor() {
|
||||||
|
this.isIntersecting = true;
|
||||||
|
this.target = {};
|
||||||
|
this.boundingClientRect = {};
|
||||||
|
this.intersectionRatio = 1;
|
||||||
|
this.intersectionRect = {};
|
||||||
|
this.rootBounds = {};
|
||||||
|
this.time = Date.now();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
|
writable: true,
|
||||||
|
value: jest.fn().mockImplementation(query => ({
|
||||||
|
matches: false,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addListener: jest.fn(),
|
||||||
|
removeListener: jest.fn(),
|
||||||
|
addEventListener: jest.fn(),
|
||||||
|
removeEventListener: jest.fn(),
|
||||||
|
dispatchEvent: jest.fn(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
global.Request = class Request {
|
||||||
|
constructor(input, init = {}) {
|
||||||
|
this.url = typeof input === 'string' ? input : input.url;
|
||||||
|
this.method = init.method || 'GET';
|
||||||
|
this.headers = new Headers(init.headers);
|
||||||
|
this.body = init.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
async json() {
|
||||||
|
return typeof this.body === 'string' ? JSON.parse(this.body) : this.body;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
global.Headers = class Headers {
|
||||||
|
constructor(init = {}) {
|
||||||
|
this.headers = {};
|
||||||
|
if (init) {
|
||||||
|
Object.entries(init).forEach(([key, value]) => {
|
||||||
|
this.headers[key.toLowerCase()] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(name) {
|
||||||
|
return this.headers[name.toLowerCase()];
|
||||||
|
}
|
||||||
|
|
||||||
|
set(name, value) {
|
||||||
|
this.headers[name.toLowerCase()] = value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
global.Response = class Response {
|
||||||
|
constructor(body, init = {}) {
|
||||||
|
this.body = body;
|
||||||
|
this.status = init.status || 200;
|
||||||
|
this.statusText = init.statusText || 'OK';
|
||||||
|
this.headers = new Headers(init.headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
async json() {
|
||||||
|
return typeof this.body === 'string' ? JSON.parse(this.body) : this.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
async text() {
|
||||||
|
return String(this.body);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { GET, POST } from './route';
|
||||||
|
|
||||||
|
jest.mock('@/lib/auth', () => ({
|
||||||
|
handlers: {
|
||||||
|
GET: jest.fn(() => new Response('GET response')),
|
||||||
|
POST: jest.fn(() => new Response('POST response')),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('/api/auth/[...nextauth]', () => {
|
||||||
|
describe('GET handler', () => {
|
||||||
|
it('should export GET handler', () => {
|
||||||
|
expect(typeof GET).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call auth GET handler', async () => {
|
||||||
|
const response = await GET(new Request('http://localhost/api/auth/signin'));
|
||||||
|
expect(response).toBeDefined();
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST handler', () => {
|
||||||
|
it('should export POST handler', () => {
|
||||||
|
expect(typeof POST).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call auth POST handler', async () => {
|
||||||
|
const response = await POST(
|
||||||
|
new Request('http://localhost/api/auth/signin', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ email: 'test@example.com', password: 'password' }),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(response).toBeDefined();
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
import { POST } from './route';
|
||||||
|
import { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
jest.mock('resend', () => {
|
||||||
|
const mockSend = jest.fn();
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
default: jest.fn().mockImplementation(() => ({
|
||||||
|
emails: {
|
||||||
|
send: mockSend,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
Resend: jest.fn().mockImplementation(() => ({
|
||||||
|
emails: {
|
||||||
|
send: mockSend,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('/api/contact', () => {
|
||||||
|
let mockRequest: NextRequest;
|
||||||
|
let mockSend: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const { default: Resend } = require('resend');
|
||||||
|
const resendInstance = new Resend();
|
||||||
|
mockSend = resendInstance.emails.send;
|
||||||
|
mockSend.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
const createMockRequest = (body: any): NextRequest => {
|
||||||
|
return {
|
||||||
|
json: async () => body,
|
||||||
|
} as unknown as NextRequest;
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should handle POST request with valid data', async () => {
|
||||||
|
mockSend.mockResolvedValue({
|
||||||
|
data: { id: 'test-id' },
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
mockRequest = createMockRequest({
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
subject: 'Test Subject',
|
||||||
|
message: 'Test Message',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await POST(mockRequest);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(data.success).toBe(true);
|
||||||
|
expect(data.message).toBe('消息已发送');
|
||||||
|
expect(mockSend).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate required fields', async () => {
|
||||||
|
mockRequest = createMockRequest({
|
||||||
|
name: 'Test User',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await POST(mockRequest);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(data.success).toBe(false);
|
||||||
|
expect(data.error).toBe('请填写必填字段');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate email format', async () => {
|
||||||
|
mockRequest = createMockRequest({
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'invalid-email',
|
||||||
|
subject: 'Test Subject',
|
||||||
|
message: 'Test Message',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await POST(mockRequest);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(data.success).toBe(false);
|
||||||
|
expect(data.error).toBe('请输入有效的邮箱地址');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject honeypot field', async () => {
|
||||||
|
mockRequest = createMockRequest({
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
subject: 'Test Subject',
|
||||||
|
message: 'Test Message',
|
||||||
|
website: 'spam-bot',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await POST(mockRequest);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(data.success).toBe(true);
|
||||||
|
expect(mockSend).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject submission too fast', async () => {
|
||||||
|
mockRequest = createMockRequest({
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
subject: 'Test Subject',
|
||||||
|
message: 'Test Message',
|
||||||
|
submitTime: Date.now().toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await POST(mockRequest);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(data.success).toBe(false);
|
||||||
|
expect(data.error).toBe('提交过快,请稍后再试');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate math captcha', async () => {
|
||||||
|
mockRequest = createMockRequest({
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
subject: 'Test Subject',
|
||||||
|
message: 'Test Message',
|
||||||
|
mathHash: 'invalid-hash',
|
||||||
|
mathTimestamp: Date.now().toString(),
|
||||||
|
mathAnswer: '5',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await POST(mockRequest);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(400);
|
||||||
|
expect(data.success).toBe(false);
|
||||||
|
expect(data.error).toBe('验证码错误,请重新计算');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Resend API error', async () => {
|
||||||
|
mockSend.mockResolvedValue({
|
||||||
|
data: null,
|
||||||
|
error: { message: 'API Error' },
|
||||||
|
});
|
||||||
|
|
||||||
|
mockRequest = createMockRequest({
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
subject: 'Test Subject',
|
||||||
|
message: 'Test Message',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await POST(mockRequest);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(data.success).toBe(false);
|
||||||
|
expect(data.error).toBe('邮件发送失败,请稍后重试');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle JSON parsing error', async () => {
|
||||||
|
mockRequest = {
|
||||||
|
json: async () => {
|
||||||
|
throw new Error('Invalid JSON');
|
||||||
|
},
|
||||||
|
} as unknown as NextRequest;
|
||||||
|
|
||||||
|
const response = await POST(mockRequest);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(500);
|
||||||
|
expect(data.success).toBe(false);
|
||||||
|
expect(data.error).toBe('提交失败,请重试');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept valid submission with phone', async () => {
|
||||||
|
mockSend.mockResolvedValue({
|
||||||
|
data: { id: 'test-id' },
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
mockRequest = createMockRequest({
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
phone: '13800138000',
|
||||||
|
subject: 'Test Subject',
|
||||||
|
message: 'Test Message',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await POST(mockRequest);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(data.success).toBe(true);
|
||||||
|
expect(mockSend).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept valid submission with math captcha', async () => {
|
||||||
|
mockSend.mockResolvedValue({
|
||||||
|
data: { id: 'test-id' },
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const answer = '5';
|
||||||
|
const hash = btoa(`${answer}-${timestamp}`);
|
||||||
|
|
||||||
|
mockRequest = createMockRequest({
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
subject: 'Test Subject',
|
||||||
|
message: 'Test Message',
|
||||||
|
mathHash: hash,
|
||||||
|
mathTimestamp: timestamp.toString(),
|
||||||
|
mathAnswer: answer,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await POST(mockRequest);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(data.success).toBe(true);
|
||||||
|
expect(mockSend).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle submission after minimum time', async () => {
|
||||||
|
mockSend.mockResolvedValue({
|
||||||
|
data: { id: 'test-id' },
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pastTime = Date.now() - 3000;
|
||||||
|
mockRequest = createMockRequest({
|
||||||
|
name: 'Test User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
subject: 'Test Subject',
|
||||||
|
message: 'Test Message',
|
||||||
|
submitTime: pastTime.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await POST(mockRequest);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(data.success).toBe(true);
|
||||||
|
expect(mockSend).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user