mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 20:49:53 +08:00
Two issues from before the provider refactor, found by its review. The redirect guard dropped Authorization, cookies and the providers' key headers when a redirect left the origin, but kept x-access-code, this site's own access code; it is dropped too now. edgeOneEndpoint took the request's Origin header as the site's address. Behind a proxy the request URL is the internal one and Origin is the site's, so Origin still wins, but only when its host matches Host or X-Forwarded-Host; a request carrying another site's Origin now gets the request's own origin, so the EdgeOne cookies and the access code never travel to that site.
154 lines
5.2 KiB
TypeScript
154 lines
5.2 KiB
TypeScript
// @vitest-environment node
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
|
|
|
vi.mock("@/lib/dynamo-quota-manager", () => ({
|
|
isQuotaEnabled: () => false,
|
|
checkAndIncrementRequest: async () => ({ allowed: true }),
|
|
recordTokenUsage: async () => {},
|
|
}))
|
|
|
|
import { POST as chat } from "@/app/api/chat/route"
|
|
|
|
const calls: Array<{ url: string; headers: Headers }> = []
|
|
|
|
beforeEach(() => {
|
|
vi.stubEnv("AI_PROVIDER", undefined)
|
|
vi.stubEnv("AI_MODEL", undefined)
|
|
calls.length = 0
|
|
vi.stubGlobal(
|
|
"fetch",
|
|
vi.fn(async (url: string, init: RequestInit) => {
|
|
calls.push({ url: String(url), headers: new Headers(init.headers) })
|
|
throw new Error("no network in tests")
|
|
}),
|
|
)
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
describe("EdgeOne as a server model", () => {
|
|
it("calls the site's Edge AI function with the cookies", async () => {
|
|
// An admin panel or ai-models.json provider: the client sends the
|
|
// provider name's slug, not "edgeone"
|
|
vi.stubEnv(
|
|
"AI_MODELS_CONFIG",
|
|
JSON.stringify({
|
|
providers: [
|
|
{
|
|
name: "Edge Pages",
|
|
provider: "edgeone",
|
|
models: ["@tx/deepseek-ai/deepseek-v3-0324"],
|
|
},
|
|
],
|
|
}),
|
|
)
|
|
const res = await chat(
|
|
new Request("http://localhost/api/chat", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"x-ai-provider": "edge-pages",
|
|
"x-selected-model-id":
|
|
"server:edge-pages:@tx/deepseek-ai/deepseek-v3-0324",
|
|
cookie: "eo_token=t; eo_time=1",
|
|
},
|
|
body: JSON.stringify({
|
|
messages: [
|
|
{
|
|
id: "u1",
|
|
role: "user",
|
|
parts: [{ type: "text", text: "Draw two boxes" }],
|
|
},
|
|
],
|
|
xml: "",
|
|
}),
|
|
}),
|
|
)
|
|
await res.text()
|
|
expect(calls[0]?.url).toBe(
|
|
"http://localhost/api/edgeai/chat/completions",
|
|
)
|
|
expect(calls[0]?.headers.get("cookie")).toBe("eo_token=t; eo_time=1")
|
|
})
|
|
})
|
|
|
|
const send = (headers: Record<string, string>) =>
|
|
chat(
|
|
new Request("http://localhost/api/chat", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", ...headers },
|
|
body: JSON.stringify({
|
|
messages: [
|
|
{
|
|
id: "u1",
|
|
role: "user",
|
|
parts: [{ type: "text", text: "Draw two boxes" }],
|
|
},
|
|
],
|
|
xml: "",
|
|
}),
|
|
}),
|
|
).then((r) => r.text())
|
|
|
|
describe("EdgeOne endpoints", () => {
|
|
it("works when the deployment names EdgeOne only in AI_PROVIDER", async () => {
|
|
vi.stubEnv("AI_PROVIDER", "edgeone")
|
|
vi.stubEnv("AI_MODEL", "@tx/deepseek-ai/deepseek-v3-0324")
|
|
await send({})
|
|
expect(calls[0]?.url).toBe(
|
|
"http://localhost/api/edgeai/chat/completions",
|
|
)
|
|
})
|
|
|
|
it("always calls the site's own function, whatever URL the request names", async () => {
|
|
// Another host would get the user's EdgeOne cookies
|
|
await send({
|
|
"x-ai-provider": "edgeone",
|
|
"x-ai-model": "@tx/deepseek-ai/deepseek-v3-0324",
|
|
"x-ai-base-url": "https://elsewhere.example/api/edgeai",
|
|
cookie: "eo_token=t",
|
|
})
|
|
expect(calls[0]?.url).toBe(
|
|
"http://localhost/api/edgeai/chat/completions",
|
|
)
|
|
})
|
|
|
|
it("calls the function at the site root, also with a base path", async () => {
|
|
vi.stubEnv("NEXT_PUBLIC_BASE_PATH", "/draw")
|
|
await send({
|
|
"x-ai-provider": "edgeone",
|
|
"x-ai-model": "@tx/deepseek-ai/deepseek-v3-0324",
|
|
})
|
|
expect(calls[0]?.url).toBe(
|
|
"http://localhost/api/edgeai/chat/completions",
|
|
)
|
|
})
|
|
})
|
|
|
|
describe("the EdgeOne endpoint's origin", () => {
|
|
it("takes the browser's Origin for the host the request was addressed to", async () => {
|
|
vi.stubEnv("AI_PROVIDER", "edgeone")
|
|
vi.stubEnv("AI_MODEL", "@tx/deepseek-ai/deepseek-v3-0324")
|
|
// Behind a proxy the request URL is internal; Origin names the site
|
|
await send({ origin: "https://draw.example", host: "draw.example" })
|
|
expect(calls[0]?.url).toBe(
|
|
"https://draw.example/api/edgeai/chat/completions",
|
|
)
|
|
})
|
|
|
|
it("ignores another site's Origin, so the cookies stay on this site", async () => {
|
|
vi.stubEnv("AI_PROVIDER", "edgeone")
|
|
vi.stubEnv("AI_MODEL", "@tx/deepseek-ai/deepseek-v3-0324")
|
|
await send({
|
|
origin: "https://attacker.example",
|
|
host: "localhost",
|
|
cookie: "eo_token=t",
|
|
})
|
|
expect(calls[0]?.url).toBe(
|
|
"http://localhost/api/edgeai/chat/completions",
|
|
)
|
|
})
|
|
})
|