Skip to content

Commit 18f81b7

Browse files
dcalhounclaude
andcommitted
test(js): add nativeMediaUploadMiddleware tests
Tests covering passthrough behavior (missing port, non-POST, non-media paths, sub-paths, non-FormData), upload interception with Relay-Authorization auth, response transformation to WordPress REST API shape, error handling (413 file too large, generic failures), and abort signal forwarding. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a638cb9 commit 18f81b7

1 file changed

Lines changed: 379 additions & 0 deletions

File tree

Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { describe, it, expect, vi, beforeEach } from 'vitest';
5+
6+
/**
7+
* Internal dependencies
8+
*/
9+
import { nativeMediaUploadMiddleware } from './api-fetch';
10+
11+
// Mock dependencies
12+
vi.mock( './bridge', () => ( {
13+
getGBKit: vi.fn( () => ( {} ) ),
14+
} ) );
15+
16+
vi.mock( './logger', () => ( {
17+
info: vi.fn(),
18+
error: vi.fn(),
19+
} ) );
20+
21+
import { getGBKit } from './bridge';
22+
23+
function makeNext() {
24+
return vi.fn( () => Promise.resolve( { passthrough: true } ) );
25+
}
26+
27+
function makePostMediaOptions( file ) {
28+
const body = new FormData();
29+
if ( file ) {
30+
body.append( 'file', file, file.name );
31+
}
32+
return {
33+
method: 'POST',
34+
path: '/wp/v2/media',
35+
body,
36+
};
37+
}
38+
39+
function makeFile( name = 'photo.jpg', type = 'image/jpeg' ) {
40+
return new File( [ 'fake data' ], name, { type } );
41+
}
42+
43+
describe( 'nativeMediaUploadMiddleware', () => {
44+
beforeEach( () => {
45+
vi.restoreAllMocks();
46+
global.fetch = vi.fn();
47+
} );
48+
49+
// MARK: - Passthrough cases
50+
51+
it( 'passes through when nativeUploadPort is not configured', () => {
52+
getGBKit.mockReturnValue( {} );
53+
const next = makeNext();
54+
55+
nativeMediaUploadMiddleware( makePostMediaOptions( makeFile() ), next );
56+
57+
expect( next ).toHaveBeenCalled();
58+
expect( global.fetch ).not.toHaveBeenCalled();
59+
} );
60+
61+
it( 'passes through for non-POST requests', () => {
62+
getGBKit.mockReturnValue( {
63+
nativeUploadPort: 8080,
64+
nativeUploadToken: 'token',
65+
} );
66+
const next = makeNext();
67+
68+
nativeMediaUploadMiddleware(
69+
{ method: 'GET', path: '/wp/v2/media', body: new FormData() },
70+
next
71+
);
72+
73+
expect( next ).toHaveBeenCalled();
74+
expect( global.fetch ).not.toHaveBeenCalled();
75+
} );
76+
77+
it( 'passes through for non-media paths', () => {
78+
getGBKit.mockReturnValue( {
79+
nativeUploadPort: 8080,
80+
nativeUploadToken: 'token',
81+
} );
82+
const next = makeNext();
83+
84+
nativeMediaUploadMiddleware(
85+
{ method: 'POST', path: '/wp/v2/posts', body: new FormData() },
86+
next
87+
);
88+
89+
expect( next ).toHaveBeenCalled();
90+
expect( global.fetch ).not.toHaveBeenCalled();
91+
} );
92+
93+
it( 'passes through for media sub-paths like /wp/v2/media/123', () => {
94+
getGBKit.mockReturnValue( {
95+
nativeUploadPort: 8080,
96+
nativeUploadToken: 'token',
97+
} );
98+
const next = makeNext();
99+
const body = new FormData();
100+
body.append( 'file', makeFile(), 'photo.jpg' );
101+
102+
nativeMediaUploadMiddleware(
103+
{ method: 'POST', path: '/wp/v2/media/123', body },
104+
next
105+
);
106+
107+
expect( next ).toHaveBeenCalled();
108+
expect( global.fetch ).not.toHaveBeenCalled();
109+
} );
110+
111+
it( 'passes through for similarly-prefixed paths like /wp/v2/media-categories', () => {
112+
getGBKit.mockReturnValue( {
113+
nativeUploadPort: 8080,
114+
nativeUploadToken: 'token',
115+
} );
116+
const next = makeNext();
117+
const body = new FormData();
118+
body.append( 'file', makeFile(), 'photo.jpg' );
119+
120+
nativeMediaUploadMiddleware(
121+
{ method: 'POST', path: '/wp/v2/media-categories', body },
122+
next
123+
);
124+
125+
expect( next ).toHaveBeenCalled();
126+
expect( global.fetch ).not.toHaveBeenCalled();
127+
} );
128+
129+
it( 'passes through when body is not FormData', () => {
130+
getGBKit.mockReturnValue( {
131+
nativeUploadPort: 8080,
132+
nativeUploadToken: 'token',
133+
} );
134+
const next = makeNext();
135+
136+
nativeMediaUploadMiddleware(
137+
{ method: 'POST', path: '/wp/v2/media', body: '{}' },
138+
next
139+
);
140+
141+
expect( next ).toHaveBeenCalled();
142+
expect( global.fetch ).not.toHaveBeenCalled();
143+
} );
144+
145+
it( 'passes through when FormData has no file field', () => {
146+
getGBKit.mockReturnValue( {
147+
nativeUploadPort: 8080,
148+
nativeUploadToken: 'token',
149+
} );
150+
const next = makeNext();
151+
const body = new FormData();
152+
body.append( 'title', 'no file here' );
153+
154+
nativeMediaUploadMiddleware(
155+
{ method: 'POST', path: '/wp/v2/media', body },
156+
next
157+
);
158+
159+
expect( next ).toHaveBeenCalled();
160+
expect( global.fetch ).not.toHaveBeenCalled();
161+
} );
162+
163+
// MARK: - Interception
164+
165+
it( 'intercepts POST /wp/v2/media with file and fetches to local server', async () => {
166+
getGBKit.mockReturnValue( {
167+
nativeUploadPort: 12345,
168+
nativeUploadToken: 'test-token',
169+
} );
170+
const next = makeNext();
171+
172+
global.fetch = vi.fn( () =>
173+
Promise.resolve( {
174+
ok: true,
175+
json: () =>
176+
Promise.resolve( {
177+
id: 42,
178+
url: 'https://example.com/photo.jpg',
179+
alt: '',
180+
caption: '',
181+
title: 'photo',
182+
mime: 'image/jpeg',
183+
type: 'image',
184+
} ),
185+
} )
186+
);
187+
188+
await nativeMediaUploadMiddleware(
189+
makePostMediaOptions( makeFile() ),
190+
next
191+
);
192+
193+
expect( next ).not.toHaveBeenCalled();
194+
expect( global.fetch ).toHaveBeenCalledOnce();
195+
196+
const [ url, options ] = global.fetch.mock.calls[ 0 ];
197+
expect( url ).toBe( 'http://localhost:12345/upload' );
198+
expect( options.method ).toBe( 'POST' );
199+
expect( options.headers[ 'Relay-Authorization' ] ).toBe(
200+
'Bearer test-token'
201+
);
202+
expect( options.body ).toBeInstanceOf( FormData );
203+
} );
204+
205+
it( 'transforms native response to WordPress REST API shape', async () => {
206+
getGBKit.mockReturnValue( {
207+
nativeUploadPort: 8080,
208+
nativeUploadToken: 'token',
209+
} );
210+
211+
global.fetch = vi.fn( () =>
212+
Promise.resolve( {
213+
ok: true,
214+
json: () =>
215+
Promise.resolve( {
216+
id: 77,
217+
url: 'https://example.com/image.jpg',
218+
alt: 'alt text',
219+
caption: 'a caption',
220+
title: 'image',
221+
mime: 'image/jpeg',
222+
type: 'image',
223+
} ),
224+
} )
225+
);
226+
227+
const result = await nativeMediaUploadMiddleware(
228+
makePostMediaOptions( makeFile() ),
229+
makeNext()
230+
);
231+
232+
expect( result ).toEqual( {
233+
id: 77,
234+
source_url: 'https://example.com/image.jpg',
235+
alt_text: 'alt text',
236+
caption: { raw: 'a caption', rendered: 'a caption' },
237+
title: { raw: 'image', rendered: 'image' },
238+
mime_type: 'image/jpeg',
239+
media_type: 'image',
240+
media_details: { width: 0, height: 0 },
241+
link: 'https://example.com/image.jpg',
242+
} );
243+
} );
244+
245+
it( 'handles missing optional fields in native response', async () => {
246+
getGBKit.mockReturnValue( {
247+
nativeUploadPort: 8080,
248+
nativeUploadToken: 'token',
249+
} );
250+
251+
global.fetch = vi.fn( () =>
252+
Promise.resolve( {
253+
ok: true,
254+
json: () =>
255+
Promise.resolve( {
256+
id: 1,
257+
url: 'https://example.com/file.pdf',
258+
title: 'file',
259+
mime: 'application/pdf',
260+
type: 'application',
261+
} ),
262+
} )
263+
);
264+
265+
const result = await nativeMediaUploadMiddleware(
266+
makePostMediaOptions( makeFile( 'file.pdf', 'application/pdf' ) ),
267+
makeNext()
268+
);
269+
270+
expect( result.alt_text ).toBe( '' );
271+
expect( result.caption ).toEqual( { raw: '', rendered: '' } );
272+
} );
273+
274+
// MARK: - Error handling
275+
276+
it( 'throws user-friendly error on 413 response', async () => {
277+
getGBKit.mockReturnValue( {
278+
nativeUploadPort: 8080,
279+
nativeUploadToken: 'token',
280+
} );
281+
282+
global.fetch = vi.fn( () =>
283+
Promise.resolve( {
284+
ok: false,
285+
status: 413,
286+
statusText: 'Payload Too Large',
287+
text: () =>
288+
Promise.resolve( 'Upload exceeds maximum allowed size' ),
289+
} )
290+
);
291+
292+
await expect(
293+
nativeMediaUploadMiddleware(
294+
makePostMediaOptions( makeFile() ),
295+
makeNext()
296+
)
297+
).rejects.toMatchObject( {
298+
code: 'upload_file_too_large',
299+
message: expect.stringContaining( 'too large' ),
300+
} );
301+
} );
302+
303+
it( 'throws on non-ok response from local server', async () => {
304+
getGBKit.mockReturnValue( {
305+
nativeUploadPort: 8080,
306+
nativeUploadToken: 'token',
307+
} );
308+
309+
global.fetch = vi.fn( () =>
310+
Promise.resolve( {
311+
ok: false,
312+
status: 500,
313+
statusText: 'Internal Server Error',
314+
text: () => Promise.resolve( 'Server crashed' ),
315+
} )
316+
);
317+
318+
await expect(
319+
nativeMediaUploadMiddleware(
320+
makePostMediaOptions( makeFile() ),
321+
makeNext()
322+
)
323+
).rejects.toMatchObject( {
324+
code: 'upload_failed',
325+
message: expect.stringContaining( '500' ),
326+
} );
327+
} );
328+
329+
it( 'throws on fetch network error', async () => {
330+
getGBKit.mockReturnValue( {
331+
nativeUploadPort: 8080,
332+
nativeUploadToken: 'token',
333+
} );
334+
335+
global.fetch = vi.fn( () =>
336+
Promise.reject( new Error( 'Failed to fetch' ) )
337+
);
338+
339+
await expect(
340+
nativeMediaUploadMiddleware(
341+
makePostMediaOptions( makeFile() ),
342+
makeNext()
343+
)
344+
).rejects.toBeDefined();
345+
} );
346+
347+
// MARK: - Signal forwarding
348+
349+
it( 'forwards abort signal to fetch', async () => {
350+
getGBKit.mockReturnValue( {
351+
nativeUploadPort: 8080,
352+
nativeUploadToken: 'token',
353+
} );
354+
355+
global.fetch = vi.fn( () =>
356+
Promise.resolve( {
357+
ok: true,
358+
json: () =>
359+
Promise.resolve( {
360+
id: 1,
361+
url: '',
362+
title: '',
363+
mime: '',
364+
type: '',
365+
} ),
366+
} )
367+
);
368+
369+
const controller = new AbortController();
370+
const options = makePostMediaOptions( makeFile() );
371+
options.signal = controller.signal;
372+
373+
await nativeMediaUploadMiddleware( options, makeNext() );
374+
375+
expect( global.fetch.mock.calls[ 0 ][ 1 ].signal ).toBe(
376+
controller.signal
377+
);
378+
} );
379+
} );

0 commit comments

Comments
 (0)