httpUtil.test.ts 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import { beforeEach, describe, expect, it, vi } from 'vitest';
  2. const toast = vi.hoisted(() => ({
  3. success: vi.fn(),
  4. error: vi.fn(),
  5. warning: vi.fn(),
  6. info: vi.fn(),
  7. loading: vi.fn(),
  8. }));
  9. vi.mock('@/api/http-init', () => ({
  10. httpRequest: vi.fn(),
  11. HttpError: class HttpError extends Error {
  12. status: number;
  13. response: { status: number; statusText: string; data: unknown };
  14. constructor(status: number, statusText: string, data: unknown) {
  15. super(`Request failed with status ${status}`);
  16. this.status = status;
  17. this.response = { status, statusText, data };
  18. }
  19. },
  20. }));
  21. vi.mock('@/utils/messageBus', () => ({
  22. getMessage: () => toast,
  23. }));
  24. import { HttpUtil } from '@/utils';
  25. import { HttpError, httpRequest } from '@/api/http-init';
  26. import type { HttpResponse } from '@/api/http-init';
  27. const mockRequest = vi.mocked(httpRequest);
  28. const envelope = (data: unknown): HttpResponse => ({
  29. ok: true,
  30. status: 200,
  31. statusText: 'OK',
  32. data,
  33. });
  34. describe('HttpUtil', () => {
  35. beforeEach(() => {
  36. vi.clearAllMocks();
  37. vi.spyOn(console, 'error').mockImplementation(() => undefined);
  38. });
  39. it('unwraps a success envelope and shows a success toast', async () => {
  40. mockRequest.mockResolvedValue(envelope({ success: true, msg: 'done', obj: { id: 1 } }));
  41. const msg = await HttpUtil.post('/x', { a: 1 });
  42. expect(msg.success).toBe(true);
  43. expect(msg.obj).toEqual({ id: 1 });
  44. expect(toast.success).toHaveBeenCalledWith('done');
  45. });
  46. it('suppresses the success toast with silentSuccess but still warns on nodePending', async () => {
  47. mockRequest.mockResolvedValue(
  48. envelope({ success: true, msg: 'saved', obj: { nodePending: true } }),
  49. );
  50. await HttpUtil.post('/x', { a: 1 }, { silentSuccess: true });
  51. expect(toast.success).not.toHaveBeenCalled();
  52. expect(toast.warning).toHaveBeenCalled();
  53. });
  54. it('shows an error toast for a failure envelope', async () => {
  55. mockRequest.mockResolvedValue(envelope({ success: false, msg: 'nope', obj: null }));
  56. const msg = await HttpUtil.post('/x');
  57. expect(msg.success).toBe(false);
  58. expect(toast.error).toHaveBeenCalledWith('nope');
  59. });
  60. it('suppresses all toasts with silent', async () => {
  61. mockRequest.mockResolvedValue(envelope({ success: false, msg: 'nope', obj: null }));
  62. await HttpUtil.post('/x', undefined, { silent: true });
  63. expect(toast.error).not.toHaveBeenCalled();
  64. });
  65. it('surfaces the backend error text from a thrown HttpError body (msg field)', async () => {
  66. mockRequest.mockRejectedValue(
  67. new HttpError(400, 'Bad Request', { success: false, msg: 'bad input' }),
  68. );
  69. const msg = await HttpUtil.post('/x', undefined, { silent: true });
  70. expect(msg.success).toBe(false);
  71. expect(msg.msg).toBe('bad input');
  72. expect(console.error).not.toHaveBeenCalled();
  73. });
  74. it('maps a thrown native error to a failure Msg via its message', async () => {
  75. mockRequest.mockRejectedValue(new Error('Network down'));
  76. const msg = await HttpUtil.get('/x', undefined, { silent: true });
  77. expect(msg.msg).toBe('Network down');
  78. expect(console.error).not.toHaveBeenCalled();
  79. });
  80. it('returns "No response data" for an empty body', async () => {
  81. mockRequest.mockResolvedValue(envelope(''));
  82. const msg = await HttpUtil.get('/x', undefined, { silent: true });
  83. expect(msg.success).toBe(false);
  84. expect(msg.msg).toBe('No response data');
  85. });
  86. });