Skip to content

Commit cc65c74

Browse files
committed
test(rsc-mf): expand remote manifest fallback contracts
1 parent 260278b commit cc65c74

1 file changed

Lines changed: 342 additions & 0 deletions

File tree

tests/integration/rsc-mf/tests/remoteModernServerConfig.test.ts

Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,4 +241,346 @@ describe('rsc-mf remote modern.server middleware contracts', () => {
241241
expect(next).toHaveBeenCalledTimes(1);
242242
expect(context.res).toBeUndefined();
243243
});
244+
245+
it('recovers stale css expose assets via manifest fallback', async () => {
246+
const handler = getRecoverMiddlewareHandler();
247+
const next = jest.fn(async (): Promise<void> => undefined);
248+
const fetchMock = installFetchMock(
249+
jest
250+
.fn()
251+
.mockResolvedValueOnce(
252+
new Response(
253+
JSON.stringify({
254+
exposes: [
255+
{
256+
assets: {
257+
js: {
258+
sync: [],
259+
async: [],
260+
},
261+
css: {
262+
sync: [
263+
'static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css',
264+
],
265+
async: [],
266+
},
267+
},
268+
},
269+
],
270+
}),
271+
{
272+
status: 200,
273+
headers: {
274+
'content-type': 'application/json',
275+
},
276+
},
277+
),
278+
)
279+
.mockResolvedValueOnce(
280+
new Response('.fallback-style{}', {
281+
status: 200,
282+
headers: {
283+
'content-type': 'text/css',
284+
},
285+
}),
286+
),
287+
);
288+
const context: {
289+
req: { url: string; headers?: { get?: (name: string) => string | null } };
290+
res?: Response;
291+
} = {
292+
req: {
293+
url: 'http://127.0.0.1:3008/static/css/async/__federation_expose_RemoteClientCounter.css?cache=1',
294+
},
295+
};
296+
297+
await handler(context, next);
298+
299+
expect(fetchMock).toHaveBeenNthCalledWith(
300+
2,
301+
'http://127.0.0.1:3008/static/css/async/__federation_expose_RemoteClientCounter.9f773de2aa.css?cache=1',
302+
{
303+
headers: {
304+
[INTERNAL_FALLBACK_HEADER]: '1',
305+
},
306+
},
307+
);
308+
expect(next).not.toHaveBeenCalled();
309+
await expect(context.res?.text()).resolves.toBe('.fallback-style{}');
310+
});
311+
312+
it('falls through when manifest response body is invalid json', async () => {
313+
const handler = getRecoverMiddlewareHandler();
314+
const next = jest.fn(async (): Promise<void> => undefined);
315+
const fetchMock = installFetchMock(
316+
jest.fn().mockResolvedValueOnce(
317+
new Response('not-json-manifest', {
318+
status: 200,
319+
headers: {
320+
'content-type': 'application/json',
321+
},
322+
}),
323+
),
324+
);
325+
const context: {
326+
req: { url: string; headers?: { get?: (name: string) => string | null } };
327+
res?: Response;
328+
} = {
329+
req: {
330+
url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js',
331+
},
332+
};
333+
334+
await handler(context, next);
335+
336+
expect(fetchMock).toHaveBeenCalledTimes(1);
337+
expect(next).toHaveBeenCalledTimes(1);
338+
expect(context.res).toBeUndefined();
339+
});
340+
341+
it('falls through when manifest request throws', async () => {
342+
const handler = getRecoverMiddlewareHandler();
343+
const next = jest.fn(async (): Promise<void> => undefined);
344+
const fetchMock = installFetchMock(async () => {
345+
throw new Error('manifest-fetch-failed');
346+
});
347+
const context: {
348+
req: { url: string; headers?: { get?: (name: string) => string | null } };
349+
res?: Response;
350+
} = {
351+
req: {
352+
url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js',
353+
},
354+
};
355+
356+
await handler(context, next);
357+
358+
expect(fetchMock).toHaveBeenCalledTimes(1);
359+
expect(next).toHaveBeenCalledTimes(1);
360+
expect(context.res).toBeUndefined();
361+
});
362+
363+
it('falls through when manifest fallback lookup has no canonical asset match', async () => {
364+
const handler = getRecoverMiddlewareHandler();
365+
const next = jest.fn(async (): Promise<void> => undefined);
366+
const fetchMock = installFetchMock(
367+
jest.fn().mockResolvedValueOnce(
368+
new Response(
369+
JSON.stringify({
370+
exposes: [
371+
{
372+
assets: {
373+
js: {
374+
sync: [
375+
'static/js/async/__federation_expose_other.abc123.js',
376+
],
377+
async: [],
378+
},
379+
css: {
380+
sync: [],
381+
async: [],
382+
},
383+
},
384+
},
385+
],
386+
}),
387+
{
388+
status: 200,
389+
headers: {
390+
'content-type': 'application/json',
391+
},
392+
},
393+
),
394+
),
395+
);
396+
const context: {
397+
req: { url: string; headers?: { get?: (name: string) => string | null } };
398+
res?: Response;
399+
} = {
400+
req: {
401+
url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js',
402+
},
403+
};
404+
405+
await handler(context, next);
406+
407+
expect(fetchMock).toHaveBeenCalledTimes(1);
408+
expect(next).toHaveBeenCalledTimes(1);
409+
expect(context.res).toBeUndefined();
410+
});
411+
412+
it('falls through when fallback asset fetch returns non-ok response', async () => {
413+
const handler = getRecoverMiddlewareHandler();
414+
const next = jest.fn(async (): Promise<void> => undefined);
415+
const fetchMock = installFetchMock(
416+
jest
417+
.fn()
418+
.mockResolvedValueOnce(
419+
new Response(
420+
JSON.stringify({
421+
exposes: [
422+
{
423+
assets: {
424+
js: {
425+
sync: [
426+
'static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js',
427+
],
428+
async: [],
429+
},
430+
css: {
431+
sync: [],
432+
async: [],
433+
},
434+
},
435+
},
436+
],
437+
}),
438+
{
439+
status: 200,
440+
headers: {
441+
'content-type': 'application/json',
442+
},
443+
},
444+
),
445+
)
446+
.mockResolvedValueOnce(
447+
new Response('missing-fallback-asset', {
448+
status: 404,
449+
headers: {
450+
'content-type': 'text/plain',
451+
},
452+
}),
453+
),
454+
);
455+
const context: {
456+
req: { url: string; headers?: { get?: (name: string) => string | null } };
457+
res?: Response;
458+
} = {
459+
req: {
460+
url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js',
461+
},
462+
};
463+
464+
await handler(context, next);
465+
466+
expect(fetchMock).toHaveBeenCalledTimes(2);
467+
expect(next).toHaveBeenCalledTimes(1);
468+
expect(context.res).toBeUndefined();
469+
});
470+
471+
it('merges request query params into absolute same-origin manifest fallback assets', async () => {
472+
const handler = getRecoverMiddlewareHandler();
473+
const next = jest.fn(async (): Promise<void> => undefined);
474+
const fetchMock = installFetchMock(
475+
jest
476+
.fn()
477+
.mockResolvedValueOnce(
478+
new Response(
479+
JSON.stringify({
480+
shared: [
481+
{
482+
assets: {
483+
js: {
484+
sync: [
485+
'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?manifest=1',
486+
],
487+
async: [],
488+
},
489+
css: {
490+
sync: [],
491+
async: [],
492+
},
493+
},
494+
},
495+
],
496+
}),
497+
{
498+
status: 200,
499+
headers: {
500+
'content-type': 'application/json',
501+
},
502+
},
503+
),
504+
)
505+
.mockResolvedValueOnce(
506+
new Response('absolute-fallback-asset', {
507+
status: 200,
508+
headers: {
509+
'content-type': 'application/javascript',
510+
},
511+
}),
512+
),
513+
);
514+
const context: {
515+
req: { url: string; headers?: { get?: (name: string) => string | null } };
516+
res?: Response;
517+
} = {
518+
req: {
519+
url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js?cache=1',
520+
},
521+
};
522+
523+
await handler(context, next);
524+
525+
expect(fetchMock).toHaveBeenNthCalledWith(
526+
2,
527+
'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.7745fe5f0a.js?manifest=1&cache=1',
528+
{
529+
headers: {
530+
[INTERNAL_FALLBACK_HEADER]: '1',
531+
},
532+
},
533+
);
534+
expect(next).not.toHaveBeenCalled();
535+
await expect(context.res?.text()).resolves.toBe('absolute-fallback-asset');
536+
});
537+
538+
it('falls through when fallback asset resolves to the same request url', async () => {
539+
const handler = getRecoverMiddlewareHandler();
540+
const next = jest.fn(async (): Promise<void> => undefined);
541+
const fetchMock = installFetchMock(
542+
jest.fn().mockResolvedValueOnce(
543+
new Response(
544+
JSON.stringify({
545+
exposes: [
546+
{
547+
assets: {
548+
js: {
549+
sync: [
550+
'static/js/async/__federation_expose_RemoteClientCounter.js',
551+
],
552+
async: [],
553+
},
554+
css: {
555+
sync: [],
556+
async: [],
557+
},
558+
},
559+
},
560+
],
561+
}),
562+
{
563+
status: 200,
564+
headers: {
565+
'content-type': 'application/json',
566+
},
567+
},
568+
),
569+
),
570+
);
571+
const context: {
572+
req: { url: string; headers?: { get?: (name: string) => string | null } };
573+
res?: Response;
574+
} = {
575+
req: {
576+
url: 'http://127.0.0.1:3008/static/js/async/__federation_expose_RemoteClientCounter.js',
577+
},
578+
};
579+
580+
await handler(context, next);
581+
582+
expect(fetchMock).toHaveBeenCalledTimes(1);
583+
expect(next).toHaveBeenCalledTimes(1);
584+
expect(context.res).toBeUndefined();
585+
});
244586
});

0 commit comments

Comments
 (0)