jest.mock('next/server', () => { class MockNextResponse extends Response { static next() { return new MockNextResponse(null, { status: 200 }) } static json(body: unknown, init?: ResponseInit) { return new MockNextResponse(JSON.stringify(body), { ...init, headers: { 'content-type': 'application/json', ...(init?.headers || {}), }, }) } static redirect(url: URL | string) { return new MockNextResponse(null, { status: 307, headers: { Location: url.toString() }, }) } } return { NextResponse: MockNextResponse } }) jest.mock('next-auth/jwt', () => ({ getToken: jest.fn(), })) function createRequest(url: string, options: { method?: string; origin?: string } = {}) { const parsed = new URL(url) const headers = new Headers() if (options.origin) { headers.set('origin', options.origin) } return { method: options.method || 'GET', url, headers, nextUrl: { pathname: parsed.pathname, origin: parsed.origin, }, } as any } describe('middleware CORS', () => { const originalEnv = process.env beforeEach(() => { jest.resetModules() process.env = { ...originalEnv, ALLOWED_ORIGINS: 'https://portal.example.com', APP_PUBLIC_URL: 'https://app.example.com', NEXTAUTH_URL: 'https://app.example.com', } }) afterEach(() => { process.env = originalEnv }) it('允许白名单来源携带凭据访问 API', async () => { const { middleware } = await import('../middleware') const response = await middleware(createRequest('https://app.example.com/api/materials', { origin: 'https://portal.example.com', })) expect(response.headers.get('Access-Control-Allow-Origin')).toBe('https://portal.example.com') expect(response.headers.get('Access-Control-Allow-Credentials')).toBe('true') expect(response.headers.get('Vary')).toBe('Origin') }) it('拒绝陌生来源的预检请求', async () => { const { middleware } = await import('../middleware') const response = await middleware(createRequest('https://app.example.com/api/materials', { method: 'OPTIONS', origin: 'https://evil.example.com', })) expect(response.status).toBe(403) expect(response.headers.get('Access-Control-Allow-Origin')).toBeNull() }) it('陌生来源的普通 API 响应不返回 CORS 凭据头', async () => { const { middleware } = await import('../middleware') const response = await middleware(createRequest('https://app.example.com/api/materials', { origin: 'https://evil.example.com', })) expect(response.status).toBe(200) expect(response.headers.get('Access-Control-Allow-Origin')).toBeNull() expect(response.headers.get('Access-Control-Allow-Credentials')).toBeNull() }) }) describe('middleware admin pages', () => { const originalEnv = process.env beforeEach(() => { jest.resetModules() process.env = { ...originalEnv, NEXTAUTH_SECRET: 'test-secret', } }) afterEach(() => { process.env = originalEnv }) it('普通用户访问监控页会被带回首页', async () => { const { getToken } = require('next-auth/jwt') getToken.mockResolvedValue({ role: 'USER' }) const { middleware } = await import('../middleware') const response = await middleware(createRequest('https://app.example.com/admin/monitor')) expect(response.status).toBe(307) expect(response.headers.get('Location')).toBe('https://app.example.com/') }) it('管理员可以访问监控页', async () => { const { getToken } = require('next-auth/jwt') getToken.mockResolvedValue({ role: 'ADMIN' }) const { middleware } = await import('../middleware') const response = await middleware(createRequest('https://app.example.com/admin/monitor')) expect(response.status).toBe(200) }) it('首次改密前访问后台页面会被带回首页', async () => { const { getToken } = require('next-auth/jwt') getToken.mockResolvedValue({ role: 'ADMIN', mustChangePassword: true }) const { middleware } = await import('../middleware') const response = await middleware(createRequest('https://app.example.com/admin/settings')) expect(response.status).toBe(307) expect(response.headers.get('Location')).toBe('https://app.example.com/') }) it('首次改密前业务 API 会被拦截,但改密接口仍可访问', async () => { const { getToken } = require('next-auth/jwt') getToken.mockResolvedValue({ role: 'ADMIN', mustChangePassword: true }) const { middleware } = await import('../middleware') const blocked = await middleware(createRequest('https://app.example.com/api/admin/users')) expect(blocked.status).toBe(403) await expect(blocked.json()).resolves.toMatchObject({ code: 'PASSWORD_CHANGE_REQUIRED', }) const allowed = await middleware(createRequest('https://app.example.com/api/user/change-password')) expect(allowed.status).toBe(200) }) })