Testing Next.js API with Jest and Supertest library: incorrect request object in POST route

322 views Asked by At

I encountered an issue during tests launch for Next.js API call. It's just JWT authentication request with Next and Jose library. I can't recognize a response object to check its body, status and headers at least. Thus, the problem is to get correct request argument into Jest environment to test the request as it's working well into browser and Postman

I got route.js API route file:

import { SignJWT } from "jose";
import { NextResponse } from "next/server";
import { getJwtSecretKey } from "@/libs/auth";

export async function POST(request) {
    try {
        const body = await request.json();
        if (body.username === "test-name" && body.password === "password1234") {
            const token = await new SignJWT({
                username: body.username,
            })
                .setProtectedHeader({ alg: "HS256" })
                .setIssuedAt()
                .setExpirationTime("30s")
                .sign(getJwtSecretKey());
            const response = NextResponse.json(
                { success: true },
                { status: 200, headers: { "content-type": "application/json" } }
            );
            response.cookies.set({
                name: "token",
                value: token,
                path: "/",
            });
            // Temporary stub for user profile
            response.cookies.set({
                name: "user-name",
                value: body.username,
                path: "/api",
            });
            return response;
        }
        return NextResponse.json(
            { success: false },
            { status: 401, headers: { "content-type": "application/json" } }
        );
    } catch (error) {
        return console.error(error)
    }
}

I'm using console.error here to expose exact exception to console

The test file is:

import request from 'supertest';
import { POST } from './route';
import { getJwtSecretKey } from '../../../libs/auth';
import * as Jose from "jose";

jest.mock('../../../libs/auth', () => ({
    getJwtSecretKey: jest.fn(),
}));

jest.mock('next/dist/server/web/exports/next-response.js', () => ({ Request: jest.fn() }));

jest.mock('jose', () => ({
    SignJWT: jest.fn(() => Jose.SignJWT)
}));

describe('POST API Endpoint', () => {
    it('should return a success response with a token for valid credentials', async () => {
        jest.useFakeTimers('legacy');
        getJwtSecretKey.mockReturnValue('your-secret-key');

        const validCredentials = {
            username: 'test-name',
            password: 'password1234',
        };
 
        const response = await request(POST)
            .post('/api/login')
            .set('Content-Type', 'application/json')
            .send(JSON.stringify(validCredentials));
        ;
        expect(response.status).toBe(200);
        expect(response.body).toEqual({ success: true });
        expect(response.headers['content-type']).toBe('application/json');
        expect(response.header['set-cookie']).toContain('token=');
    });
    // Other test cases here
});

The test result is:

  console.error
    TypeError: request.json is not a function
        at Server.json (C:\Users\smiss-dev\projects\auth-jwt\src\app\api\login\route.js:7:36)
        at Server.emit (node:events:517:28)
        at parserOnIncoming (node:_http_server:1107:12)
        at HTTPParser.parserOnHeadersComplete (node:_http_common:119:17)

      33 |         return NextResponse.json({ success: false });
      34 |     } catch (error) {
    > 35 |         console.error(error)
         |                 ^
      36 |     }
      37 | }

      at Server.error (src/app/api/login/route.js:35:17)

 FAIL  src/app/api/login/route.test.js (5.487 s)
  POST API Endpoint
    × should return a success response with a token for valid credentials (5002 ms)

  ● POST API Endpoint › should return a success response with a token for valid credentials

    thrown: "Exceeded timeout of 5000 ms for a test.
    Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout."

      15 |
      16 | describe('POST API Endpoint', () => {
    > 17 |     it('should return a success response with a token for valid credentials', async () => {
         |     ^
      18 |         jest.useFakeTimers('legacy');
      19 |         getJwtSecretKey.mockReturnValue('your-secret-key');
      20 |

      at it (src/app/api/login/route.test.js:17:5)
      at Object.describe (src/app/api/login/route.test.js:16:1)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        5.911 s, estimated 6 s
Ran all test suites matching /route/i.
Jest did not exit one second after the test run has completed.

It looks that request is falling into catch block and request object does not consist json, hence the request has timeout and the test is not completed

The first of all I've checked the request object and I found out that the body also absent here: const body = await request.body exposes undefined. Also the request is not NextRequest object.

I've applied "http" library to test the request:

import request from 'supertest';
import { POST } from './route';
import { getJwtSecretKey } from '../../../libs/auth';
import { createServer } from "http";
import * as Jose from "jose";

jest.mock('../../../libs/auth', () => ({
    getJwtSecretKey: jest.fn(),
}));

jest.mock('next/dist/server/web/exports/next-response.js', () => ({ Request: jest.fn() }));

jest.mock('jose', () => ({
    SignJWT: jest.fn(() => Jose.SignJWT)
}));

describe('POST API Endpoint', () => {
    let server;

    beforeAll(() => {
        server = createServer(POST);
        server.listen();
    });

    afterAll((done) => {
        server.close(done);
    });
    it('should return a success response with a token for valid credentials', async () => {
        jest.useFakeTimers('legacy');
        getJwtSecretKey.mockReturnValue('your-secret-key');

        const validCredentials = {
            username: 'test-name',
            password: 'password1234',
        };

        const response = await request(server)
            .post('/api/login')
            .set('Content-Type', 'application/json')
            .send(JSON.stringify(validCredentials));
        ;
        expect(response.status).toBe(200);
        expect(response.body).toEqual({ success: true });
        expect(response.headers['content-type']).toBe('application/json');
        expect(response.header['set-cookie']).toContain('token=');
    });
    // Other tests here
}); 

Result is the same

jest.config.js:

const nextJest = require('next/jest')

const createJestConfig = nextJest({
    // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
    dir: './',
})

const customJestConfig = {
    setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
    testEnvironment: 'jest-environment-jsdom',
}

module.exports = createJestConfig(customJestConfig)
0

There are 0 answers