nextauthjs / next-auth

Authentication for the Web.
https://authjs.dev
ISC License
23.24k stars 3.18k forks source link

unstable_getServerSession breaks Jest tests due to "node_modules/jose/" dependency #4866

Closed timgcarlson closed 2 years ago

timgcarlson commented 2 years ago

Environment

  System:
    OS: Windows 10 10.0.22000
    CPU: (16) x64 Intel(R) Core(TM) i7-10875H CPU @ 2.30GHz
    Memory: 16.24 GB / 31.87 GB
  Binaries:
    Node: 16.13.0 - C:\Program Files\nodejs\node.EXE
    npm: 8.13.2 - C:\Program Files\nodejs\npm.CMD
  Browsers:
    Edge: Spartan (44.22000.120.0), Chromium (103.0.1264.44)
    Internet Explorer: 11.0.22000.120
  npmPackages:
    next: 12.1.6 => 12.1.6
    next-auth: ^4.9.0 => 4.9.0
    react: ^18.2.0 => 18.2.0

Reproduction URL

https://github.com/timgcarlson/next-auth-jest-bug-getServerSession

Describe the issue

Description

The error happens when running Jest tests on any file that uses unstable_getServerSession. From what I can tell, there is a dependency being used by unstable_getServerSession (jose) that Jest cannot transform properly. It appears that it's using the browser build of jose rather than the node version.

This occurs when unstable_getServerSession is used in getServerSideProps, as shown in the attached repo. The tests fail on the import of the function, so the component is never run by the tests.

I have not encountered this issue anywhere else in Next-Auth. I encountered this issue when I converted my uses of getSession to unstable_getServerSession, as recommended when needing to get the session on the server.


Example Code

A Page Component that uses unstable_getServerSession

import { GetServerSideProps } from "next"
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "./api/auth/[...nextauth]"

export default function GetServerSessionPage() {
  return <h1>Example Page Using getServerSideProps</h1>
}

export const getServerSideProps: GetServerSideProps = async (context) => {
  const session = await unstable_getServerSession(
    context.req,
    context.res,
    authOptions
  )

  return {
    props: {
      session,
    },
  }
}

A test for that page

import { render, screen } from "@testing-library/react"
import GetServerSessionPage from "../../pages/get-server-session-page"

describe("pages/get-server-session-page", () => {
  it("should render a title", () => {
    render(<GetServerSessionPage />)

    expect(
      screen.getByText("Example Page Using getServerSideProps")
    ).toBeInTheDocument()
  })
})

The full error that occurs when running Jest tests

 FAIL  __tests__/pages/get-server-session-page.tests.tsx
  ● Test suite failed to run

    Jest encountered an unexpected token

    Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

    Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.

    By default "node_modules" folder is ignored by transformers.

    Here's what you can do:
     • If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.
     • If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript
     • To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
     • If you need a custom transformation specify a "transform" option in your config.
     • If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.

    You'll find more details and examples of these config options in the docs:
    https://jestjs.io/docs/configuration
    For information about custom transformations, see:
    https://jestjs.io/docs/code-transformation

    Details:

    C:\Users\...\next-auth-jest-bug-getServerSession\node_modules\jose\dist\browser\index.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){export { compactDecrypt } from './jwe/compact/decrypt.js';
                                                                                      ^^^^^^

    SyntaxError: Unexpected token 'export'

      at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1796:14)

How to reproduce

  1. Clone the repo: https://github.com/timgcarlson/next-auth-jest-bug-getServerSession
  2. run npm i
  3. run npm run test

A single test was added for the GetServerSessionPage. The test will fail before it can run because it fails on the import of unstable_getServerSession.

