2
0

availability-route.test.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import { beforeEach, describe, expect, it, vi } from "vitest";
  2. import { NextRequest } from "next/server";
  3. const mockGetSession = vi.hoisted(() => vi.fn());
  4. const mockQueryProviderAvailability = vi.hoisted(() => vi.fn());
  5. const MockAvailabilityQueryValidationError = vi.hoisted(
  6. () =>
  7. class AvailabilityQueryValidationError extends Error {
  8. constructor(message: string) {
  9. super(message);
  10. this.name = "AvailabilityQueryValidationError";
  11. }
  12. }
  13. );
  14. vi.mock("@/lib/auth", () => ({
  15. getSession: mockGetSession,
  16. }));
  17. vi.mock("@/lib/availability", () => ({
  18. AvailabilityQueryValidationError: MockAvailabilityQueryValidationError,
  19. MIN_BUCKET_SIZE_MINUTES: 0.25,
  20. MAX_BUCKETS_HARD_LIMIT: 100,
  21. MAX_BUCKET_SIZE_MINUTES: 1440,
  22. queryProviderAvailability: mockQueryProviderAvailability,
  23. }));
  24. function makeRequest(query = ""): NextRequest {
  25. const suffix = query ? `?${query}` : "";
  26. return new NextRequest(`http://localhost/api/availability${suffix}`);
  27. }
  28. describe("GET /api/availability", () => {
  29. let GET: (request: NextRequest) => Promise<Response>;
  30. beforeEach(async () => {
  31. vi.resetModules();
  32. vi.clearAllMocks();
  33. mockGetSession.mockResolvedValue({
  34. user: {
  35. id: 1,
  36. role: "admin",
  37. },
  38. });
  39. mockQueryProviderAvailability.mockResolvedValue({
  40. queriedAt: "2026-04-13T09:00:00.000Z",
  41. startTime: "2026-04-13T08:00:00.000Z",
  42. endTime: "2026-04-13T09:00:00.000Z",
  43. bucketSizeMinutes: 5,
  44. providers: [],
  45. systemAvailability: 0,
  46. });
  47. const mod = await import("@/app/api/availability/route");
  48. GET = mod.GET;
  49. });
  50. it("未认证时返回 401", async () => {
  51. mockGetSession.mockResolvedValueOnce(null);
  52. const res = await GET(makeRequest());
  53. expect(res.status).toBe(401);
  54. expect(await res.json()).toEqual({ error: "Unauthorized" });
  55. expect(mockQueryProviderAvailability).not.toHaveBeenCalled();
  56. });
  57. it("参数合法时将规范化后的查询参数传给 service", async () => {
  58. const res = await GET(
  59. makeRequest(
  60. [
  61. "startTime=2026-04-13T08:00:00.000Z",
  62. "endTime=2026-04-13T09:00:00.000Z",
  63. "providerIds=2,1,2",
  64. "bucketSizeMinutes=0.5",
  65. "includeDisabled=true",
  66. "maxBuckets=60",
  67. ].join("&")
  68. )
  69. );
  70. expect(res.status).toBe(200);
  71. expect(mockQueryProviderAvailability).toHaveBeenCalledTimes(1);
  72. expect(mockQueryProviderAvailability).toHaveBeenCalledWith({
  73. startTime: "2026-04-13T08:00:00.000Z",
  74. endTime: "2026-04-13T09:00:00.000Z",
  75. providerIds: [2, 1],
  76. bucketSizeMinutes: 0.5,
  77. includeDisabled: true,
  78. maxBuckets: 60,
  79. });
  80. });
  81. it("providerIds 非法时返回 400 且不访问 service", async () => {
  82. const res = await GET(makeRequest("providerIds=1,foo"));
  83. expect(res.status).toBe(400);
  84. expect(await res.json()).toEqual({
  85. error: "Invalid providerIds: expected a positive integer",
  86. });
  87. expect(mockQueryProviderAvailability).not.toHaveBeenCalled();
  88. });
  89. it("providerIds 存在空 token 时返回 400 且不访问 service", async () => {
  90. const res = await GET(makeRequest("providerIds=1,,2"));
  91. expect(res.status).toBe(400);
  92. expect(await res.json()).toEqual({
  93. error: "Invalid providerIds: expected comma-separated positive integers",
  94. });
  95. expect(mockQueryProviderAvailability).not.toHaveBeenCalled();
  96. });
  97. it("includeDisabled 非法时返回 400 且不访问 service", async () => {
  98. const res = await GET(makeRequest("includeDisabled=yes"));
  99. expect(res.status).toBe(400);
  100. expect(await res.json()).toEqual({
  101. error: "Invalid includeDisabled: expected true or false",
  102. });
  103. expect(mockQueryProviderAvailability).not.toHaveBeenCalled();
  104. });
  105. it("bucketSizeMinutes 为 Infinity 时返回 400 且不访问 service", async () => {
  106. const res = await GET(makeRequest("bucketSizeMinutes=Infinity"));
  107. expect(res.status).toBe(400);
  108. expect(await res.json()).toEqual({
  109. error: "Invalid bucketSizeMinutes: expected a positive number",
  110. });
  111. expect(mockQueryProviderAvailability).not.toHaveBeenCalled();
  112. });
  113. it("bucketSizeMinutes 低于最小值时返回 400 且不访问 service", async () => {
  114. const res = await GET(makeRequest("bucketSizeMinutes=0.001"));
  115. expect(res.status).toBe(400);
  116. expect(await res.json()).toEqual({
  117. error: "Invalid bucketSizeMinutes: expected a positive number not less than 0.25",
  118. });
  119. expect(mockQueryProviderAvailability).not.toHaveBeenCalled();
  120. });
  121. it("bucketSizeMinutes 超过硬上限时返回 400 且不访问 service", async () => {
  122. const res = await GET(makeRequest("bucketSizeMinutes=1441"));
  123. expect(res.status).toBe(400);
  124. expect(await res.json()).toEqual({
  125. error: "Invalid bucketSizeMinutes: expected a positive number not greater than 1440",
  126. });
  127. expect(mockQueryProviderAvailability).not.toHaveBeenCalled();
  128. });
  129. it("maxBuckets 超过硬上限时返回 400 且不访问 service", async () => {
  130. const res = await GET(makeRequest("maxBuckets=101"));
  131. expect(res.status).toBe(400);
  132. expect(await res.json()).toEqual({
  133. error: "Invalid maxBuckets: expected a positive integer not greater than 100",
  134. });
  135. expect(mockQueryProviderAvailability).not.toHaveBeenCalled();
  136. });
  137. it("空的 startTime 参数返回 400 且不访问 service", async () => {
  138. const res = await GET(makeRequest("startTime="));
  139. expect(res.status).toBe(400);
  140. expect(await res.json()).toEqual({
  141. error: "Invalid startTime: expected a valid Date or ISO timestamp",
  142. });
  143. expect(mockQueryProviderAvailability).not.toHaveBeenCalled();
  144. });
  145. it("service 抛出参数校验错误时映射为 400", async () => {
  146. mockQueryProviderAvailability.mockRejectedValueOnce(
  147. new MockAvailabilityQueryValidationError(
  148. "Invalid time range: endTime must be greater than or equal to startTime"
  149. )
  150. );
  151. const res = await GET(
  152. makeRequest("startTime=2026-04-13T09:00:00.000Z&endTime=2026-04-13T08:00:00.000Z")
  153. );
  154. expect(res.status).toBe(400);
  155. expect(await res.json()).toEqual({
  156. error: "Invalid time range: endTime must be greater than or equal to startTime",
  157. });
  158. });
  159. it("service 抛出非校验错误时返回 500", async () => {
  160. mockQueryProviderAvailability.mockRejectedValueOnce(new Error("db down"));
  161. const res = await GET(makeRequest());
  162. expect(res.status).toBe(500);
  163. expect(await res.json()).toEqual({ error: "Internal server error" });
  164. });
  165. });