Files
webmail/lib/__tests__/proxy-matcher.test.ts

77 lines
3.4 KiB
TypeScript

/**
* @vitest-environment node
*
* Next copies the body of every request proxy.ts runs for, up to
* proxyClientMaxBodySize, before the route sees it. That limit stays at the
* 10 MB default; the upload routes that need more are left out of the
* proxy's matcher. The matcher is read from proxy.ts the way `next build`
* reads it (static extraction), and a path counts as proxied the way Next's
* router decides it: when the raw or the decoded path matches.
*/
import path from 'node:path';
import { describe, it, expect, vi } from 'vitest';
import type { NextConfig } from 'next';
import { loadBindings } from 'next/dist/build/swc';
import { getPagesPageStaticInfo } from 'next/dist/build/analysis/get-page-static-info';
import { getMiddlewareRouteMatcher } from 'next/dist/shared/lib/router/utils/middleware-route-matcher';
import type { PAGE_TYPES } from 'next/dist/lib/page-types';
async function proxied(basePath = '') {
await loadBindings();
const info = await getPagesPageStaticInfo({
pageFilePath: path.resolve(__dirname, '../../proxy.ts'),
nextConfig: { basePath },
isDev: false,
page: '/proxy',
pageType: 'pages' as PAGE_TYPES,
});
const matchers = info.middleware?.matchers;
expect(matchers?.length).toBeGreaterThan(0);
const match = getMiddlewareRouteMatcher(matchers!);
const req = { headers: {} } as Parameters<typeof match>[1];
return (p: string) => {
let decoded = p;
try { decoded = decodeURIComponent(p); } catch { /* as Next: keep it */ }
return match(p, req, {}) || match(decoded, req, {});
};
}
describe('proxy matcher', () => {
it('leaves out exactly the upload routes', async () => {
const runs = await proxied();
for (const p of ['/api/webdav', '/api/plugin-proxy', '/api/wopi/files/f123/contents', '/api/wopi/files/a%20b/contents']) {
expect(runs(p), p).toBe(false);
}
});
it('runs for every other path, including near misses and other spellings', async () => {
const runs = await proxied();
for (const p of [
'/', '/en/mail', '/admin', '/setup', '/api/settings', '/api/auth/session', '/api/account/stalwart/jmap',
'/api/webdav/', '/api/webdav/x', '/api/webdavx', '/api/plugin-proxy/x', '/api/plugin-proxy2',
'/api/wopi/files/f123', '/api/wopi/files/f123/contents/x', '/api/wopi/launch', '/api/wopi/files//contents',
'/api/%77ebdav', '/api/wopi/files/a%2Fb/contents', '/x/api/webdav', '/_next/static/chunk.js',
]) expect(runs(p), p).toBe(true);
});
it('applies under a base path', async () => {
const runs = await proxied('/webmail');
expect(runs('/webmail/api/webdav')).toBe(false);
expect(runs('/webmail/api/settings')).toBe(true);
});
it('keeps the body limit at the default and gives the left-out routes the proxy\'s API headers', async () => {
vi.resetModules();
const { default: nextConfig } = (await import('../../next.config')) as { default: NextConfig };
expect(nextConfig.experimental?.proxyClientMaxBodySize).toBeUndefined();
const rules = (await nextConfig.headers?.()) ?? [];
for (const source of ['/api/webdav', '/api/plugin-proxy', '/api/wopi/files/:fileId/contents']) {
const headers = rules.find((rule) => rule.source === source)?.headers ?? [];
expect(headers, source).toEqual(expect.arrayContaining([
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
]));
}
});
});