users-action-get-users-compat.test.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import { beforeEach, describe, expect, test, vi } from "vitest";
  2. import type { User } from "@/types/user";
  3. const getSessionMock = vi.fn();
  4. vi.mock("@/lib/auth", () => ({
  5. getSession: getSessionMock,
  6. }));
  7. vi.mock("next/cache", () => ({
  8. revalidatePath: vi.fn(),
  9. }));
  10. const getTranslationsMock = vi.fn(async () => (key: string) => key);
  11. const getLocaleMock = vi.fn(async () => "en");
  12. vi.mock("next-intl/server", () => ({
  13. getTranslations: getTranslationsMock,
  14. getLocale: getLocaleMock,
  15. }));
  16. const findUserByIdMock = vi.fn();
  17. const findUserListBatchMock = vi.fn();
  18. vi.mock("@/repository/user", async (importOriginal) => {
  19. const actual = await importOriginal<typeof import("@/repository/user")>();
  20. return {
  21. ...actual,
  22. findUserById: findUserByIdMock,
  23. findUserListBatch: findUserListBatchMock,
  24. };
  25. });
  26. const findKeyListBatchMock = vi.fn();
  27. const findKeyUsageTodayBatchMock = vi.fn();
  28. const findKeysStatisticsBatchFromKeysMock = vi.fn();
  29. vi.mock("@/repository/key", async (importOriginal) => {
  30. const actual = await importOriginal<typeof import("@/repository/key")>();
  31. return {
  32. ...actual,
  33. findKeyListBatch: findKeyListBatchMock,
  34. findKeyUsageTodayBatch: findKeyUsageTodayBatchMock,
  35. findKeysStatisticsBatchFromKeys: findKeysStatisticsBatchFromKeysMock,
  36. };
  37. });
  38. function makeUser(id: number, name = `user-${id}`): User {
  39. return {
  40. id,
  41. name,
  42. description: `${name}-desc`,
  43. role: "user",
  44. rpm: null,
  45. dailyQuota: null,
  46. providerGroup: null,
  47. tags: [],
  48. createdAt: new Date("2026-03-01T00:00:00.000Z"),
  49. updatedAt: new Date("2026-03-01T00:00:00.000Z"),
  50. deletedAt: undefined,
  51. dailyResetMode: "fixed",
  52. dailyResetTime: "00:00",
  53. isEnabled: true,
  54. expiresAt: null,
  55. allowedClients: [],
  56. blockedClients: [],
  57. allowedModels: [],
  58. };
  59. }
  60. describe("getUsers compatibility", () => {
  61. beforeEach(() => {
  62. getSessionMock.mockReset();
  63. findUserByIdMock.mockReset();
  64. findUserListBatchMock.mockReset();
  65. findKeyListBatchMock.mockReset();
  66. findKeyUsageTodayBatchMock.mockReset();
  67. findKeysStatisticsBatchFromKeysMock.mockReset();
  68. getSessionMock.mockResolvedValue({
  69. user: { id: 1, role: "admin" },
  70. key: { canLoginWebUi: true },
  71. });
  72. findKeyListBatchMock.mockResolvedValue(new Map());
  73. findKeyUsageTodayBatchMock.mockResolvedValue(new Map());
  74. findKeysStatisticsBatchFromKeysMock.mockResolvedValue(new Map());
  75. });
  76. test("loads all admin users instead of stopping at the first 50", async () => {
  77. const firstPageUsers = Array.from({ length: 200 }, (_, index) => makeUser(index + 1));
  78. const secondPageUser = makeUser(201, "after-first-200");
  79. findUserListBatchMock
  80. .mockResolvedValueOnce({
  81. users: firstPageUsers,
  82. nextCursor: '{"v":"2026-03-01T00:00:00.000Z","id":200}',
  83. hasMore: true,
  84. })
  85. .mockResolvedValueOnce({
  86. users: [secondPageUser],
  87. nextCursor: null,
  88. hasMore: false,
  89. });
  90. const { getUsers } = await import("@/actions/users");
  91. const result = await getUsers();
  92. expect(findUserListBatchMock).toHaveBeenNthCalledWith(1, {
  93. cursor: undefined,
  94. searchTerm: undefined,
  95. tagFilters: undefined,
  96. keyGroupFilters: undefined,
  97. statusFilter: undefined,
  98. limit: 200,
  99. sortBy: undefined,
  100. sortOrder: undefined,
  101. });
  102. expect(findUserListBatchMock).toHaveBeenNthCalledWith(2, {
  103. cursor: '{"v":"2026-03-01T00:00:00.000Z","id":200}',
  104. searchTerm: undefined,
  105. tagFilters: undefined,
  106. keyGroupFilters: undefined,
  107. statusFilter: undefined,
  108. limit: 200,
  109. sortBy: undefined,
  110. sortOrder: undefined,
  111. });
  112. expect(result).toHaveLength(201);
  113. expect(result.at(-1)?.name).toBe("after-first-200");
  114. });
  115. test("normalizes legacy getUsers page and query params", async () => {
  116. findUserListBatchMock.mockResolvedValueOnce({
  117. users: [makeUser(51, "xiaolunanbei")],
  118. nextCursor: null,
  119. hasMore: false,
  120. });
  121. const { getUsers } = await import("@/actions/users");
  122. const result = await getUsers({
  123. page: 2,
  124. limit: 50,
  125. query: " 小鹿楠贝 ",
  126. });
  127. expect(findUserListBatchMock).toHaveBeenCalledWith({
  128. cursor: "50",
  129. limit: 50,
  130. searchTerm: "小鹿楠贝",
  131. tagFilters: undefined,
  132. keyGroupFilters: undefined,
  133. statusFilter: undefined,
  134. sortBy: undefined,
  135. sortOrder: undefined,
  136. });
  137. expect(result).toHaveLength(1);
  138. expect(result[0]?.name).toBe("xiaolunanbei");
  139. });
  140. test("falls back to legacy query when searchTerm is blank", async () => {
  141. findUserListBatchMock.mockResolvedValueOnce({
  142. users: [makeUser(77, "legacy-query-hit")],
  143. nextCursor: null,
  144. hasMore: false,
  145. });
  146. const { getUsersBatch } = await import("@/actions/users");
  147. await getUsersBatch({
  148. searchTerm: " ",
  149. query: " alice ",
  150. });
  151. expect(findUserListBatchMock).toHaveBeenCalledWith({
  152. cursor: undefined,
  153. limit: undefined,
  154. searchTerm: "alice",
  155. tagFilters: undefined,
  156. keyGroupFilters: undefined,
  157. statusFilter: undefined,
  158. sortBy: undefined,
  159. sortOrder: undefined,
  160. });
  161. });
  162. test("search-only getUsers requests keep paging until all matches are returned", async () => {
  163. findUserListBatchMock
  164. .mockResolvedValueOnce({
  165. users: Array.from({ length: 200 }, (_, index) => makeUser(index + 1, `match-${index + 1}`)),
  166. nextCursor: '{"v":"2026-03-01T00:00:00.000Z","id":200}',
  167. hasMore: true,
  168. })
  169. .mockResolvedValueOnce({
  170. users: [makeUser(201, "match-201")],
  171. nextCursor: null,
  172. hasMore: false,
  173. });
  174. const { getUsers } = await import("@/actions/users");
  175. const result = await getUsers({ query: "match" });
  176. expect(findUserListBatchMock).toHaveBeenNthCalledWith(1, {
  177. cursor: undefined,
  178. limit: 200,
  179. searchTerm: "match",
  180. tagFilters: undefined,
  181. keyGroupFilters: undefined,
  182. statusFilter: undefined,
  183. sortBy: undefined,
  184. sortOrder: undefined,
  185. });
  186. expect(findUserListBatchMock).toHaveBeenNthCalledWith(2, {
  187. cursor: '{"v":"2026-03-01T00:00:00.000Z","id":200}',
  188. limit: 200,
  189. searchTerm: "match",
  190. tagFilters: undefined,
  191. keyGroupFilters: undefined,
  192. statusFilter: undefined,
  193. sortBy: undefined,
  194. sortOrder: undefined,
  195. });
  196. expect(result).toHaveLength(201);
  197. expect(result.at(-1)?.name).toBe("match-201");
  198. });
  199. test("treats whitespace cursor as missing pagination and keeps loading matches", async () => {
  200. findUserListBatchMock
  201. .mockResolvedValueOnce({
  202. users: Array.from({ length: 200 }, (_, index) =>
  203. makeUser(index + 1, `cursor-match-${index + 1}`)
  204. ),
  205. nextCursor: '{"v":"2026-03-01T00:00:00.000Z","id":200}',
  206. hasMore: true,
  207. })
  208. .mockResolvedValueOnce({
  209. users: [makeUser(201, "cursor-match-201")],
  210. nextCursor: null,
  211. hasMore: false,
  212. });
  213. const { getUsers } = await import("@/actions/users");
  214. const result = await getUsers({
  215. cursor: " ",
  216. query: "cursor-match",
  217. });
  218. expect(findUserListBatchMock).toHaveBeenNthCalledWith(1, {
  219. cursor: undefined,
  220. limit: 200,
  221. searchTerm: "cursor-match",
  222. tagFilters: undefined,
  223. keyGroupFilters: undefined,
  224. statusFilter: undefined,
  225. sortBy: undefined,
  226. sortOrder: undefined,
  227. });
  228. expect(findUserListBatchMock).toHaveBeenNthCalledWith(2, {
  229. cursor: '{"v":"2026-03-01T00:00:00.000Z","id":200}',
  230. limit: 200,
  231. searchTerm: "cursor-match",
  232. tagFilters: undefined,
  233. keyGroupFilters: undefined,
  234. statusFilter: undefined,
  235. sortBy: undefined,
  236. sortOrder: undefined,
  237. });
  238. expect(result).toHaveLength(201);
  239. expect(result.at(-1)?.name).toBe("cursor-match-201");
  240. });
  241. test("normalizes legacy getUsersBatch keyword and offset params", async () => {
  242. findUserListBatchMock.mockResolvedValueOnce({
  243. users: [makeUser(88, "keyword-hit")],
  244. nextCursor: null,
  245. hasMore: false,
  246. });
  247. const { getUsersBatch } = await import("@/actions/users");
  248. const result = await getUsersBatch({
  249. offset: 75,
  250. limit: 25,
  251. keyword: " key-word ",
  252. });
  253. expect(findUserListBatchMock).toHaveBeenCalledWith({
  254. cursor: "75",
  255. limit: 25,
  256. searchTerm: "key-word",
  257. tagFilters: undefined,
  258. keyGroupFilters: undefined,
  259. statusFilter: undefined,
  260. sortBy: undefined,
  261. sortOrder: undefined,
  262. });
  263. expect(result).toEqual({
  264. ok: true,
  265. data: {
  266. users: [
  267. expect.objectContaining({
  268. id: 88,
  269. name: "keyword-hit",
  270. }),
  271. ],
  272. nextCursor: null,
  273. hasMore: false,
  274. },
  275. });
  276. });
  277. });