users-reset-all-statistics.test.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import { beforeEach, describe, expect, test, vi } from "vitest";
  2. import { ERROR_CODES } from "@/lib/utils/error-messages";
  3. // Mock getSession
  4. const getSessionMock = vi.fn();
  5. vi.mock("@/lib/auth", () => ({
  6. getSession: getSessionMock,
  7. }));
  8. // Mock next-intl
  9. const getTranslationsMock = vi.fn(async () => (key: string) => key);
  10. vi.mock("next-intl/server", () => ({
  11. getTranslations: getTranslationsMock,
  12. getLocale: vi.fn(async () => "en"),
  13. }));
  14. // Mock next/cache
  15. const revalidatePathMock = vi.fn();
  16. vi.mock("next/cache", () => ({
  17. revalidatePath: revalidatePathMock,
  18. }));
  19. // Mock repository/user
  20. const findUserByIdMock = vi.fn();
  21. vi.mock("@/repository/user", async (importOriginal) => {
  22. const actual = await importOriginal<typeof import("@/repository/user")>();
  23. return {
  24. ...actual,
  25. findUserById: findUserByIdMock,
  26. };
  27. });
  28. // Mock repository/key
  29. const findKeyListMock = vi.fn();
  30. vi.mock("@/repository/key", async (importOriginal) => {
  31. const actual = await importOriginal<typeof import("@/repository/key")>();
  32. return {
  33. ...actual,
  34. findKeyList: findKeyListMock,
  35. };
  36. });
  37. // Mock drizzle db
  38. const dbDeleteWhereMock = vi.fn();
  39. const dbDeleteMock = vi.fn(() => ({ where: dbDeleteWhereMock }));
  40. vi.mock("@/drizzle/db", () => ({
  41. db: {
  42. delete: dbDeleteMock,
  43. },
  44. }));
  45. // Mock logger
  46. const loggerMock = {
  47. info: vi.fn(),
  48. warn: vi.fn(),
  49. error: vi.fn(),
  50. };
  51. vi.mock("@/lib/logger", () => ({
  52. logger: loggerMock,
  53. }));
  54. // Mock Redis
  55. const redisPipelineMock = {
  56. del: vi.fn().mockReturnThis(),
  57. exec: vi.fn(),
  58. };
  59. const redisMock = {
  60. status: "ready",
  61. pipeline: vi.fn(() => redisPipelineMock),
  62. };
  63. const getRedisClientMock = vi.fn(() => redisMock);
  64. vi.mock("@/lib/redis", () => ({
  65. getRedisClient: getRedisClientMock,
  66. }));
  67. // Mock scanPattern
  68. const scanPatternMock = vi.fn();
  69. vi.mock("@/lib/redis/scan-helper", () => ({
  70. scanPattern: scanPatternMock,
  71. }));
  72. describe("resetUserAllStatistics", () => {
  73. beforeEach(() => {
  74. vi.clearAllMocks();
  75. // Reset redis mock to ready state
  76. redisMock.status = "ready";
  77. redisPipelineMock.exec.mockResolvedValue([]);
  78. // DB delete returns resolved promise
  79. dbDeleteWhereMock.mockResolvedValue(undefined);
  80. });
  81. test("should return PERMISSION_DENIED for non-admin user", async () => {
  82. getSessionMock.mockResolvedValue({ user: { id: 1, role: "user" } });
  83. const { resetUserAllStatistics } = await import("@/actions/users");
  84. const result = await resetUserAllStatistics(123);
  85. expect(result.ok).toBe(false);
  86. expect(result.errorCode).toBe(ERROR_CODES.PERMISSION_DENIED);
  87. expect(findUserByIdMock).not.toHaveBeenCalled();
  88. });
  89. test("should return PERMISSION_DENIED when no session", async () => {
  90. getSessionMock.mockResolvedValue(null);
  91. const { resetUserAllStatistics } = await import("@/actions/users");
  92. const result = await resetUserAllStatistics(123);
  93. expect(result.ok).toBe(false);
  94. expect(result.errorCode).toBe(ERROR_CODES.PERMISSION_DENIED);
  95. });
  96. test("should return NOT_FOUND for non-existent user", async () => {
  97. getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
  98. findUserByIdMock.mockResolvedValue(null);
  99. const { resetUserAllStatistics } = await import("@/actions/users");
  100. const result = await resetUserAllStatistics(999);
  101. expect(result.ok).toBe(false);
  102. expect(result.errorCode).toBe(ERROR_CODES.NOT_FOUND);
  103. expect(dbDeleteMock).not.toHaveBeenCalled();
  104. });
  105. test("should successfully reset all user statistics", async () => {
  106. getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
  107. findUserByIdMock.mockResolvedValue({ id: 123, name: "Test User" });
  108. findKeyListMock.mockResolvedValue([{ id: 1 }, { id: 2 }]);
  109. scanPatternMock.mockResolvedValue(["key:1:cost_daily", "key:2:cost_weekly"]);
  110. redisPipelineMock.exec.mockResolvedValue([]);
  111. const { resetUserAllStatistics } = await import("@/actions/users");
  112. const result = await resetUserAllStatistics(123);
  113. expect(result.ok).toBe(true);
  114. // DB delete called
  115. expect(dbDeleteMock).toHaveBeenCalled();
  116. expect(dbDeleteWhereMock).toHaveBeenCalled();
  117. // Redis operations
  118. expect(redisMock.pipeline).toHaveBeenCalled();
  119. expect(redisPipelineMock.del).toHaveBeenCalled();
  120. expect(redisPipelineMock.exec).toHaveBeenCalled();
  121. // Revalidate path
  122. expect(revalidatePathMock).toHaveBeenCalledWith("/dashboard/users");
  123. // Logging
  124. expect(loggerMock.info).toHaveBeenCalled();
  125. });
  126. test("should succeed even when Redis is not ready", async () => {
  127. getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
  128. findUserByIdMock.mockResolvedValue({ id: 123, name: "Test User" });
  129. findKeyListMock.mockResolvedValue([{ id: 1 }]);
  130. redisMock.status = "connecting";
  131. const { resetUserAllStatistics } = await import("@/actions/users");
  132. const result = await resetUserAllStatistics(123);
  133. expect(result.ok).toBe(true);
  134. // DB delete still called
  135. expect(dbDeleteMock).toHaveBeenCalled();
  136. // Redis pipeline NOT called (status not ready)
  137. expect(redisMock.pipeline).not.toHaveBeenCalled();
  138. });
  139. test("should succeed with warning when Redis has partial failures", async () => {
  140. getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
  141. findUserByIdMock.mockResolvedValue({ id: 123, name: "Test User" });
  142. findKeyListMock.mockResolvedValue([{ id: 1 }]);
  143. scanPatternMock.mockResolvedValue(["key:1:cost_daily"]);
  144. // Simulate partial failure - some commands return errors
  145. redisPipelineMock.exec.mockResolvedValue([
  146. [null, 1], // success
  147. [new Error("Connection reset"), null], // failure
  148. ]);
  149. const { resetUserAllStatistics } = await import("@/actions/users");
  150. const result = await resetUserAllStatistics(123);
  151. expect(result.ok).toBe(true);
  152. expect(loggerMock.warn).toHaveBeenCalledWith(
  153. "Some Redis deletes failed during user statistics reset",
  154. expect.objectContaining({ errorCount: 1, userId: 123 })
  155. );
  156. });
  157. test("should succeed with warning when scanPattern fails", async () => {
  158. getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
  159. findUserByIdMock.mockResolvedValue({ id: 123, name: "Test User" });
  160. findKeyListMock.mockResolvedValue([{ id: 1 }]);
  161. // scanPattern fails but is caught by .catch() in Promise.all
  162. scanPatternMock.mockRejectedValue(new Error("Redis connection lost"));
  163. redisPipelineMock.exec.mockResolvedValue([]);
  164. const { resetUserAllStatistics } = await import("@/actions/users");
  165. const result = await resetUserAllStatistics(123);
  166. // Should still succeed - error is caught inside Promise.all
  167. expect(result.ok).toBe(true);
  168. expect(loggerMock.warn).toHaveBeenCalled();
  169. });
  170. test("should succeed with error log when pipeline.exec throws", async () => {
  171. getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
  172. findUserByIdMock.mockResolvedValue({ id: 123, name: "Test User" });
  173. findKeyListMock.mockResolvedValue([{ id: 1 }]);
  174. scanPatternMock.mockResolvedValue(["key:1:cost_daily"]);
  175. // pipeline.exec throws - caught by outer try-catch
  176. redisPipelineMock.exec.mockRejectedValue(new Error("Pipeline failed"));
  177. const { resetUserAllStatistics } = await import("@/actions/users");
  178. const result = await resetUserAllStatistics(123);
  179. // Should still succeed - DB logs already deleted
  180. expect(result.ok).toBe(true);
  181. expect(loggerMock.error).toHaveBeenCalledWith(
  182. "Failed to clear Redis cache during user statistics reset",
  183. expect.objectContaining({ userId: 123 })
  184. );
  185. });
  186. test("should return OPERATION_FAILED on unexpected error", async () => {
  187. getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
  188. findUserByIdMock.mockRejectedValue(new Error("Database connection failed"));
  189. const { resetUserAllStatistics } = await import("@/actions/users");
  190. const result = await resetUserAllStatistics(123);
  191. expect(result.ok).toBe(false);
  192. expect(result.errorCode).toBe(ERROR_CODES.OPERATION_FAILED);
  193. expect(loggerMock.error).toHaveBeenCalled();
  194. });
  195. test("should handle user with no keys", async () => {
  196. getSessionMock.mockResolvedValue({ user: { id: 1, role: "admin" } });
  197. findUserByIdMock.mockResolvedValue({ id: 123, name: "Test User" });
  198. findKeyListMock.mockResolvedValue([]); // No keys
  199. scanPatternMock.mockResolvedValue([]);
  200. redisPipelineMock.exec.mockResolvedValue([]);
  201. const { resetUserAllStatistics } = await import("@/actions/users");
  202. const result = await resetUserAllStatistics(123);
  203. expect(result.ok).toBe(true);
  204. expect(dbDeleteMock).toHaveBeenCalled();
  205. });
  206. });