2026-09-04 11:50:50 +08:00

143 lines
4.8 KiB
TypeScript

/** @jest-environment node */
import { NextRequest, NextResponse } from 'next/server'
import { GET, POST } from '@/app/api/dingtalk/h5-login/route'
import { loginDingtalkUserByH5AuthCode } from '@/lib/dingtalk-login'
import { authorizeDingtalkH5UserForNextAuth } from '@/lib/dingtalk-login'
import { createDingtalkPerformanceSessionToken } from '@/lib/dingtalk-performance-session'
jest.mock('@/lib/dingtalk-login', () => ({
authorizeDingtalkH5UserForNextAuth: jest.fn(),
loginDingtalkUserByH5AuthCode: jest.fn(),
}))
jest.mock('@/lib/dingtalk-performance-session', () => ({
createDingtalkPerformanceSessionToken: jest.fn(),
resolveDingtalkPerformanceSessionScope: jest.fn((redirectPath: string) => {
const url = new URL(redirectPath, 'https://performance-survey.test')
const segments = url.pathname.split('/').filter(Boolean)
const surveyId = segments[2] || ''
const targetId = url.searchParams.get('targetId')?.trim() || ''
if (segments[0] === 'performance-survey' && segments[1] === 'survey' && surveyId && targetId) {
return { kind: 'assignment', surveyId, targetId }
}
if (segments[0] === 'performance-survey' && segments[1] === 'results' && surveyId) {
return { kind: 'results', surveyId, targetId: targetId || null }
}
return null
}),
}))
jest.mock('@/lib/public-app-url', () => ({
getPublicAppOrigin: jest.fn(() => 'https://example.test'),
}))
jest.mock('@/lib/safe-redirect', () => ({
normalizeInternalRedirectPath: jest.fn((value: unknown, fallback: string) => (
typeof value === 'string' && value.startsWith('/') ? value : fallback
)),
}))
describe('DingTalk H5 public login body limit', () => {
beforeEach(() => {
jest.clearAllMocks()
jest.spyOn(console, 'error').mockImplementation(() => undefined)
})
afterEach(() => {
jest.restoreAllMocks()
})
test('超限 multipart 在 formData 解析前返回 413', async () => {
const formData = jest.fn()
const request = {
headers: new Headers({
'content-type': 'multipart/form-data; boundary=test',
'content-length': String(64 * 1024 + 1),
}),
formData,
} as unknown as NextRequest
const response = await POST(request)
const body = await response.json()
expect(response.status).toBe(413)
expect(body.code).toBe('REQUEST_TOO_LARGE')
expect(formData).not.toHaveBeenCalled()
expect(loginDingtalkUserByH5AuthCode).not.toHaveBeenCalled()
})
test('缺少 Content-Length 时不读取匿名请求体', async () => {
const text = jest.fn()
const request = {
headers: new Headers({ 'content-type': 'text/plain' }),
text,
} as unknown as NextRequest
const response = await POST(request)
expect(response.status).toBe(411)
expect(text).not.toHaveBeenCalled()
})
test('小型 JSON 登录请求保持原有成功链路', async () => {
const payload = JSON.stringify({ code: 'short-code' })
;(loginDingtalkUserByH5AuthCode as jest.Mock).mockResolvedValue(
NextResponse.json({ success: true }),
)
const request = new NextRequest('http://localhost/api/dingtalk/h5-login', {
method: 'POST',
headers: {
'content-type': 'application/json',
'content-length': String(Buffer.byteLength(payload)),
},
body: payload,
})
const response = await POST(request)
expect(response.status).toBe(200)
expect(loginDingtalkUserByH5AuthCode).toHaveBeenCalledWith(
request,
'short-code',
{ appSource: 'workNotice' },
)
})
test('绩效调研免登落地页写入限定路径的会话 Cookie', async () => {
;(authorizeDingtalkH5UserForNextAuth as jest.Mock).mockResolvedValue({
id: 'user-1',
email: 'external@example.com',
username: '跨部门同事',
role: 'USER',
department: '运营中心',
isSuperAdmin: false,
})
;(createDingtalkPerformanceSessionToken as jest.Mock).mockReturnValue('signed-performance-token')
const response = await GET(new NextRequest(
'https://sc.jiansun.vip/api/dingtalk/h5-login?code=short-code&redirect=%2Fperformance-survey%2Fsurvey%2Fsurvey-1%3FtargetId%3Dtarget-1',
{
headers: {
accept: 'text/html',
'user-agent': 'DingTalk/7.0',
},
},
))
expect(response.status).toBe(200)
expect(response.headers.get('set-cookie')).toContain(
'dingtalk-performance-session=signed-performance-token',
)
expect(response.headers.get('set-cookie')).toContain('Path=/performance-survey')
expect(response.headers.get('set-cookie')).toContain('Path=/api')
expect(response.headers.get('set-cookie')).toContain('HttpOnly')
expect(authorizeDingtalkH5UserForNextAuth).toHaveBeenCalledWith('short-code', {
appSource: 'workNotice',
clientIp: null,
allowRestrictedPerformanceAccess: true,
})
})
})