sidebase / nuxt-auth

Authentication built for Nuxt 3! Easily add authentication via OAuth providers, credentials or Email Magic URLs!
https://auth.sidebase.io
MIT License
1.26k stars 162 forks source link

How to mock useAuth in Vitest ? #596

Open ErinTran15 opened 10 months ago

ErinTran15 commented 10 months ago

Environment

No response

Reproduction

No response

Describe the bug

I've been trying to do vi.mock and unplugin-auto-import to mock it but none of them works when I config it in unplugin-auto-import in vitest.config.ts like this

 imports: [
      'vue',
      'vue-router',
      {
        '@sidebase/nuxt-auth: [
          "useAuth"
        ],
      },
    ],

surely it won't work because its the wrong path but when I import it with the correct path like in auto import path it will give me error:

 imports: [
      'vue',
      'vue-router',
      {
        '@sidebase/nuxt-auth/dist/runtime/composables/authjs/useAuth': [
          "useAuth"
        ],
      },
    ],

Missing "./dist/runtime/composables/authjs/useAuth" specifier in "@sidebase/nuxt-auth" package

pls is there any other ways I could mock it ? Really appreciate if you guys can help me

Additional context

No response

Logs

No response

zoey-kaiser commented 9 months ago

Hi! Mocking is generally a bit harder to accomplish inside of the module, as everyone has different session data. The way we handled it internally is as follows:

1.) Inside of /tests/mock/setup.ts we created a new "mocked" version of nuxt-auth:

import { createResolver, defineNuxtModule } from '@nuxt/kit'

export default defineNuxtModule({
  setup: (_options, nuxt) => {
    const { resolve } = createResolver(import.meta.url)
    const pathToMocks = resolve('./nuxt-auth.ts')

    nuxt.hook('imports:extend', (_imports) => {
      _imports.push({ name: 'useAuth', from: pathToMocks })
    })

    nuxt.hook('nitro:config', (nitroConfig) => {
      if (!nitroConfig.alias) {
        throw new Error('Alias must exist at this point, otherwise server-side cannot be mocked')
      }
      nitroConfig.alias['#auth'] = pathToMocks
    })
  },
})
  1. Inside of this file we referenced a mocked version of the composable used inside of NuxtAuth written in /tests/mock/nuxt-auth.ts
import { eventHandler } from 'h3'

export const MOCKED_USER = { user: { role: 'admin', email: 'test@example.com', name: 'John Doe' } }

// App-side mocks
export const useAuth = () => ({
  data: ref(MOCKED_USER),
  status: ref('authenticated'),
  getSession: () => MOCKED_USER,
  signOut: () => {},
})

// Server-side mocks
export const getServerSession = () => MOCKED_USER
export const NuxtAuthHandler = () => eventHandler(() => MOCKED_USER)
  1. Inside of our nuxt.config.ts we check if the environment is currently a test, environment. If it is we add this new mocked module into our modules array:
const mockAuthModule = process.env.VITEST ? ['./test/mock/setup.ts'] : []

export default defineNuxtConfig({
  modules: [
    '@sidebase/nuxt-auth',
    ...mockAuthModule,
  ],
}

When running the application in ViTest, the mocked module will overwrite the real NuxtAuth module. This will result in a console warning that the composables are being overwritten, which is fine, as the mock will then work!

If we want to integrate this functionality into the module itself, I only see one issue: How do we allow people to define their own session data, as everyones session datatype can be different (also between providers). However I hope this quick overview helps you!

peterbud commented 9 months ago

That's a great summary @zoey-kaiser.

Few suggestions:

Klumper commented 8 months ago

I managed to get it to work with Cypress a while back. Maybe this can be useful information for the Vitest implementation.

Created a class to generate a token which then will be set within the cookies (next-auth.session-token), which is used within the sidebase package.

The token includes the provided user information, which can be extracted within your tests. I used it to specify permissions to test them 😄 .

Note that the user secret should be the same as your configured secret within sidebase.

import { EncryptJWT, type JWTPayload } from 'jose';
import hkdf from '@panva/hkdf';
import User from './User';

export default class Session {
    user: User;

    secret: string;

    accessToken: string;

    expires: Date;

    constructor (secret: string, user?: User) {
        this.secret = secret;
        this.user = user || new User();
        // Set the expiration date to 1 year from now
        this.expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000);
    }

    private getDerivedEncryptionKey () {
        return hkdf(
            'sha256',
            this.secret,
            '',
            'NextAuth.js Generated Encryption Key',
            32,
        );
    }

    private async encode (token: JWTPayload) {
        const maxAge = 30 * 24 * 60 * 60; // 30 days
        const encryptionSecret = await this.getDerivedEncryptionKey();
        return new EncryptJWT(token)
            .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
            .setIssuedAt()
            .setExpirationTime(Math.round(Date.now() / 1000 + maxAge))
            .setJti('cypress-test')
            .encrypt(encryptionSecret);
    }

    async generateAccessToken () {
        this.accessToken = await this.encode({ user: this.user });
    }
}
zoey-kaiser commented 7 months ago

Lets add a section to the documentation this these examples!

pablo-vega-kloeckner-i commented 3 months ago

@zoey-kaiser Where can I fine this information in the documentation?

zoey-kaiser commented 3 months ago

Hi @pablo-vega-kloeckner-i 👋

We currently do not have any documentation on this, as we are currently rewriting the entire docs (https://github.com/sidebase/docs/pull/180). However, I have outlined our vitest setup in this comment: https://github.com/sidebase/nuxt-auth/issues/596#issuecomment-1864222564

If you have any additional questions to this, feel free to ask them here 😊