import { beforeEach, describe, expect, it, vi } from "vitest"; import { POST } from "@/app/api/quotes/route"; vi.mock("@/modules/cache/redis-cache", () => ({ getL2: vi.fn(), setL2: vi.fn(), setL3: vi.fn(), })); vi.mock("@/modules/quote/idempotency", () => ({ checkIdempotency: vi.fn(), saveIdempotency: vi.fn(), })); vi.mock("@/modules/quote/quote-id", () => ({ generateQuoteId: vi.fn(), handleQuoteIdConflict: vi.fn(), isQuoteIdUniqueConflict: vi.fn(), })); vi.mock("@/modules/quote/rpa-queue", () => ({ enqueueQuoteJob: vi.fn(), })); vi.mock("@/modules/metrics/collector", () => ({ safeRecord: vi.fn(), recordPostTotal: vi.fn().mockResolvedValue(undefined), recordCacheHit: vi.fn().mockResolvedValue(undefined), recordPostDone: vi.fn().mockResolvedValue(undefined), recordRpaTriggered: vi.fn().mockResolvedValue(undefined), markQuoteStarted: vi.fn().mockResolvedValue(undefined), })); vi.mock("@/lib/api/rate-limit", () => ({ enforceRateLimits: vi.fn().mockResolvedValue(null), })); vi.mock("@/lib/prisma", () => ({ prisma: { quoteRecord: { create: vi.fn() }, markupConfig: { findFirst: vi.fn().mockResolvedValue(null) }, }, })); import { getL2 } from "@/modules/cache/redis-cache"; import { checkIdempotency } from "@/modules/quote/idempotency"; import { generateQuoteId } from "@/modules/quote/quote-id"; import { enqueueQuoteJob } from "@/modules/quote/rpa-queue"; import { prisma } from "@/lib/prisma"; import { resetServiceTokenCache } from "@/modules/auth/service-token"; import { buildCargoHash, serializeAddress } from "@/modules/quote/cargo-hash"; 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", }; const FOUR_TIERS = [ { service_level: "standard", rate_option: "lowest", raw_freight: 320, surcharges: 0, raw_total: 320, carrier: "M", transit_days: "5", transit_description: "x", }, { service_level: "standard", rate_option: "fastest", raw_freight: 365, surcharges: 0, raw_total: 365, carrier: "M", transit_days: "3", transit_description: "x", }, { service_level: "guaranteed", rate_option: "lowest", raw_freight: 380, surcharges: 0, raw_total: 380, carrier: "M", transit_days: "4", transit_description: "x", }, { service_level: "guaranteed", rate_option: "fastest", raw_freight: 410, surcharges: 0, raw_total: 410, carrier: "M", transit_days: "3", transit_description: "x", }, ]; beforeEach(() => { vi.clearAllMocks(); resetServiceTokenCache(); process.env.HOST_SERVICE_TOKENS = JSON.stringify({ "demo-host-token": { customerId: "CUST_001", permissions: ["pricing:markup:write"], }, }); vi.mocked(checkIdempotency).mockResolvedValue({ hit: false }); vi.mocked(getL2).mockResolvedValue(null); vi.mocked(generateQuoteId).mockResolvedValue("QTE_INT_0001"); vi.mocked(prisma.quoteRecord.create).mockResolvedValue({} as never); vi.mocked(enqueueQuoteJob).mockResolvedValue(undefined); }); describe("POST /api/quotes 集成(task-085)", () => { it("TC-101:首次询价 → processing + 入队 RPA", async () => { const response = await POST( new Request("http://localhost/api/quotes", { method: "POST", headers: { "Content-Type": "application/json", ...SERVICE_HEADERS }, body: JSON.stringify(VALID_BODY), }), ); const json = await response.json(); expect(response.status).toBe(200); expect(json.data.status).toBe("processing"); expect(enqueueQuoteJob).toHaveBeenCalled(); }); it("TC-104:L2 命中 → done + source=cache", async () => { vi.mocked(getL2).mockResolvedValue({ quotes: FOUR_TIERS }); const response = await POST( new Request("http://localhost/api/quotes", { method: "POST", headers: { "Content-Type": "application/json", ...SERVICE_HEADERS }, body: JSON.stringify(VALID_BODY), }), ); const json = await response.json(); expect(json.data.status).toBe("done"); expect(json.data.source_type).toBe("cache"); expect(enqueueQuoteJob).not.toHaveBeenCalled(); }); it("TC-105:L1 幂等 → 返回原 quote_id", async () => { vi.mocked(checkIdempotency).mockResolvedValue({ hit: true, quoteId: "QTE_EXISTING", response: { quote_id: "QTE_EXISTING", status: "done", source_type: "rpa", }, }); const response = await POST( new Request("http://localhost/api/quotes", { method: "POST", headers: { "Content-Type": "application/json", ...SERVICE_HEADERS }, body: JSON.stringify(VALID_BODY), }), ); const json = await response.json(); expect(json.data.quote_id).toBe("QTE_EXISTING"); expect(prisma.quoteRecord.create).not.toHaveBeenCalled(); }); it("TC-106/107:相同货物 hash 一致;service_level 不影响 hash", () => { const pickup = serializeAddress({ street: "1234 Warehouse Blvd", city: "Los Angeles", state: "CA", zip: "90001", place_id: "ChIJ_pickup", }); const delivery = serializeAddress({ street: "5678 Distribution Dr", city: "Dallas", state: "TX", zip: "75201", place_id: "ChIJ_delivery", }); const base = { pickupSerialized: pickup, deliverySerialized: delivery, weightLb: 500, dimLIn: 48, dimWIn: 40, dimHIn: 48, palletCount: 2, cargoType: "general_freight", }; const h1 = buildCargoHash(base); const h2 = buildCargoHash({ ...base, cargoType: "general_freight" }); expect(h1).toBe(h2); expect(h1).toHaveLength(32); }); });