import { beforeEach, describe, expect, it, vi } from "vitest"; import { POST } from "@/app/api/quotes/route"; import { GET as getQuote } from "@/app/api/quotes/[quote_id]/route"; import { PUT as putMarkup } from "@/app/api/markup-configs/[customer_id]/route"; vi.mock("@/modules/quote/orchestrator", () => ({ submitQuote: vi.fn(), })); vi.mock("@/lib/api/rate-limit", () => ({ enforceRateLimits: vi.fn(), })); vi.mock("@/lib/prisma", () => ({ prisma: { quoteRecord: { findFirst: vi.fn() }, auditLog: { create: vi.fn() }, markupConfig: { upsert: vi.fn() }, }, })); vi.mock("@/modules/alert/service", () => ({ writeAlert: vi.fn(), })); import { submitQuote } from "@/modules/quote/orchestrator"; import { enforceRateLimits } from "@/lib/api/rate-limit"; import { prisma } from "@/lib/prisma"; import { writeAlert } from "@/modules/alert/service"; import { resetServiceTokenCache } from "@/modules/auth/service-token"; import { fail } from "@/lib/response"; import { MOTHERSHIP_DELIVERY_CONFIRMED, MOTHERSHIP_PICKUP_CONFIRMED, } from "@/__tests__/fixtures/mothership-address"; const SERVICE_HEADERS = { "x-auth-type": "service", "x-customer-id": "CUST_001", "x-permissions": "pricing:markup:write", }; const VALID_BODY = { request_id: "550e8400-e29b-41d4-a716-446655440000", customer_id: "CUST_001", pickup_address: { street: "1234 Warehouse Blvd", city: "Los Angeles", state: "CA", zip: "90001", place_id: "ChIJ_pickup", formatted_address: "1234 Warehouse Blvd, Los Angeles, CA 90001, USA", selected_from_suggestions: true, ...MOTHERSHIP_PICKUP_CONFIRMED, }, delivery_address: { street: "5678 Distribution Dr", city: "Dallas", state: "TX", zip: "75201", place_id: "ChIJ_delivery", formatted_address: "5678 Distribution Dr, Dallas, TX 75201, USA", selected_from_suggestions: true, ...MOTHERSHIP_DELIVERY_CONFIRMED, }, weight: { value: 500, unit: "lb" }, dimensions: { length: 48, width: 40, height: 48, unit: "in" }, pallet_count: 2, cargo_type: "general_freight", }; beforeEach(() => { vi.clearAllMocks(); resetServiceTokenCache(); process.env.HOST_SERVICE_TOKENS = JSON.stringify({ "demo-host-token": { customerId: "CUST_001", permissions: ["pricing:markup:write"], }, }); vi.mocked(enforceRateLimits).mockResolvedValue(null); vi.mocked(submitQuote).mockResolvedValue({ quote_id: "QTE_001", status: "done", source_type: "cache", }); vi.mocked(prisma.auditLog.create).mockResolvedValue({} as never); vi.mocked(prisma.markupConfig.upsert).mockResolvedValue({} as never); }); describe("安全与权限集成(task-086)", () => { it("TC-501:越权查询他人 quote → 403 + audit", async () => { vi.mocked(prisma.quoteRecord.findFirst).mockResolvedValue({ quoteId: "QTE_B", customerId: "CUST_B", requestId: "req", status: "done", currency: "USD", isRealtime: true, sourceType: "cache", confidenceScore: null, validUntil: new Date(Date.now() + 60_000), quotesJson: [], errorCode: null, createdAt: new Date(), updatedAt: new Date(), } as never); const response = await getQuote( new Request("http://localhost/api/quotes/QTE_B", { headers: SERVICE_HEADERS, }), { params: Promise.resolve({ quote_id: "QTE_B" }) }, ); expect(response.status).toBe(403); expect(prisma.auditLog.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ action: "FORBIDDEN_ACCESS" }), }), ); }); it("TC-502:无 pricing:markup:write → 403", async () => { const response = await putMarkup( new Request("http://localhost/api/markup-configs/CUST_001", { method: "PUT", headers: { "Content-Type": "application/json", "x-auth-type": "service", "x-customer-id": "CUST_001", "x-permissions": "", }, body: JSON.stringify({ markup_percent: 10, operator_id: "op1" }), }), { params: Promise.resolve({ customer_id: "CUST_001" }) }, ); expect(response.status).toBe(403); }); it("TC-503:customer 限流 → 429", async () => { vi.mocked(enforceRateLimits).mockResolvedValue( fail("RATE_LIMITED", "请求过于频繁,请稍后再试", 429), ); const response = await POST( new Request("http://localhost/api/quotes", { method: "POST", headers: { "Content-Type": "application/json", ...SERVICE_HEADERS, "x-forwarded-for": "10.0.0.1", }, body: JSON.stringify(VALID_BODY), }), ); expect(response.status).toBe(429); }); it("TC-504:SQL 注入 street → 400 + SECURITY 预警", async () => { const { SecurityValidationError } = await import("@/modules/quote/types"); vi.mocked(submitQuote).mockImplementation(() => { throw new SecurityValidationError("提货地址无效,请检查"); }); const response = await POST( new Request("http://localhost/api/quotes", { method: "POST", headers: { "Content-Type": "application/json", ...SERVICE_HEADERS }, body: JSON.stringify({ ...VALID_BODY, pickup_address: { ...VALID_BODY.pickup_address, street: "'; DROP TABLE quote_record; --", }, }), }), ); expect(response.status).toBe(400); await vi.waitFor(() => { expect(writeAlert).toHaveBeenCalledWith( "SECURITY", expect.any(Object), ); }); }); it("TC-505:markup=30.1 → 400", async () => { const response = await putMarkup( new Request("http://localhost/api/markup-configs/CUST_001", { method: "PUT", headers: { "Content-Type": "application/json", ...SERVICE_HEADERS }, body: JSON.stringify({ markup_percent: 30.1, operator_id: "op1" }), }), { params: Promise.resolve({ customer_id: "CUST_001" }) }, ); expect(response.status).toBe(400); }); });