|
|
/**
|
|
|
* 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(
|
|
|
/询价会话已失效/,
|
|
|
);
|
|
|
});
|
|
|
});
|