You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

201 lines
6.0 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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-503customer 限流 → 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-504SQL 注入 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-505markup=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);
});
});