179 lines
5.7 KiB
TypeScript
179 lines
5.7 KiB
TypeScript
import { NextRequest } from 'next/server'
|
||
|
||
const VALID_RESTORE_TOKEN = 'restore-token-12345678901234567890'
|
||
|
||
jest.mock('@/lib/prisma', () => ({
|
||
prisma: {
|
||
user: {
|
||
create: jest.fn(),
|
||
findUnique: jest.fn(),
|
||
},
|
||
category: {
|
||
create: jest.fn(),
|
||
},
|
||
scene: {
|
||
create: jest.fn(),
|
||
},
|
||
material: {
|
||
create: jest.fn(),
|
||
},
|
||
},
|
||
}))
|
||
|
||
function createRequest(
|
||
token?: string,
|
||
ip = '127.0.0.1',
|
||
confirmPhrase?: string
|
||
) {
|
||
return new NextRequest('http://localhost/api/restore-data', {
|
||
method: 'POST',
|
||
headers: {
|
||
...(token ? { 'x-restore-data-token': token } : {}),
|
||
...(confirmPhrase ? { 'x-restore-data-confirm': confirmPhrase } : {}),
|
||
'x-forwarded-for': ip,
|
||
},
|
||
})
|
||
}
|
||
|
||
async function loadPost() {
|
||
const route = await import('../route')
|
||
return route.POST
|
||
}
|
||
|
||
describe('POST /api/restore-data', () => {
|
||
const originalEnv = process.env
|
||
|
||
beforeEach(() => {
|
||
jest.resetModules()
|
||
process.env = { ...originalEnv }
|
||
delete process.env.RESTORE_DATA_ENABLED
|
||
delete process.env.RESTORE_DATA_TOKEN
|
||
delete process.env.RESTORE_DATA_ALLOWED_IPS
|
||
delete process.env.RESTORE_DATA_TRUST_PROXY
|
||
delete process.env.RESTORE_ADMIN_PASSWORD
|
||
})
|
||
|
||
afterAll(() => {
|
||
process.env = originalEnv
|
||
})
|
||
|
||
it('默认未显式启用时拒绝执行', async () => {
|
||
process.env.RESTORE_DATA_TOKEN = VALID_RESTORE_TOKEN
|
||
process.env.RESTORE_ADMIN_PASSWORD = 'configured-password'
|
||
const POST = await loadPost()
|
||
|
||
const response = await POST(createRequest(VALID_RESTORE_TOKEN))
|
||
const data = await response.json()
|
||
|
||
expect(response.status).toBe(503)
|
||
expect(data.success).toBe(false)
|
||
expect(data.message).toContain('RESTORE_DATA_ENABLED=1')
|
||
})
|
||
|
||
it('启用后仍要求配置恢复令牌', async () => {
|
||
process.env.RESTORE_DATA_ENABLED = '1'
|
||
process.env.RESTORE_ADMIN_PASSWORD = 'configured-password'
|
||
const POST = await loadPost()
|
||
|
||
const response = await POST(createRequest())
|
||
const data = await response.json()
|
||
|
||
expect(response.status).toBe(503)
|
||
expect(data.success).toBe(false)
|
||
expect(data.message).toContain('RESTORE_DATA_TOKEN')
|
||
})
|
||
|
||
it('启用后会拒绝过短恢复令牌', async () => {
|
||
process.env.RESTORE_DATA_ENABLED = '1'
|
||
process.env.RESTORE_DATA_TOKEN = 'restore-token'
|
||
process.env.RESTORE_DATA_ALLOWED_IPS = '127.0.0.1'
|
||
process.env.RESTORE_ADMIN_PASSWORD = 'configured-password'
|
||
const POST = await loadPost()
|
||
|
||
const response = await POST(createRequest('restore-token'))
|
||
const data = await response.json()
|
||
|
||
expect(response.status).toBe(503)
|
||
expect(data.success).toBe(false)
|
||
expect(data.message).toContain('RESTORE_DATA_TOKEN 长度不足')
|
||
})
|
||
|
||
it('启用且配置令牌后会拒绝错误令牌', async () => {
|
||
process.env.RESTORE_DATA_ENABLED = '1'
|
||
process.env.RESTORE_DATA_TOKEN = VALID_RESTORE_TOKEN
|
||
process.env.RESTORE_DATA_ALLOWED_IPS = '127.0.0.1'
|
||
process.env.RESTORE_DATA_TRUST_PROXY = '1'
|
||
process.env.RESTORE_ADMIN_PASSWORD = 'configured-password'
|
||
const POST = await loadPost()
|
||
|
||
const response = await POST(createRequest('wrong-token'))
|
||
const data = await response.json()
|
||
|
||
expect(response.status).toBe(401)
|
||
expect(data.success).toBe(false)
|
||
expect(data.message).toBe('恢复令牌无效')
|
||
})
|
||
|
||
it('启用且配置令牌后仍要求配置 IP 白名单', async () => {
|
||
process.env.RESTORE_DATA_ENABLED = '1'
|
||
process.env.RESTORE_DATA_TOKEN = VALID_RESTORE_TOKEN
|
||
process.env.RESTORE_ADMIN_PASSWORD = 'configured-password'
|
||
const POST = await loadPost()
|
||
|
||
const response = await POST(createRequest(VALID_RESTORE_TOKEN))
|
||
const data = await response.json()
|
||
|
||
expect(response.status).toBe(503)
|
||
expect(data.success).toBe(false)
|
||
expect(data.message).toContain('RESTORE_DATA_ALLOWED_IPS')
|
||
})
|
||
|
||
it('启用且配置令牌后会拒绝非白名单 IP', async () => {
|
||
process.env.RESTORE_DATA_ENABLED = '1'
|
||
process.env.RESTORE_DATA_TOKEN = VALID_RESTORE_TOKEN
|
||
process.env.RESTORE_DATA_ALLOWED_IPS = '127.0.0.1'
|
||
process.env.RESTORE_DATA_TRUST_PROXY = '1'
|
||
process.env.RESTORE_ADMIN_PASSWORD = 'configured-password'
|
||
const POST = await loadPost()
|
||
|
||
const response = await POST(createRequest(VALID_RESTORE_TOKEN, '10.0.0.8'))
|
||
const data = await response.json()
|
||
|
||
expect(response.status).toBe(403)
|
||
expect(data.success).toBe(false)
|
||
expect(data.message).toBe('当前来源 IP 不允许执行恢复')
|
||
})
|
||
|
||
it('默认不信任可伪造的代理 IP 头', async () => {
|
||
process.env.RESTORE_DATA_ENABLED = '1'
|
||
process.env.RESTORE_DATA_TOKEN = VALID_RESTORE_TOKEN
|
||
process.env.RESTORE_DATA_ALLOWED_IPS = '127.0.0.1'
|
||
process.env.RESTORE_ADMIN_PASSWORD = 'configured-password'
|
||
const POST = await loadPost()
|
||
|
||
const response = await POST(createRequest(VALID_RESTORE_TOKEN, '127.0.0.1'))
|
||
const data = await response.json()
|
||
|
||
expect(response.status).toBe(403)
|
||
expect(data.success).toBe(false)
|
||
expect(data.message).toBe('无法确认可信来源 IP,恢复接口已拒绝')
|
||
})
|
||
|
||
it('通过令牌和白名单后仍要求恢复确认头', async () => {
|
||
process.env.RESTORE_DATA_ENABLED = '1'
|
||
process.env.RESTORE_DATA_TOKEN = VALID_RESTORE_TOKEN
|
||
process.env.RESTORE_DATA_ALLOWED_IPS = '127.0.0.1'
|
||
process.env.RESTORE_DATA_TRUST_PROXY = '1'
|
||
process.env.RESTORE_ADMIN_PASSWORD = 'configured-password'
|
||
const POST = await loadPost()
|
||
|
||
const response = await POST(createRequest(VALID_RESTORE_TOKEN, '127.0.0.1'))
|
||
const data = await response.json()
|
||
|
||
expect(response.status).toBe(400)
|
||
expect(data.success).toBe(false)
|
||
expect(data.message).toBe('恢复确认头无效')
|
||
expect(data.requiredConfirmHeader).toBe('x-restore-data-confirm')
|
||
})
|
||
})
|