diff --git a/PROJECT_AUDIT_2026-03-03.md b/PROJECT_AUDIT_2026-03-03.md index 3ed65a0..025045c 100644 --- a/PROJECT_AUDIT_2026-03-03.md +++ b/PROJECT_AUDIT_2026-03-03.md @@ -55,7 +55,11 @@ - 对 `undo/reset` 等相关接口同步增加 ID 合法性校验; - 增加“非法 restaurantId”单测。 -### P1-1 `suggest-item` 接口缺失鉴权(可被匿名滥用,产生外部 API 成本) +### P1-1 `suggest-item` 接口缺失鉴权(可被匿名滥用,产生外部 API 成本)【已完成】 +- 修复状态:✅ 已完成(2026-03-03) +- 修复内容: + - `POST /api/blindbox/plan/suggest-item` 增加登录鉴权(无登录态返回 401); + - 新增接口测试,覆盖“未登录拒绝访问”与“登录成功返回推荐”。 - 证据: - `src/app/api/blindbox/plan/suggest-item/route.ts:6-11`(无 `getAuthUserId` / 无 membership 校验) - 影响: diff --git a/src/app/api/blindbox/plan/suggest-item/route.test.ts b/src/app/api/blindbox/plan/suggest-item/route.test.ts new file mode 100644 index 0000000..bb556da --- /dev/null +++ b/src/app/api/blindbox/plan/suggest-item/route.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { createRequest, parseJsonResponse } from "@/__tests__/helpers/api-test-utils"; +import { ApiError } from "@/lib/api"; + +vi.mock("@/lib/ai", () => ({ + suggestAlternativeItems: vi.fn(), +})); + +vi.mock("@/lib/amap", () => ({ + searchPois: vi.fn(), +})); + +vi.mock("@/lib/auth", () => ({ + getAuthUserId: vi.fn(), +})); + +import { POST } from "./route"; +import { suggestAlternativeItems } from "@/lib/ai"; +import { searchPois } from "@/lib/amap"; +import { getAuthUserId } from "@/lib/auth"; + +const mockSuggestAlternativeItems = vi.mocked(suggestAlternativeItems); +const mockSearchPois = vi.mocked(searchPois); +const mockGetAuthUserId = vi.mocked(getAuthUserId); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe("POST /api/blindbox/plan/suggest-item", () => { + it("returns 401 when not authenticated", async () => { + mockGetAuthUserId.mockRejectedValue(new ApiError("请先登录", 401)); + + const req = createRequest("/api/blindbox/plan/suggest-item", { + method: "POST", + body: { activity: "看展" }, + }); + const res = await POST(req); + expect(res.status).toBe(401); + }); + + it("returns mapped suggestions for authenticated user", async () => { + mockGetAuthUserId.mockResolvedValue("user-1"); + mockSuggestAlternativeItems.mockResolvedValue([ + { + activity: "看展", + searchQuery: "上海博物馆", + reason: "交通方便", + }, + ]); + mockSearchPois.mockResolvedValue([ + { + name: "上海博物馆", + address: "黄浦区人民大道201号", + lat: 31.2301, + lng: 121.4737, + }, + ]); + + const req = createRequest("/api/blindbox/plan/suggest-item", { + method: "POST", + body: { + activity: "文艺活动", + time: "14:00", + location: "121.47,31.23", + }, + }); + const res = await POST(req); + const { status, data } = await parseJsonResponse(res); + + expect(status).toBe(200); + expect(data.suggestions).toHaveLength(1); + expect(data.suggestions[0]).toMatchObject({ + activity: "看展", + poi: "上海博物馆", + address: "黄浦区人民大道201号", + lat: 31.2301, + lng: 121.4737, + reason: "交通方便", + }); + }); +}); diff --git a/src/app/api/blindbox/plan/suggest-item/route.ts b/src/app/api/blindbox/plan/suggest-item/route.ts index 181c069..5950c1f 100644 --- a/src/app/api/blindbox/plan/suggest-item/route.ts +++ b/src/app/api/blindbox/plan/suggest-item/route.ts @@ -2,8 +2,10 @@ import { NextResponse } from "next/server"; import { apiHandler, ApiError } from "@/lib/api"; import { suggestAlternativeItems } from "@/lib/ai"; import { searchPois } from "@/lib/amap"; +import { getAuthUserId } from "@/lib/auth"; export const POST = apiHandler(async (req) => { + await getAuthUserId(req); const { activity, time, location } = await req.json(); if (!activity) throw new ApiError("activity 不能为空", 400);