-
I have added some logic in my pages middleware (using Next 12) and would like to add tests now but am pretty lost on how to get that started. Can someone direct me to a tutorial or resource that shows a complete example of middleware being tested? Specifically this is what my middleware is doing: export function middleware(request: NextRequest) {
// Redirect a user if they don't have an auth token and are not the admin role
if (request.nextUrl.pathname.startsWith('/admin')) {
const authTokenCookie = request.cookies.token;
const parsedToken = authTokenCookie ? jose.decodeJwt(authTokenCookie) : null;
const role = typeof parsedToken === 'object' ? parsedToken?.role : null;
if (!authTokenCookie || !role || role !== USER_ROLES.admin) {
return NextResponse.redirect(new URL('/', request.url));
}
}
// Redirect the user if a query parameter is present
if (request.nextUrl.pathname === '/' && request.nextUrl.searchParams.has('directToStore')) {
NextResponse.redirect(new URL('/store', request.url));
}
return NextResponse.next();
} |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 9 replies
-
This is how I ended up testing my middleware: import { middleware } from '../pages/_middleware';
import { NextResponse, NextRequest } from 'next/server';
import * as jose from 'jose';
describe('Middleware', () => {
const redirectSpy = jest.spyOn(NextResponse, 'redirect');
afterEach(() => {
redirectSpy.mockReset();
});
it('should redirect to the homepage if visiting an admin page as a user without an auth token', async () => {
const req = new NextRequest(new Request('https://www.whatever.com/admin/check-it-out'), {});
req.headers.set(
'cookie',
'token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2NTg3NjczMjYsImV4cCI6MTY5MDMwMzMyNiwiYXVkIjoid3d3LmV4YW1wbGUuY29tIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsInJvbGUiOiJ1c2VyIn0.G7rkptAKt1icBp92KcHYpGdcWOnn4gO8vWiCMtIHc0c;',
);
const { role } = jose.decodeJwt(req.cookies.token);
await middleware(req);
expect(role).toEqual('user');
expect(redirectSpy).toHaveBeenCalledTimes(1);
expect(redirectSpy).toHaveBeenCalledWith(new URL('/', req.url));
});
it('should redirect to the store if the directToStore query param is set', async () => {
const req = new NextRequest(new Request('https://www.whatever.com'), {});
req.nextUrl.searchParams.set('directToStore', 'true');
await middleware(req);
expect(redirectSpy).toHaveBeenCalledTimes(1);
expect(redirectSpy).toHaveBeenCalledWith(new URL('/store', req.url));
});
}); |
Beta Was this translation helpful? Give feedback.
-
The provided answer works for unit testing, but does not work for integration testing. To integration test a NextJS app that uses middleware, we would need to be able to intercept/mock any requests made by the middleware, probably using something like mock-service-worker. However, this package does not support the edge runtime currently. I have opened a discussion about mocking requests in the middleware process here and a ticket in msw here. |
Beta Was this translation helpful? Give feedback.
-
@zeckdude 's solution didn't work for my middleware I wanted to share my middleware and middleware test, since I spent longer than I wished figuring this out for my case. Hope it helps someone! DISCLAIMER: I am bad at mocking, this can probably be done in a smarter/more robust way... Would love to hear suggestions 😄 NOTES:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { CUSTOM_DOMAINS } from '../constants/customDomains';
export default function middleware(req: NextRequest) {
// Clone the request url
const url = req.nextUrl.clone();
// Get pathname of request (e.g. /p/abc123)
const { pathname } = req.nextUrl;
// Get hostname of request (everything before the pathname e.g. subdomain.example.com; app.localhost:3000; www.example.com; example.com)
const hostname = req.headers.get('host');
// for my use case I need to handle custom domains, adding the userSlug as a queryParam. Here is how I do it:
if (hostname in CUSTOM_DOMAINS) {
// TODO: Is there a better way to look this up instead of a hardcoded object?
const userSlug = CUSTOM_DOMAINS[hostname];
if (pathname === '/') {
url.pathname = `/u/${id}`; // rewrite "/" to user page
}
const params = new URLSearchParams(url.search);
params.append('userSlug', userSlug);
url.search = params.toString();
return NextResponse.rewrite(url);
}
// base case, no subdomain
if (
hostname === 'localhost:3001' ||
hostname === 'example.com' ||
hostname === 'www.localhost:3001' ||
hostname === 'www.example.com'
) {
if (pathname === '/') {
url.pathname = '/home'; // rewrite "/" to "/home" (landing page)
}
return NextResponse.rewrite(url);
}
const subdomain = hostname
.replace(`.example.com`, '')
.replace(`.localhost:3001`, '');
// rewrites for subdomains (user pages) - in my use case, tom.example.com should be my user page, and tom.example.com/a/b/c should be tom.example.com/a/b/c?userSlug=tom
if (subdomain !== '') {
const userSlug = subdomain;
const params = new URLSearchParams(url.search);
params.append('userSlug', userSlug);
url.search = params.toString();
// home page should be the user's profile page
if (url.pathname === '/') {
url.pathname = `/u/${userSlug}`;
}
return NextResponse.rewrite(url);
}
return NextResponse.next();
} // customDomains.ts
export const CUSTOM_DOMAINS = {
'mycoolsite.com': 'user_id_123456',
'fancyblog.com:' 'user_id_987654',
...
} // middleware.test.ts
import middleware from '../middleware';
import { NextRequest, NextResponse } from 'next/server';
import { CUSTOM_DOMAINS } from '../constants/customDomains';
const rewriteSpy = jest.spyOn(NextResponse, 'rewrite');
const PROTOCOL = 'http://';
// This is the main step I needed to get tests working. Because in my middleware I'm modifying the cloned url, mocking .clone
// returns this object with shape { pathname: string, search: string }, and I'm only testing against the fields that my middleware is
// manipulating (which in my case are just pathname and search). Note that in my case I also needed to mock returns for
// req.nextUrl.pathname and req.headers.get('host'). YMMV!
jest.mock('next/server', () => ({
...jest.requireActual('next/server'),
NextRequest: jest.fn().mockImplementation(req => ({
nextUrl: {
clone: jest.fn().mockReturnValue({
pathname: req.pathname,
search: req.search,
}),
pathname: req.pathname,
},
headers: {
get: jest.fn().mockImplementation(key => key === 'host' && req.host),
},
})),
}));
describe('middleware', () => {
beforeEach(() => {
rewriteSpy.mockReset();
});
describe('base case - when there is no subdomain', () => {
const domains = [
'localhost:3001',
'example.com',
'www.example.com',
];
it('should rewrite to /home when the route is "/"', async () => {
// Setup
const path = '/';
for (const domain of domains) {
const url = new URL(path, PROTOCOL + domain);
const req = new NextRequest(url);
rewriteSpy.mockReset();
// Test
await middleware(req);
expect(rewriteSpy).toHaveBeenCalledWith({
pathname: '/home',
search: '',
});
}
});
it('should return the path for any other route', async () => {
// Setup
const path = '/p/abc123';
for (const domain of domains) {
const url = new URL(path, PROTOCOL + domain);
const req = new NextRequest(url);
rewriteSpy.mockReset();
// Test
await middleware(req);
expect(rewriteSpy).toHaveBeenCalledWith({
pathname: path,
search: '',
});
}
});
it('should retain queryParams', async () => {
// Setup
const path = '/';
for (const domain of domains) {
const url = new URL(path, PROTOCOL + domain);
url.searchParams.append('foo', 'bar');
const req = new NextRequest(url);
// Test
await middleware(req);
expect(rewriteSpy).toHaveBeenCalledWith({
pathname: '/home',
search: '?foo=bar',
});
}
});
});
describe('when the domain includes a subdomain', () => {
const domains = [
'tom.localhost:3001',
'tom.example.com',
];
it('should redirect to the users profile page if the original root is "/" (root path)', async () => {
// Setup
const path = '/';
for (const domain of domains) {
const url = new URL(path, PROTOCOL + domain);
const req = new NextRequest(url);
rewriteSpy.mockReset();
// Test
await middleware(req);
expect(rewriteSpy).toHaveBeenCalledWith({
pathname: '/u/tom',
search: 'userSlug=tom',
});
}
});
it('should include the subdomain as a query params called userSlug', async () => {
// Setup
const path = '/p/abc123';
for (const domain of domains) {
const url = new URL(path, PROTOCOL + domain);
const req = new NextRequest(url);
rewriteSpy.mockReset();
// Test
await middleware(req);
expect(rewriteSpy).toHaveBeenCalledWith({
pathname: path,
search: 'userSlug=tom',
});
}
});
it('should keep original queryParams, in addition to the new userSlug one', async () => {
// Setup
const path = '/a/b/c';
for (const domain of domains) {
const url = new URL(path, PROTOCOL + domain);
url.searchParams.append('foo', 'bar');
const req = new NextRequest(url);
rewriteSpy.mockReset();
// Test
await middleware(req);
expect(rewriteSpy).toHaveBeenCalledWith({
pathname: path,
search: 'foo=bar&userSlug=tom',
});
}
});
});
describe('custom domains', () => {
it('should redirect to the users profile page if the original root is "/" (root path)', async () => {
// Setup
const path = '/';
for (const domain of Object.keys(CUSTOM_DOMAINS)) {
const url = new URL(path, PROTOCOL + domain);
const req = new NextRequest(url);
rewriteSpy.mockReset();
// Test
await middleware(req);
const userSlug = CUSTOM_DOMAINS[domain];
expect(rewriteSpy).toHaveBeenCalledWith({
pathname: `/u/${userSlug}`,
search: `userSlug=${userSlug}`,
});
}
});
it('should keep the same path for non-root paths', async () => {
// Setup
const path = '/p/xyz123';
for (const domain of Object.keys(CUSTOM_DOMAINS)) {
const url = new URL(path, PROTOCOL + domain);
const req = new NextRequest(url);
rewriteSpy.mockReset();
// Test
await middleware(req);
const userSlug = CUSTOM_DOMAINS[domain];
expect(rewriteSpy).toHaveBeenCalledWith({
pathname: path,
search: `userSlug=${userSlug}`,
});
}
});
it('should maintain any original queryParams', async () => {
// Setup
const path = '/p/xyz123';
for (const domain of Object.keys(CUSTOM_DOMAINS)) {
const url = new URL(path, PROTOCOL + domain);
url.searchParams.append('foo', 'bar');
const req = new NextRequest(url);
rewriteSpy.mockReset();
// Test
await middleware(req);
const userSlug = CUSTOM_DOMAINS[domain];
expect(rewriteSpy).toHaveBeenCalledWith({
pathname: path,
search: `foo=bar&userSlug=${userSlug}`,
});
}
});
});
}); |
Beta Was this translation helpful? Give feedback.
-
For anyone surfacing across the web and ends up being here, just a note that it seems incredibly hard to setup unit tests (Jest) with middleware.ts in Next 13 properly and we ended up turning to Playwright tests after wasting dozens of man hours. Double think before you decide to dig into the rabbit hole deeper! Also references we have tried but not working unfortunately: |
Beta Was this translation helpful? Give feedback.
This is how I ended up testing my middleware: