|
|
import { describe, expect, it } from "vitest";
|
|
|
import {
|
|
|
MOTHERSHIP_LIMITS,
|
|
|
validateQuoteInput,
|
|
|
rejectQuantityField,
|
|
|
} from "@/modules/quote/validation";
|
|
|
import { ValidationError } from "@/modules/quote/types";
|
|
|
import {
|
|
|
MOTHERSHIP_DELIVERY_CONFIRMED,
|
|
|
MOTHERSHIP_PICKUP_CONFIRMED,
|
|
|
} from "@/__tests__/fixtures/mothership-address";
|
|
|
|
|
|
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",
|
|
|
};
|
|
|
|
|
|
describe("validateQuoteInput", () => {
|
|
|
it("正常入参通过", () => {
|
|
|
const result = validateQuoteInput(VALID_BODY);
|
|
|
expect(result.cargoHash).toHaveLength(32);
|
|
|
expect(result.weightLb).toBe(500);
|
|
|
});
|
|
|
|
|
|
it("保留业务客户标识但不进入 cargoHash", () => {
|
|
|
const a = validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
business_customer_id: "BC_001",
|
|
|
});
|
|
|
const b = validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
business_customer_id: "BC_002",
|
|
|
});
|
|
|
expect(a.businessCustomerId).toBe("BC_001");
|
|
|
expect(b.businessCustomerId).toBe("BC_002");
|
|
|
expect(a.cargoHash).toBe(b.cargoHash);
|
|
|
});
|
|
|
|
|
|
it("TC-201:weight 缺失 → VALIDATION_FAILED", () => {
|
|
|
const { weight: _w, ...rest } = VALID_BODY;
|
|
|
expect(() => validateQuoteInput(rest)).toThrow(ValidationError);
|
|
|
});
|
|
|
|
|
|
it("TC-202:weight=0 → 无效", () => {
|
|
|
expect(() =>
|
|
|
validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
weight: { value: 0, unit: "lb" },
|
|
|
}),
|
|
|
).toThrow(/有效的重量|超出允许范围/);
|
|
|
});
|
|
|
|
|
|
it("空邮编通过(MotherShip 不要求填写邮编)", () => {
|
|
|
const result = validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
pickup_address: { ...VALID_BODY.pickup_address, zip: "" },
|
|
|
delivery_address: { ...VALID_BODY.delivery_address, zip: "" },
|
|
|
});
|
|
|
expect(result.cargoHash).toHaveLength(32);
|
|
|
});
|
|
|
|
|
|
it("TC-203:zip 4 位 → 无效", () => {
|
|
|
expect(() =>
|
|
|
validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
pickup_address: {
|
|
|
...VALID_BODY.pickup_address,
|
|
|
zip: "9001",
|
|
|
},
|
|
|
}),
|
|
|
).toThrow(/邮编/);
|
|
|
});
|
|
|
|
|
|
it("TC-204:request_id 非 UUID → 无效", () => {
|
|
|
expect(() =>
|
|
|
validateQuoteInput({ ...VALID_BODY, request_id: "not-uuid" }),
|
|
|
).toThrow("请求标识无效");
|
|
|
});
|
|
|
|
|
|
it("E4.3:selected_from_suggestions=false", () => {
|
|
|
expect(() =>
|
|
|
validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
pickup_address: {
|
|
|
...VALID_BODY.pickup_address,
|
|
|
selected_from_suggestions: false,
|
|
|
},
|
|
|
}),
|
|
|
).toThrow(ValidationError);
|
|
|
});
|
|
|
|
|
|
it("未确认 MotherShip 精确地址 → 拒绝询价", () => {
|
|
|
const { mothership_option_id: _a, mothership_display_label: _b, selected_from_mothership: _c, ...pickup } =
|
|
|
VALID_BODY.pickup_address;
|
|
|
expect(() =>
|
|
|
validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
pickup_address: pickup,
|
|
|
}),
|
|
|
).toThrow(ValidationError);
|
|
|
});
|
|
|
|
|
|
it("E4.7:quantity 字段拒绝", () => {
|
|
|
expect(() =>
|
|
|
rejectQuantityField({ ...VALID_BODY, quantity: 5 }),
|
|
|
).toThrow("请使用托盘数 pallet_count");
|
|
|
});
|
|
|
|
|
|
it("E4.6:pallet_count 可为 >25(已取消 1–25 硬限)", () => {
|
|
|
const result = validateQuoteInput({ ...VALID_BODY, pallet_count: 26 });
|
|
|
expect(result.palletCount).toBe(26);
|
|
|
});
|
|
|
|
|
|
it("E4.6:pallet_count 非正整数拒绝", () => {
|
|
|
expect(() =>
|
|
|
validateQuoteInput({ ...VALID_BODY, pallet_count: 0 }),
|
|
|
).toThrow(/托盘数须为正整数/);
|
|
|
});
|
|
|
|
|
|
it("E4.6:单托重量超 9999", () => {
|
|
|
expect(() =>
|
|
|
validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
weight: { value: 10_000, unit: "lb" },
|
|
|
}),
|
|
|
).toThrow(/重量超出/);
|
|
|
});
|
|
|
|
|
|
it("E4.6:长度超 999 in", () => {
|
|
|
expect(() =>
|
|
|
validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
dimensions: { length: 1000, width: 40, height: 48, unit: "in" },
|
|
|
}),
|
|
|
).toThrow(/尺寸超出/);
|
|
|
});
|
|
|
|
|
|
it("边界:25 托、9999 lb、999×99×99 in 通过", () => {
|
|
|
const result = validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
weight: { value: 9_999, unit: "lb" },
|
|
|
dimensions: { length: 999, width: 99, height: 99, unit: "in" },
|
|
|
pallet_count: 25,
|
|
|
});
|
|
|
expect(result.palletCount).toBe(25);
|
|
|
});
|
|
|
|
|
|
it("SQL 注入拦截", () => {
|
|
|
expect(() =>
|
|
|
validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
pickup_address: {
|
|
|
...VALID_BODY.pickup_address,
|
|
|
street: "'; DROP TABLE users; --",
|
|
|
},
|
|
|
}),
|
|
|
).toThrow(/无效,请检查/);
|
|
|
});
|
|
|
|
|
|
it("kg/cm 换算并向上取整", () => {
|
|
|
const result = validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
weight: { value: 227, unit: "kg" },
|
|
|
dimensions: { length: 120, width: 100, height: 100, unit: "cm" },
|
|
|
});
|
|
|
expect(result.weightLb).toBe(501);
|
|
|
expect(result.dimLIn).toBe(48);
|
|
|
expect(result.dimWIn).toBe(40);
|
|
|
expect(result.dimHIn).toBe(40);
|
|
|
});
|
|
|
|
|
|
it("500 kg 换算为 1103 lb", () => {
|
|
|
const result = validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
weight: { value: 500, unit: "kg" },
|
|
|
});
|
|
|
expect(result.weightLb).toBe(1103);
|
|
|
});
|
|
|
|
|
|
it("0.5 t 换算为 1103 lb", () => {
|
|
|
const result = validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
weight: { value: 0.5, unit: "t" },
|
|
|
});
|
|
|
expect(result.weightLb).toBe(1103);
|
|
|
});
|
|
|
|
|
|
it("1.2 m 尺寸换算并向上取整", () => {
|
|
|
const result = validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
dimensions: { length: 1.2, width: 1, height: 1, unit: "m" },
|
|
|
});
|
|
|
expect(result.dimLIn).toBe(48);
|
|
|
expect(result.dimWIn).toBe(40);
|
|
|
expect(result.dimHIn).toBe(40);
|
|
|
});
|
|
|
|
|
|
it("拒绝 ton/tons 歧义单位", () => {
|
|
|
expect(() =>
|
|
|
validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
weight: { value: 0.5, unit: "ton" },
|
|
|
}),
|
|
|
).toThrow(/歧义/);
|
|
|
});
|
|
|
|
|
|
it("拒绝 Infinity 重量", () => {
|
|
|
expect(() =>
|
|
|
validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
weight: { value: Number.POSITIVE_INFINITY, unit: "lb" },
|
|
|
}),
|
|
|
).toThrow(ValidationError);
|
|
|
});
|
|
|
|
|
|
it("登录态 cargo_lines:托数>25 不硬拒,单件均重≤5000 可通过", () => {
|
|
|
const result = validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
pallet_count: 40,
|
|
|
weight: { value: 400, unit: "lb" },
|
|
|
cargo_lines: [
|
|
|
{
|
|
|
cargo_type: "pallet",
|
|
|
quantity: 40,
|
|
|
weight_lb: 400,
|
|
|
length_in: 48,
|
|
|
width_in: 40,
|
|
|
height_in: 48,
|
|
|
},
|
|
|
],
|
|
|
});
|
|
|
expect(result.palletCount).toBe(40);
|
|
|
});
|
|
|
|
|
|
it("二级刷价仅 mothership_details:托数>25 不硬拒(无 cargo_lines)", () => {
|
|
|
const result = validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
pallet_count: 107,
|
|
|
mothership_details: {
|
|
|
pickup: {
|
|
|
company_name: "Acme",
|
|
|
contact_email: "a@b.com",
|
|
|
contact_phone: "555",
|
|
|
opens_at: "8:00 AM",
|
|
|
closes_at: "5:00 PM",
|
|
|
},
|
|
|
delivery: {
|
|
|
company_name: "Beta",
|
|
|
contact_email: "c@d.com",
|
|
|
contact_phone: "555",
|
|
|
opens_at: "9:00 AM",
|
|
|
closes_at: "5:00 PM",
|
|
|
},
|
|
|
},
|
|
|
});
|
|
|
expect(result.palletCount).toBe(107);
|
|
|
});
|
|
|
|
|
|
it("登录态 cargo_lines:混装托盘+纸箱,pallet_count=托盘合计可通过", () => {
|
|
|
const result = validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
pallet_count: 16,
|
|
|
weight: { value: 12, unit: "lb" },
|
|
|
dimensions: { length: 2, width: 2, height: 3, unit: "in" },
|
|
|
cargo_lines: [
|
|
|
{
|
|
|
cargo_type: "carton",
|
|
|
quantity: 4,
|
|
|
weight_lb: 12,
|
|
|
length_in: 2,
|
|
|
width_in: 2,
|
|
|
height_in: 3,
|
|
|
},
|
|
|
{
|
|
|
cargo_type: "pallet",
|
|
|
quantity: 2,
|
|
|
weight_lb: 6,
|
|
|
length_in: 12,
|
|
|
width_in: 12,
|
|
|
height_in: 12,
|
|
|
},
|
|
|
{
|
|
|
cargo_type: "pallet",
|
|
|
quantity: 5,
|
|
|
weight_lb: 31,
|
|
|
length_in: 31,
|
|
|
width_in: 31,
|
|
|
height_in: 31,
|
|
|
},
|
|
|
{
|
|
|
cargo_type: "carton",
|
|
|
quantity: 22,
|
|
|
weight_lb: 24,
|
|
|
length_in: 23,
|
|
|
width_in: 23,
|
|
|
height_in: 23,
|
|
|
},
|
|
|
{
|
|
|
cargo_type: "carton",
|
|
|
quantity: 33,
|
|
|
weight_lb: 33,
|
|
|
length_in: 33,
|
|
|
width_in: 33,
|
|
|
height_in: 33,
|
|
|
},
|
|
|
{
|
|
|
cargo_type: "pallet",
|
|
|
quantity: 6,
|
|
|
weight_lb: 56,
|
|
|
length_in: 55,
|
|
|
width_in: 55,
|
|
|
height_in: 55,
|
|
|
},
|
|
|
{
|
|
|
cargo_type: "pallet",
|
|
|
quantity: 3,
|
|
|
weight_lb: 66,
|
|
|
length_in: 66,
|
|
|
width_in: 66,
|
|
|
height_in: 66,
|
|
|
},
|
|
|
{
|
|
|
cargo_type: "carton",
|
|
|
quantity: 32,
|
|
|
weight_lb: 69,
|
|
|
length_in: 21,
|
|
|
width_in: 12,
|
|
|
height_in: 55,
|
|
|
},
|
|
|
],
|
|
|
});
|
|
|
expect(result.palletCount).toBe(16);
|
|
|
expect(result.cargoLines?.length).toBe(8);
|
|
|
});
|
|
|
|
|
|
it("登录态 cargo_lines:单件均重>5000 拒绝", () => {
|
|
|
expect(() =>
|
|
|
validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
pallet_count: 1,
|
|
|
weight: { value: 5001, unit: "lb" },
|
|
|
cargo_lines: [
|
|
|
{
|
|
|
cargo_type: "pallet",
|
|
|
quantity: 1,
|
|
|
weight_lb: 5001,
|
|
|
length_in: 48,
|
|
|
width_in: 40,
|
|
|
height_in: 48,
|
|
|
},
|
|
|
],
|
|
|
}),
|
|
|
).toThrow(/单件平均重量超过 5000/);
|
|
|
});
|
|
|
|
|
|
it("登录态 cargo_lines:整票总重>45000 拒绝", () => {
|
|
|
expect(() =>
|
|
|
validateQuoteInput({
|
|
|
...VALID_BODY,
|
|
|
pallet_count: 40,
|
|
|
weight: { value: 1200, unit: "lb" },
|
|
|
cargo_lines: [
|
|
|
{
|
|
|
cargo_type: "pallet",
|
|
|
quantity: 40,
|
|
|
weight_lb: 1200,
|
|
|
length_in: 48,
|
|
|
width_in: 40,
|
|
|
height_in: 48,
|
|
|
},
|
|
|
],
|
|
|
}),
|
|
|
).toThrow(/53 英尺货车最大载重/);
|
|
|
});
|
|
|
});
|
|
|
|
|
|
describe("MOTHERSHIP_LIMITS", () => {
|
|
|
it("托盘数不再以 25 为硬上限", () => {
|
|
|
expect(MOTHERSHIP_LIMITS.palletCount.min).toBe(1);
|
|
|
expect(MOTHERSHIP_LIMITS.palletCount.max).toBeGreaterThan(25);
|
|
|
});
|
|
|
});
|