|
1 |
| -import { withOAuth, withLogging, applyMiddleware, withWrappers } from './fetchWrapper.js'; |
| 1 | +import { withOAuth, withLogging, applyMiddleware, withWrappers, createMiddleware } from './fetchWrapper.js'; |
2 | 2 | import { OAuthClientProvider } from '../client/auth.js';
|
3 | 3 | import { FetchLike } from './transport.js';
|
4 | 4 |
|
@@ -898,3 +898,211 @@ describe('Integration Tests', () => {
|
898 | 898 | });
|
899 | 899 | });
|
900 | 900 | });
|
| 901 | + |
| 902 | +describe('createMiddleware', () => { |
| 903 | + let mockFetch: jest.MockedFunction<FetchLike>; |
| 904 | + |
| 905 | + beforeEach(() => { |
| 906 | + jest.clearAllMocks(); |
| 907 | + mockFetch = jest.fn(); |
| 908 | + }); |
| 909 | + |
| 910 | + it('should create middleware with cleaner syntax', async () => { |
| 911 | + const response = new Response('success', { status: 200 }); |
| 912 | + mockFetch.mockResolvedValue(response); |
| 913 | + |
| 914 | + const customMiddleware = createMiddleware(async (next, input, init) => { |
| 915 | + const headers = new Headers(init?.headers); |
| 916 | + headers.set('X-Custom-Header', 'custom-value'); |
| 917 | + return next(input, { ...init, headers }); |
| 918 | + }); |
| 919 | + |
| 920 | + const enhancedFetch = customMiddleware(mockFetch); |
| 921 | + await enhancedFetch('https://api.example.com/data'); |
| 922 | + |
| 923 | + expect(mockFetch).toHaveBeenCalledWith( |
| 924 | + 'https://api.example.com/data', |
| 925 | + expect.objectContaining({ |
| 926 | + headers: expect.any(Headers), |
| 927 | + }) |
| 928 | + ); |
| 929 | + |
| 930 | + const callArgs = mockFetch.mock.calls[0]; |
| 931 | + const headers = callArgs[1]?.headers as Headers; |
| 932 | + expect(headers.get('X-Custom-Header')).toBe('custom-value'); |
| 933 | + }); |
| 934 | + |
| 935 | + it('should support conditional middleware logic', async () => { |
| 936 | + const apiResponse = new Response('api response', { status: 200 }); |
| 937 | + const publicResponse = new Response('public response', { status: 200 }); |
| 938 | + mockFetch |
| 939 | + .mockResolvedValueOnce(apiResponse) |
| 940 | + .mockResolvedValueOnce(publicResponse); |
| 941 | + |
| 942 | + const conditionalMiddleware = createMiddleware(async (next, input, init) => { |
| 943 | + const url = typeof input === 'string' ? input : input.toString(); |
| 944 | + |
| 945 | + if (url.includes('/api/')) { |
| 946 | + const headers = new Headers(init?.headers); |
| 947 | + headers.set('X-API-Version', 'v2'); |
| 948 | + return next(input, { ...init, headers }); |
| 949 | + } |
| 950 | + |
| 951 | + return next(input, init); |
| 952 | + }); |
| 953 | + |
| 954 | + const enhancedFetch = conditionalMiddleware(mockFetch); |
| 955 | + |
| 956 | + // Test API route |
| 957 | + await enhancedFetch('https://example.com/api/users'); |
| 958 | + let callArgs = mockFetch.mock.calls[0]; |
| 959 | + let headers = callArgs[1]?.headers as Headers; |
| 960 | + expect(headers.get('X-API-Version')).toBe('v2'); |
| 961 | + |
| 962 | + // Test non-API route |
| 963 | + await enhancedFetch('https://example.com/public/page'); |
| 964 | + callArgs = mockFetch.mock.calls[1]; |
| 965 | + const maybeHeaders = callArgs[1]?.headers as Headers | undefined; |
| 966 | + expect(maybeHeaders?.get('X-API-Version')).toBeUndefined(); |
| 967 | + }); |
| 968 | + |
| 969 | + it('should support short-circuit responses', async () => { |
| 970 | + const customMiddleware = createMiddleware(async (next, input, init) => { |
| 971 | + const url = typeof input === 'string' ? input : input.toString(); |
| 972 | + |
| 973 | + // Short-circuit for specific URL |
| 974 | + if (url.includes('/cached')) { |
| 975 | + return new Response('cached data', { status: 200 }); |
| 976 | + } |
| 977 | + |
| 978 | + return next(input, init); |
| 979 | + }); |
| 980 | + |
| 981 | + const enhancedFetch = customMiddleware(mockFetch); |
| 982 | + |
| 983 | + // Test cached route (should not call mockFetch) |
| 984 | + const cachedResponse = await enhancedFetch('https://example.com/cached/data'); |
| 985 | + expect(await cachedResponse.text()).toBe('cached data'); |
| 986 | + expect(mockFetch).not.toHaveBeenCalled(); |
| 987 | + |
| 988 | + // Test normal route |
| 989 | + mockFetch.mockResolvedValue(new Response('fresh data', { status: 200 })); |
| 990 | + const freshResponse = await enhancedFetch('https://example.com/fresh/data'); |
| 991 | + expect(mockFetch).toHaveBeenCalledTimes(1); |
| 992 | + }); |
| 993 | + |
| 994 | + it('should handle response transformation', async () => { |
| 995 | + const originalResponse = new Response('{"data": "original"}', { |
| 996 | + status: 200, |
| 997 | + headers: { 'Content-Type': 'application/json' } |
| 998 | + }); |
| 999 | + mockFetch.mockResolvedValue(originalResponse); |
| 1000 | + |
| 1001 | + const transformMiddleware = createMiddleware(async (next, input, init) => { |
| 1002 | + const response = await next(input, init); |
| 1003 | + |
| 1004 | + if (response.headers.get('content-type')?.includes('application/json')) { |
| 1005 | + const data = await response.json(); |
| 1006 | + const transformed = { ...data, timestamp: 123456789 }; |
| 1007 | + |
| 1008 | + return new Response(JSON.stringify(transformed), { |
| 1009 | + status: response.status, |
| 1010 | + statusText: response.statusText, |
| 1011 | + headers: response.headers |
| 1012 | + }); |
| 1013 | + } |
| 1014 | + |
| 1015 | + return response; |
| 1016 | + }); |
| 1017 | + |
| 1018 | + const enhancedFetch = transformMiddleware(mockFetch); |
| 1019 | + const response = await enhancedFetch('https://api.example.com/data'); |
| 1020 | + const result = await response.json(); |
| 1021 | + |
| 1022 | + expect(result).toEqual({ |
| 1023 | + data: 'original', |
| 1024 | + timestamp: 123456789 |
| 1025 | + }); |
| 1026 | + }); |
| 1027 | + |
| 1028 | + it('should support error handling and recovery', async () => { |
| 1029 | + let attemptCount = 0; |
| 1030 | + mockFetch.mockImplementation(async () => { |
| 1031 | + attemptCount++; |
| 1032 | + if (attemptCount === 1) { |
| 1033 | + throw new Error('Network error'); |
| 1034 | + } |
| 1035 | + return new Response('success', { status: 200 }); |
| 1036 | + }); |
| 1037 | + |
| 1038 | + const retryMiddleware = createMiddleware(async (next, input, init) => { |
| 1039 | + try { |
| 1040 | + return await next(input, init); |
| 1041 | + } catch (error) { |
| 1042 | + // Retry once on network error |
| 1043 | + console.log('Retrying request after error:', error); |
| 1044 | + return await next(input, init); |
| 1045 | + } |
| 1046 | + }); |
| 1047 | + |
| 1048 | + const enhancedFetch = retryMiddleware(mockFetch); |
| 1049 | + const response = await enhancedFetch('https://api.example.com/data'); |
| 1050 | + |
| 1051 | + expect(await response.text()).toBe('success'); |
| 1052 | + expect(mockFetch).toHaveBeenCalledTimes(2); |
| 1053 | + }); |
| 1054 | + |
| 1055 | + it('should compose well with other middleware', async () => { |
| 1056 | + const response = new Response('success', { status: 200 }); |
| 1057 | + mockFetch.mockResolvedValue(response); |
| 1058 | + |
| 1059 | + // Create custom middleware using createMiddleware |
| 1060 | + const customAuth = createMiddleware(async (next, input, init) => { |
| 1061 | + const headers = new Headers(init?.headers); |
| 1062 | + headers.set('Authorization', 'Custom token'); |
| 1063 | + return next(input, { ...init, headers }); |
| 1064 | + }); |
| 1065 | + |
| 1066 | + const customLogging = createMiddleware(async (next, input, init) => { |
| 1067 | + const url = typeof input === 'string' ? input : input.toString(); |
| 1068 | + console.log(`Request to: ${url}`); |
| 1069 | + const response = await next(input, init); |
| 1070 | + console.log(`Response status: ${response.status}`); |
| 1071 | + return response; |
| 1072 | + }); |
| 1073 | + |
| 1074 | + // Compose with existing middleware |
| 1075 | + const enhancedFetch = applyMiddleware( |
| 1076 | + customAuth, |
| 1077 | + customLogging, |
| 1078 | + withLogging({ statusLevel: 400 }) |
| 1079 | + )(mockFetch); |
| 1080 | + |
| 1081 | + await enhancedFetch('https://api.example.com/data'); |
| 1082 | + |
| 1083 | + const callArgs = mockFetch.mock.calls[0]; |
| 1084 | + const headers = callArgs[1]?.headers as Headers; |
| 1085 | + expect(headers.get('Authorization')).toBe('Custom token'); |
| 1086 | + }); |
| 1087 | + |
| 1088 | + it('should have access to both input types (string and URL)', async () => { |
| 1089 | + const response = new Response('success', { status: 200 }); |
| 1090 | + mockFetch.mockResolvedValue(response); |
| 1091 | + |
| 1092 | + let capturedInputType: string | undefined; |
| 1093 | + const inspectMiddleware = createMiddleware(async (next, input, init) => { |
| 1094 | + capturedInputType = typeof input === 'string' ? 'string' : 'URL'; |
| 1095 | + return next(input, init); |
| 1096 | + }); |
| 1097 | + |
| 1098 | + const enhancedFetch = inspectMiddleware(mockFetch); |
| 1099 | + |
| 1100 | + // Test with string input |
| 1101 | + await enhancedFetch('https://api.example.com/data'); |
| 1102 | + expect(capturedInputType).toBe('string'); |
| 1103 | + |
| 1104 | + // Test with URL input |
| 1105 | + await enhancedFetch(new URL('https://api.example.com/data')); |
| 1106 | + expect(capturedInputType).toBe('URL'); |
| 1107 | + }); |
| 1108 | +}); |
0 commit comments