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