|
|
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);
|
|
|
});
|
|
|
});
|