| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
- import { PROVIDER_BATCH_PATCH_ERROR_CODES } from "../../../src/lib/provider-batch-patch-error-codes";
- import { buildRedisMock, createRedisStore } from "./redis-mock-utils";
- const getSessionMock = vi.fn();
- const deleteProvidersBatchMock = vi.fn();
- const restoreProvidersBatchMock = vi.fn();
- const publishCacheInvalidationMock = vi.fn();
- const clearProviderStateMock = vi.fn();
- const clearConfigCacheMock = vi.fn();
- const { store: redisStore, mocks: redisMocks } = createRedisStore();
- vi.mock("@/lib/auth", () => ({
- getSession: getSessionMock,
- }));
- vi.mock("@/repository/provider", () => ({
- deleteProvidersBatch: deleteProvidersBatchMock,
- findAllProvidersFresh: vi.fn(),
- updateProvidersBatch: vi.fn(),
- }));
- vi.mock("@/repository", () => ({
- restoreProvidersBatch: restoreProvidersBatchMock,
- }));
- vi.mock("@/lib/cache/provider-cache", () => ({
- publishProviderCacheInvalidation: publishCacheInvalidationMock,
- }));
- vi.mock("@/lib/circuit-breaker", () => ({
- clearProviderState: clearProviderStateMock,
- clearConfigCache: clearConfigCacheMock,
- resetCircuit: vi.fn(),
- getAllHealthStatusAsync: vi.fn(),
- }));
- vi.mock("@/lib/redis/client", () => buildRedisMock(redisMocks));
- vi.mock("@/lib/logger", () => ({
- logger: {
- trace: vi.fn(),
- debug: vi.fn(),
- info: vi.fn(),
- warn: vi.fn(),
- error: vi.fn(),
- },
- }));
- describe("Provider Delete Undo Actions", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- vi.resetModules();
- redisStore.clear();
- getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
- deleteProvidersBatchMock.mockResolvedValue(2);
- restoreProvidersBatchMock.mockResolvedValue(2);
- publishCacheInvalidationMock.mockResolvedValue(undefined);
- clearProviderStateMock.mockReturnValue(undefined);
- clearConfigCacheMock.mockReturnValue(undefined);
- });
- afterEach(() => {
- vi.useRealTimers();
- });
- it("batchDeleteProviders should return undoToken and operationId", async () => {
- const { batchDeleteProviders } = await import("../../../src/actions/providers");
- const result = await batchDeleteProviders({ providerIds: [3, 1, 3] });
- expect(result.ok).toBe(true);
- if (!result.ok) return;
- expect(deleteProvidersBatchMock).toHaveBeenCalledWith([1, 3]);
- expect(result.data.deletedCount).toBe(2);
- expect(result.data.undoToken).toMatch(/^provider_patch_undo_/);
- expect(result.data.operationId).toMatch(/^provider_patch_apply_/);
- });
- it("batchDeleteProviders should return repository errors", async () => {
- deleteProvidersBatchMock.mockRejectedValueOnce(new Error("delete failed"));
- const { batchDeleteProviders } = await import("../../../src/actions/providers");
- const result = await batchDeleteProviders({ providerIds: [7] });
- expect(result.ok).toBe(false);
- if (result.ok) return;
- expect(result.error).toBe("delete failed");
- });
- it("batchDeleteProviders should reject non-admin session", async () => {
- getSessionMock.mockResolvedValueOnce({ user: { id: 3, role: "user" } });
- const { batchDeleteProviders } = await import("../../../src/actions/providers");
- const result = await batchDeleteProviders({ providerIds: [1] });
- expect(result.ok).toBe(false);
- if (result.ok) return;
- expect(result.error).toBe("无权限执行此操作");
- expect(deleteProvidersBatchMock).not.toHaveBeenCalled();
- });
- it("batchDeleteProviders should reject empty provider list", async () => {
- const { batchDeleteProviders } = await import("../../../src/actions/providers");
- const result = await batchDeleteProviders({ providerIds: [] });
- expect(result.ok).toBe(false);
- if (result.ok) return;
- expect(result.error).toBe("请选择要删除的供应商");
- expect(deleteProvidersBatchMock).not.toHaveBeenCalled();
- });
- it("batchDeleteProviders should reject provider lists over max size", async () => {
- const { batchDeleteProviders } = await import("../../../src/actions/providers");
- const result = await batchDeleteProviders({
- providerIds: Array.from({ length: 501 }, (_, index) => index + 1),
- });
- expect(result.ok).toBe(false);
- if (result.ok) return;
- expect(result.error).toContain("单次批量操作最多支持");
- expect(deleteProvidersBatchMock).not.toHaveBeenCalled();
- });
- it("undoProviderDelete should restore providers by snapshot", async () => {
- const { batchDeleteProviders, undoProviderDelete } = await import(
- "../../../src/actions/providers"
- );
- const deleted = await batchDeleteProviders({ providerIds: [2, 4] });
- if (!deleted.ok) throw new Error(`Delete should succeed: ${deleted.error}`);
- restoreProvidersBatchMock.mockClear();
- publishCacheInvalidationMock.mockClear();
- clearProviderStateMock.mockClear();
- clearConfigCacheMock.mockClear();
- const undone = await undoProviderDelete({
- undoToken: deleted.data.undoToken,
- operationId: deleted.data.operationId,
- });
- expect(undone.ok).toBe(true);
- if (!undone.ok) return;
- expect(restoreProvidersBatchMock).toHaveBeenCalledWith([2, 4]);
- expect(undone.data.operationId).toBe(deleted.data.operationId);
- expect(undone.data.restoredCount).toBe(2);
- expect(clearProviderStateMock).toHaveBeenCalledTimes(2);
- expect(clearConfigCacheMock).toHaveBeenCalledTimes(2);
- expect(publishCacheInvalidationMock).toHaveBeenCalledTimes(1);
- });
- it("undoProviderDelete should expire after 61 seconds", async () => {
- vi.useFakeTimers();
- vi.setSystemTime(new Date("2026-02-19T00:00:00.000Z"));
- const { batchDeleteProviders, undoProviderDelete } = await import(
- "../../../src/actions/providers"
- );
- const deleted = await batchDeleteProviders({ providerIds: [9] });
- if (!deleted.ok) throw new Error(`Delete should succeed: ${deleted.error}`);
- restoreProvidersBatchMock.mockClear();
- vi.advanceTimersByTime(61_000);
- const undone = await undoProviderDelete({
- undoToken: deleted.data.undoToken,
- operationId: deleted.data.operationId,
- });
- expect(undone.ok).toBe(false);
- if (undone.ok) return;
- expect(undone.errorCode).toBe(PROVIDER_BATCH_PATCH_ERROR_CODES.UNDO_EXPIRED);
- expect(restoreProvidersBatchMock).not.toHaveBeenCalled();
- });
- it("undoProviderDelete should reject mismatched operation id", async () => {
- const { batchDeleteProviders, undoProviderDelete } = await import(
- "../../../src/actions/providers"
- );
- const deleted = await batchDeleteProviders({ providerIds: [10, 11] });
- if (!deleted.ok) throw new Error(`Delete should succeed: ${deleted.error}`);
- restoreProvidersBatchMock.mockClear();
- const undone = await undoProviderDelete({
- undoToken: deleted.data.undoToken,
- operationId: `${deleted.data.operationId}-mismatch`,
- });
- expect(undone.ok).toBe(false);
- if (undone.ok) return;
- expect(undone.errorCode).toBe(PROVIDER_BATCH_PATCH_ERROR_CODES.UNDO_CONFLICT);
- expect(restoreProvidersBatchMock).not.toHaveBeenCalled();
- });
- it("undoProviderDelete should reject invalid payload", async () => {
- const { undoProviderDelete } = await import("../../../src/actions/providers");
- const undone = await undoProviderDelete({
- undoToken: "",
- operationId: "provider_patch_apply_x",
- });
- expect(undone.ok).toBe(false);
- if (undone.ok) return;
- expect(undone.errorCode).toBeDefined();
- expect(restoreProvidersBatchMock).not.toHaveBeenCalled();
- });
- it("undoProviderDelete should reject non-admin session", async () => {
- getSessionMock.mockResolvedValueOnce({ user: { id: 2, role: "user" } });
- const { undoProviderDelete } = await import("../../../src/actions/providers");
- const undone = await undoProviderDelete({
- undoToken: "provider_patch_undo_x",
- operationId: "provider_patch_apply_x",
- });
- expect(undone.ok).toBe(false);
- if (undone.ok) return;
- expect(undone.error).toBe("无权限执行此操作");
- expect(restoreProvidersBatchMock).not.toHaveBeenCalled();
- });
- it("undoProviderDelete should return repository errors when restore fails", async () => {
- const { batchDeleteProviders, undoProviderDelete } = await import(
- "../../../src/actions/providers"
- );
- const deleted = await batchDeleteProviders({ providerIds: [12] });
- if (!deleted.ok) throw new Error(`Delete should succeed: ${deleted.error}`);
- restoreProvidersBatchMock.mockRejectedValueOnce(new Error("restore failed"));
- const undone = await undoProviderDelete({
- undoToken: deleted.data.undoToken,
- operationId: deleted.data.operationId,
- });
- expect(undone.ok).toBe(false);
- if (undone.ok) return;
- expect(undone.error).toBe("restore failed");
- });
- });
|