C:\Users\...\next-auth-jest-bug-getServerSession\node_modules\jose\dist\browser\index.js:1
    ({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){export { compactDecrypt } from './jwe/compact/decrypt.js';
                                                                                      ^^^^^^

    SyntaxError: Unexpected token 'export'

One of the reccommend solutions in the above test error is _"To have some of your "nodemodules" files transformed, you can specify a custom "transformIgnorePatterns" in your config." I included some commented out code in my repo to demonstrate this, but it results in another issue. This may be a dead end though.

  1. Go to jest.config.js. Comment out module.exports = createJestConfig(customJestConfig) and then uncomment the lines below it
  2. Run npm run test

This results in this new error:

ReferenceError: crypto is not defined

      1 | import { GetServerSideProps } from "next"
    > 2 | import { unstable_getServerSession } from "next-auth"
        |                                                      ^
      3 | import { authOptions } from "./api/auth/[...nextauth]"
      4 |
      5 | export default function GetServerSessionPage() {

      at Object.crypto (node_modules/jose/dist/browser/runtime/webcrypto.js:1:16)
      at Object.<anonymous> (pages/get-server-session-page.tsx:2:54)
      at Object.<anonymous> (__tests__/pages/get-server-session-page.tests.tsx:2:71)

Expected behavior

Using unstable_getServerSession should not break testing in Jest. What actually happens are the errors described above when running tests on any file that uses unstable_getServerSession, whether the test uses it or not. Ideally the function should not require any specific configuration to be testable.

ThangHuuVu commented 2 years ago

Hi, this doesn't seem like a next-auth issue but rather one with jest configuration usage. You're using testEnvironment: "jest-environment-jsdom" which results in using jose browser module instead of node. I suggest you test the getServerSideProps method separately with the following configuration: testEnvironment: "node"> More here

AlanPedro commented 1 year ago

Has anyone found a better solution than:

  1. Moving all the component code into a separate file
  2. Importing it back into the next page
  3. Then exporting as the default

As done above in Bryan's mention of this issuue

robreinhard commented 1 year ago

@AlanPedro what I did was create two different config files, a jest.api.config and jest.client.config, and setup each as a separate jest project in package.json. This resolved my errors without "overriding" anything

haderman commented 1 year ago

another approach is to add this at the beginning of your test file

/**
 * @jest-environment node
 */

test('...', () => {
   ....
});

https://jestjs.io/docs/configuration#testenvironment-string

elliotgonzalez-lk commented 1 year ago

@timgcarlson Did you ever find an adequate solution?

The proposed solution by the maintainers is not a good one IMO. Splitting your tests into two different environments does not solve the issue of the tests blowing up when you have to test the UI. To avoid this, you need to extract all your UI code into a separate component, then import that component into the page that is using unstable_getServerSession, and then test the UI component separately. This causes the return statement of your page component to go uncovered, so you need to istanbul ignore the component return statement to hide it from coverage. All in all, it just feels super hacky.

I've opened a discussion here: https://github.com/nextauthjs/next-auth/discussions/6084. Hopefully, someone out there has come to an elegant solution. If not, I'll have to move all of my session checks to the client, which I really do not want to have to do.

DoctorDerek commented 1 year ago

I managed to avoid the solution @elliotgonzalez-lk of having to create duplicate components because I had already mocked unstable_getServerSession when I was testing getServerSideProps separately.

Here's my solution to test Next.js pages using Jest + React Testing Library + Next-Auth.

I refactored all of my various "next-auth" mocks into my "Global Mocks" file, @/__tests__/test-utils.tsx, which re-exports the {render} and other methods from React Testing Library:

// **Global Mocks**
// Any mocks included here, in `@/__tests__/test-utils`, apply to all tests.
// Due to Jest transformer issues, we mock next-auth's useSession hook directly:
export const mockSession = {
  expires: new Date(Date.now() + 2 * 86400).toISOString(),
  user: { name: "admin" },
}
jest.mock("next-auth/react", () => {
  const originalModule = jest.requireActual("next-auth/react")
  return {
    __esModule: true,
    ...originalModule,
    useSession: jest.fn(() => ({
      data: mockSession,
      status: "authenticated",
    })),
  }
})
// Reference: https://github.com/nextauthjs/next-auth/discussions/4185#discussioncomment-2397318
// We also need to mock the whole next-auth package, since it's used in
// our various pages via the `export { getServerSideProps }` function.
jest.mock("next-auth", () => ({
  __esModule: true,
  default: jest.fn(),
  unstable_getServerSession: jest.fn(
    () =>
      new Promise((resolve) => {
        resolve({
          expiresIn: undefined,
          loggedInAt: undefined,
          someProp: "someString",
        })
      }),
  ),
}))
// Reference: https://github.com/nextauthjs/next-auth/issues/4866

I'm not using any custom providers; I only test the <SessionProvider> in a component that uses it explicitly. The only other global mock I set up is to mock out the Next,js router using next-router-mock:

// Mock Next.js's useRouter hook using the "next-router-mock" package:
jest.mock("next/dist/client/router", () =>
  jest.requireActual("next-router-mock"),
)
jest.mock("next/dist/shared/lib/router-context", () => {
  const { createContext } = jest.requireActual("react")
  const router = jest.requireActual("next-router-mock").default
  const RouterContext = createContext(router)
  return { RouterContext }
})
// Reference: https://github.com/scottrippey/next-router-mock/issues/58#issuecomment-1182861712
Nashtronaut commented 10 months ago

Hey @DoctorDerek, I apologize if this is a bit of a noob question, still learning these things.

I have a component <Dashboard /> that is using getServerSideProps along with getServerSession inside of it.

I have set up your solution in my global mocks, however, I am still met with the same error when I just try to import the original <Dashboard /> component since it hits that broken import almost immediately. How did you tell jest to to use your global mock rather than that imported version?

Thanks for the help!

DoctorDerek commented 10 months ago

@Nashtronaut You have to specifically mock out every single import that doesn't work. My solution was unstable_getServerSession, so if your version is getServerSession you'd need to update it to that.