Jest Unit Testing - Part 1

Next-auth, Prisma, and High-Order Functions

While unit-testing my app, I got stuck while trying to mock a few external libraries. I list them here in case someone else runs into similar issues. In this first post of the series, I'll start with testing the auth middleware by learning how to mock next-auth and Prisma, and testing high-order functions.

What we are testing

In Platform template's auth.ts, we need to mock getServerSession from Next-auth, as well as Prisma's methods for interacting with the database. Furthermore, we have functions, such as withSiteAuth, which are examples of high-order functions. They are called this because they return a new function designed to wrap around an action, adding things like authentication and authorization checks.

Here is the full function, from lib/auth/auth.ts

export function withSiteAuth(action: any) {
  return async (
    formData: FormData | null,
    siteId: string,
    key: string | null,
  ) => {
    const session = await getSession();
    if (!session) {
      return {
        error: "Not authenticated",
      };
    }
    const site = await prisma.site.findUnique({
      where: {
        id: siteId,
      },
    });
    if (!site || site.userId !== session.user.id) {
      return {
        error: "Not authorized",
      };
    }
    return action(formData, site, key);
  };
}

In the case of withSiteAuth, the user must be both authenticated with next-auth, and authorized to perform an action on this particular site. In order to be authorized, the user id associated with this site must match the current user's id.

How to mock next-auth

To mock getServerSession from next-auth, add this on top of the file:

jest.mock("next-auth", () => ({
    __esModule: true,
    getServerSession: jest.fn(),
    }));

You'll also need a mock user such as this one:

const mockUser: any = {
  user: {
    id: "userId",
    name: "John Doe",
    username: "johndoe",
    email: "johndoe@example.com",
    image: "https://example.com/johndoe.jpg"
  }
}

How to mock Prisma

In order to mock Prisma, you first need to install jest-mock-extended as a dev dependency. Then, create a file in your lib folder named singleton.ts:

import { PrismaClient } from '@prisma/client'
import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'

import prisma from '@/lib/prisma'

jest.mock('@/lib/prisma', () => ({
  __esModule: true,
  default: mockDeep<PrismaClient>(),
}))

beforeEach(() => {
  mockReset(prismaMock)
})

export const prismaMock = prisma as unknown as DeepMockProxy<PrismaClient>

Now, in your jest.config file, add this filename to your setupFilesAfterEnv: setupFilesAfterEnv: ['<rootDir>/jest.setup.ts', '<rootDir>/lib/singleton.ts'],

Now you just need to import prismMock into your test file:

import { prismaMock } from '@/lib/singleton'; and you are all set to use it. For more on testing with Prisma, see the official docs: https://www.prisma.io/docs/orm/prisma-client/testing/unit-testing

Putting it all together

auth.test.ts (happy path)

it('should return the result of the action function when the user is authenticated and authorized for the given site', async () => {
        // mock getServerSession to return mockUser we created above
        const getServerSessionMock = require('next-auth').getServerSession;
        getServerSessionMock.mockResolvedValue(mockUser);

        // action is the function that gets passed into the high-order function
        const action = jest.fn();
        // function params
        const formData = new FormData();
        const siteId = "siteId";
        const key = "key";

        // a mock site with userId that matches our mock user's id
        const site = {
            id: "siteId",
            userId: "userId",
            name: "Site Name",
            description: "Site Description",
            createdAt: new Date(),
            updatedAt: new Date(),
        };

        // mock prisma's call to find the site in the database
        prismaMock.site.findUnique.mockResolvedValue(site as Site);

        // finally, here is the function call
        await withSiteAuth(action)(formData, siteId, key);

        // Ensure the action was called with the correct arguments
        expect(action).toHaveBeenCalledWith(formData, site, key); 
         // Ensure getSession was called
        expect(getServerSessionMock).toHaveBeenCalled();
        expect(prismaMock.site.findUnique).toHaveBeenCalledWith({
            where: { id: siteId },
        });
    });

For an unauthenticated user scenario, simply return null from the user session:

 it('should return an error object with message "Not authenticated" when the user is not authenticated', async () => {
        const getServerSessionMock = require('next-auth').getServerSession;
        // getServerSession returns null
        getServerSessionMock.mockResolvedValue(null);

        const action = jest.fn();
        const formData = new FormData();
        const siteId = "siteId";
        const key = "key";

        // function call
        const result = await withSiteAuth(action)(formData, siteId, key);

        expect(result).toEqual({
            error: "Not authenticated",
        });
        expect(getServerSessionMock).toHaveBeenCalled();
    });

And for unauthorized, there are multiple scenarios:

if (!site || site.userId !== session.user.id)

I'm going to show the one where user id and site user id do not match.

    it('should return an error object with message "Not authorized" when the user is authenticated but not authorized for the given site', async () => {
        const getServerSessionMock = require('next-auth').getServerSession;
        getServerSessionMock.mockResolvedValue(mockUser);

        const site = {
            id: "siteId",
            userId: "anotherUserId", //differs from our mock user id
            name: "Site Name",
            description: "Site Description",
            createdAt: new Date(),
            updatedAt: new Date(),
        };

        const action = jest.fn();
        const formData = new FormData();
        const siteId = "siteId";
        const key = "key";

        prismaMock.site.findUnique.mockResolvedValueOnce(site as Site);
        const result = await withSiteAuth(action)(formData, siteId, key);

        expect(result).toEqual({
            error: "Not authorized",
        });
        expect(getServerSessionMock).toHaveBeenCalled();
        expect(prismaMock.site.findUnique).toHaveBeenCalled();
    });

This pattern can be used for your other tests of auth high-order functions.

I hope you found this helpful. Stay tuned for part 2, where I'll write about testing Next.js route handlers.

Happy Coding!