11 changed files with 610 additions and 21 deletions
@ -0,0 +1,86 @@ |
|||||
|
import { NextRequest } from "next/server"; |
||||
|
import { afterEach, describe, expect, it, vi } from "vitest"; |
||||
|
import { POST } from "../route"; |
||||
|
|
||||
|
const originalFetch = global.fetch; |
||||
|
const originalBaseUrl = process.env.POPIART_API_BASE_URL; |
||||
|
|
||||
|
function createPostRequest(body: unknown, headers?: Record<string, string>): NextRequest { |
||||
|
return new NextRequest("http://localhost/api/points/calculate-task-price", { |
||||
|
method: "POST", |
||||
|
headers: new Headers({ |
||||
|
"Content-Type": "application/json", |
||||
|
...(headers || {}), |
||||
|
}), |
||||
|
body: JSON.stringify(body), |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
describe("/api/points/calculate-task-price route", () => { |
||||
|
afterEach(() => { |
||||
|
global.fetch = originalFetch; |
||||
|
process.env.POPIART_API_BASE_URL = originalBaseUrl; |
||||
|
vi.restoreAllMocks(); |
||||
|
}); |
||||
|
|
||||
|
it("returns 401 when user token is missing", async () => { |
||||
|
const response = await POST(createPostRequest({ aiModelId: 32 })); |
||||
|
|
||||
|
await expect(response.json()).resolves.toEqual({ |
||||
|
success: false, |
||||
|
code: "AUTH_REQUIRED", |
||||
|
error: "请先登录", |
||||
|
}); |
||||
|
expect(response.status).toBe(401); |
||||
|
}); |
||||
|
|
||||
|
it("forwards task price requests to PopiServer with user auth", async () => { |
||||
|
process.env.POPIART_API_BASE_URL = "https://popi.example"; |
||||
|
const payload = { |
||||
|
type: 1, |
||||
|
subType: 102, |
||||
|
aiModelId: 32, |
||||
|
model: "gpt-image-2-all", |
||||
|
aspectRatio: "16:9", |
||||
|
ratio: "16:9", |
||||
|
resolution: "2K", |
||||
|
width: 1280, |
||||
|
height: 720, |
||||
|
batchSize: 4, |
||||
|
}; |
||||
|
const upstreamBody = { |
||||
|
success: true, |
||||
|
data: { |
||||
|
taskPrice: 24, |
||||
|
}, |
||||
|
}; |
||||
|
const fetchMock = vi.fn().mockResolvedValue( |
||||
|
new Response(JSON.stringify(upstreamBody), { |
||||
|
status: 200, |
||||
|
headers: { "Content-Type": "application/json" }, |
||||
|
}) |
||||
|
); |
||||
|
global.fetch = fetchMock; |
||||
|
|
||||
|
const response = await POST(createPostRequest(payload, { |
||||
|
Authorization: "Bearer user-token", |
||||
|
})); |
||||
|
|
||||
|
await expect(response.json()).resolves.toEqual(upstreamBody); |
||||
|
expect(response.status).toBe(200); |
||||
|
expect(fetchMock).toHaveBeenCalledWith( |
||||
|
"https://popi.example/api_client/anime/task/calculateTaskPrice", |
||||
|
{ |
||||
|
method: "POST", |
||||
|
headers: { |
||||
|
Accept: "application/json", |
||||
|
"Content-Type": "application/json", |
||||
|
Authorization: "Bearer user-token", |
||||
|
token: "user-token", |
||||
|
}, |
||||
|
body: JSON.stringify(payload), |
||||
|
cache: "no-store", |
||||
|
} |
||||
|
); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,56 @@ |
|||||
|
import { NextRequest, NextResponse } from "next/server"; |
||||
|
import { requireLogin } from "@/app/api/_auth"; |
||||
|
import { ApiError, apiErrorResponse } from "@/app/api/_errors"; |
||||
|
import { logGatewayRequest, logGatewayResponse } from "@/app/api/_gatewayLogging"; |
||||
|
|
||||
|
const UPSTREAM_CALCULATE_TASK_PRICE_PATH = "/api_client/anime/task/calculateTaskPrice"; |
||||
|
|
||||
|
function getUpstreamUrl(request: NextRequest): string { |
||||
|
const baseUrl = |
||||
|
process.env.POPIART_API_BASE_URL || |
||||
|
process.env.POPISERVER_BASE_URL || |
||||
|
process.env.NEXT_PUBLIC_APP_URL || |
||||
|
request.nextUrl.origin; |
||||
|
return baseUrl.replace(/\/+$/, "") + UPSTREAM_CALCULATE_TASK_PRICE_PATH; |
||||
|
} |
||||
|
|
||||
|
export async function POST(request: NextRequest) { |
||||
|
try { |
||||
|
const { token } = requireLogin(request); |
||||
|
const body = (await request.json().catch(() => null)) as Record<string, unknown> | null; |
||||
|
if (!body || typeof body !== "object") { |
||||
|
throw ApiError.badRequest("Task price request body is required"); |
||||
|
} |
||||
|
if (!body.aiModelId) { |
||||
|
throw ApiError.badRequest("aiModelId is required"); |
||||
|
} |
||||
|
|
||||
|
const url = getUpstreamUrl(request); |
||||
|
const init: RequestInit = { |
||||
|
method: "POST", |
||||
|
headers: { |
||||
|
Accept: "application/json", |
||||
|
"Content-Type": "application/json", |
||||
|
Authorization: `Bearer ${token}`, |
||||
|
token, |
||||
|
}, |
||||
|
body: JSON.stringify(body), |
||||
|
cache: "no-store", |
||||
|
}; |
||||
|
const startedAt = Date.now(); |
||||
|
logGatewayRequest("Calling task price API", url, init, { |
||||
|
aiModelId: body.aiModelId, |
||||
|
subType: body.subType, |
||||
|
}); |
||||
|
|
||||
|
const response = await fetch(url, init); |
||||
|
await logGatewayResponse("Task price API response", url, init, response, { |
||||
|
aiModelId: body.aiModelId, |
||||
|
subType: body.subType, |
||||
|
}, startedAt); |
||||
|
const payload = await response.json().catch(() => null); |
||||
|
return NextResponse.json(payload, { status: response.status }); |
||||
|
} catch (error) { |
||||
|
return apiErrorResponse(error, undefined, "Failed to calculate task price"); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue