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.

170 lines
5.2 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.

/**
* Flock hold-service:继续 / 放弃 / 超时 / 越权
*/
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("@/modules/address/parked-session-queue", () => ({
requestReleaseParkedSession: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("@/lib/flock/flock-quote-hold-store", () => ({
assertFlockQuoteHoldUsable: vi.fn(),
clearFlockQuoteHold: vi.fn().mockResolvedValue(undefined),
readFlockQuoteHold: vi.fn(),
updateFlockQuoteHoldStatus: vi.fn(),
}));
import { requestReleaseParkedSession } from "@/modules/address/parked-session-queue";
import {
assertFlockQuoteHoldUsable,
clearFlockQuoteHold,
readFlockQuoteHold,
updateFlockQuoteHoldStatus,
} from "@/lib/flock/flock-quote-hold-store";
import {
FLOCK_HOLD_DECISION_MS,
FLOCK_HOLD_MSG,
FLOCK_HOLD_TOTAL_MS,
buildFlockQuoteHoldState,
} from "@/lib/constants/flock-quote-hold";
import {
continueFlockQuoteHold,
declineFlockQuoteHold,
} from "@/modules/flock/hold-service";
const SESSION = "flock_q-hold-1";
const BASE = {
customerId: "CUST_001",
quoteId: "q-hold-1",
sessionId: SESSION,
};
function makeState(
patch?: Partial<ReturnType<typeof buildFlockQuoteHoldState>>,
) {
const now = Date.now();
return {
...buildFlockQuoteHoldState({
quoteId: BASE.quoteId,
sessionId: SESSION,
customerId: BASE.customerId,
nowMs: now,
}),
...patch,
};
}
describe("continueFlockQuoteHold / declineFlockQuoteHold", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("decline:释放驻留并清理 Redis", async () => {
vi.mocked(readFlockQuoteHold).mockResolvedValue(makeState());
vi.mocked(updateFlockQuoteHoldStatus).mockResolvedValue(
makeState({ status: "declined" }),
);
const r = await declineFlockQuoteHold(BASE);
expect(r.released).toBe(true);
expect(requestReleaseParkedSession).toHaveBeenCalledWith(SESSION);
expect(clearFlockQuoteHold).toHaveBeenCalledWith(SESSION);
});
it("decline:客户不匹配 → 无权", async () => {
vi.mocked(readFlockQuoteHold).mockResolvedValue(
makeState({ customer_id: "OTHER" }),
);
await expect(declineFlockQuoteHold(BASE)).rejects.toThrow(/无权/);
});
it("decline:quote 不匹配 → 报错", async () => {
vi.mocked(readFlockQuoteHold).mockResolvedValue(
makeState({ quote_id: "other-q" }),
);
await expect(declineFlockQuoteHold(BASE)).rejects.toThrow(/不匹配/);
});
it("continue:awaiting → filling", async () => {
vi.mocked(assertFlockQuoteHoldUsable).mockResolvedValue(makeState());
vi.mocked(updateFlockQuoteHoldStatus).mockResolvedValue(
makeState({ status: "filling" }),
);
const r = await continueFlockQuoteHold(BASE);
expect(r.status).toBe("filling");
expect(updateFlockQuoteHoldStatus).toHaveBeenCalledWith(
SESSION,
"filling",
);
});
it("continue:已 filling / checking_out → 幂等返回", async () => {
vi.mocked(assertFlockQuoteHoldUsable).mockResolvedValue(
makeState({ status: "filling" }),
);
const r = await continueFlockQuoteHold(BASE);
expect(r.status).toBe("filling");
expect(updateFlockQuoteHoldStatus).not.toHaveBeenCalled();
vi.mocked(assertFlockQuoteHoldUsable).mockResolvedValue(
makeState({ status: "checking_out" }),
);
await expect(continueFlockQuoteHold(BASE)).resolves.toEqual({
status: "filling",
});
});
it("continue:60s 决策窗已过 → 自动 decline + 中文超时", async () => {
const parked = 1_000_000;
const state = makeState({
parked_at_ms: parked,
decision_deadline_ms: parked + FLOCK_HOLD_DECISION_MS,
total_deadline_ms: parked + FLOCK_HOLD_TOTAL_MS,
status: "awaiting_decision",
});
// 模拟「现在」已过决策窗:直接构造已到期的 deadline
const expiredDecision = {
...state,
decision_deadline_ms: Date.now() - 1,
total_deadline_ms: Date.now() + 60_000,
};
vi.mocked(assertFlockQuoteHoldUsable).mockResolvedValue(expiredDecision);
vi.mocked(readFlockQuoteHold).mockResolvedValue(expiredDecision);
vi.mocked(updateFlockQuoteHoldStatus).mockResolvedValue({
...expiredDecision,
status: "declined",
});
await expect(continueFlockQuoteHold(BASE)).rejects.toThrow(
FLOCK_HOLD_MSG.decisionTimeoutApi,
);
expect(requestReleaseParkedSession).toHaveBeenCalled();
});
it("continue:5min 总硬限已过 → totalTimeoutApi", async () => {
const expired = makeState({
status: "filling",
total_deadline_ms: Date.now() - 1,
decision_deadline_ms: Date.now() - 120_000,
});
vi.mocked(assertFlockQuoteHoldUsable).mockResolvedValue(expired);
vi.mocked(readFlockQuoteHold).mockResolvedValue(expired);
vi.mocked(updateFlockQuoteHoldStatus).mockResolvedValue({
...expired,
status: "declined",
});
await expect(continueFlockQuoteHold(BASE)).rejects.toThrow(
FLOCK_HOLD_MSG.totalTimeoutApi,
);
});
it("continue:会话不存在 → assert 抛出", async () => {
vi.mocked(assertFlockQuoteHoldUsable).mockRejectedValue(
new Error(FLOCK_HOLD_MSG.sessionGone),
);
await expect(continueFlockQuoteHold(BASE)).rejects.toThrow(
/询价会话已失效/,
);
});
});