Request: example of cypress testing #2053
Replies: 12 comments 35 replies
-
I would also appreciate some example which is fully working - would even be up to contributing to one 🙌 @CaptainChemist so far your problem is that you are opening an incorrect url probably, the example on the next-auth page assumes you have your login buttons at the I have another problem though - when I get some cookies back, there is no |
Beta Was this translation helpful? Give feedback.
-
The signing page that is rendered has this content: https://www.github.com/nextauthjs/next-auth/tree/main/src%2Fserver%2Fpages%2Fsignin.js I cannot see the #login_field element, so not surprising that Cypress just times out. See the link above and try targeting something else that exists. (although you probably don't want to actually log in in tests, just mock a user. see: #2053 (reply in thread) |
Beta Was this translation helpful? Give feedback.
-
@CaptainChemist you are using a @balazsorban44 I am almost done with testing the provider auth setup - it works locally and almost works on github action, I will be up to contributing to docs to clarify it even better and provide a small app with pipeline as an example. I am still having troubles with those
But on my CI pipeline(GH actions) the next-auth.session-token is totally missing 🤔 any ideas? |
Beta Was this translation helpful? Give feedback.
-
I'm not sure if this has been mentioned here but I would like to explain how we handle the login in cypress. NOTE: We do not test the login flow we just stub the session to be able to access pages hidden behind the auth wall Set up a login with cypress
NOTE: the
{
"user": {
"name": "Morty Smith",
"email": "test@example.com",
"image": "/path/to/your/mock/user.jpg"
},
"expires": "3000-01-01T00:00:00.000Z",
"accessToken": "abcdefghijklmnopqrst"
}
Cypress.Commands.add("login", () => {
cy.intercept("/api/auth/session", { fixture: "session.json" }).as("session");
// Set the cookie for cypress.
// It has to be a valid cookie so next-auth can decrypt it and confirm its validity.
// This step can probably/hopefully be improved.
// We are currently unsure about this part.
// We need to refresh this cookie once in a while.
// We are unsure if this is true and if true, when it needs to be refreshed.
cy.setCookie("next-auth.session-token", "a valid cookie from your browser session");
Cypress.Cookies.preserveOnce("next-auth.session-token");
});
require("./commands"); NOTE: You can grab a valid session token from your browser:
describe("Cypress login", () => {
it("should provide a valid session", () => {
// Call your custom cypress command
cy.login();
// Visit a route in order to allow cypress to actually set the cookie
cy.visit("/");
// Wait until the intercepted request is ready
cy.wait("@session");
// This is where you can now add assertions
// Example: provide a data-test-id on an element.
// This can be any selector that "always and only" exists when the user is logged in
cy.get("[data-test-id='authenticated']").should("exist").then(() => {
cy.log("Cypress login successful");
});
});
}); This setup works without flaws (for us) and is very easy to setup.
We hope it helps. 💖 Thank you 🚀 |
Beta Was this translation helpful? Give feedback.
-
I'm not a fan of manually get a session token and set it as an environment variable so I came up with another solution. I add the if (process.env.NODE_ENV !== 'production')
providers.push(
CredentialsProvider({
name: 'Credentials',
credentials: {
email: {
label: 'Email',
type: 'email',
placeholder: 'email@email.com',
},
},
async authorize(credentials) {
// fetch user from your database or else
return user
},
})
)
export default NextAuth({
adapter: PrismaAdapter(prisma),
secret: process.env.SECRET,
providers,
session: {
strategy: process.env.NODE_ENV === 'production' ? 'database' : 'jwt',
},
}) And I use this as a Cypress command to sign in the user: Cypress.Commands.add('signIn', (email: string) => {
cy.log(`🔐 Sign in as ${email}`)
return cy.wrap(signIn('credentials', { redirect: false, email }))
}) Easy peasy 👌 |
Beta Was this translation helpful? Give feedback.
-
I've since updated the code so that a valid next-auth cookie can be generated from JSON object which seemed a bit more flexible to me compared to a fixture. I have the below in my import hkdf from "@panva/hkdf";
import { EncryptJWT, JWTPayload } from "jose";
// Function logic derived from https://github.com/nextauthjs/next-auth/blob/5c1826a8d1f8d8c2d26959d12375704b0a693bfc/packages/next-auth/src/jwt/index.ts#L113-L121
async function getDerivedEncryptionKey(secret: string) {
return await hkdf(
"sha256",
secret,
"",
"NextAuth.js Generated Encryption Key",
32
);
}
// Function logic derived from https://github.com/nextauthjs/next-auth/blob/5c1826a8d1f8d8c2d26959d12375704b0a693bfc/packages/next-auth/src/jwt/index.ts#L16-L25
export async function encode(
token: JWTPayload,
secret: string
): Promise<string> {
const maxAge = 30 * 24 * 60 * 60; // 30 days
const encryptionSecret = await getDerivedEncryptionKey(secret);
return await new EncryptJWT(token)
.setProtectedHeader({ alg: "dir", enc: "A256GCM" })
.setIssuedAt()
.setExpirationTime(Math.round(Date.now() / 1000 + maxAge))
.setJti("test")
.encrypt(encryptionSecret);
}
Cypress.Commands.add("login", (userObj: JWTPayload) => {
// Generate and set a valid cookie from the fixture that next-auth can decrypt
cy.wrap(null)
.then(() => {
return encode(userObj, Cypress.env("NEXTAUTH_JWT_SECRET"));
})
.then((encryptedToken) =>
cy.setCookie("next-auth.session-token", encryptedToken)
);
}); Here's a usage example: const user = {
name: "Morty Smith",
email: "test@picklerick.com",
image: "/path/to/butterbot.jpg",
birthdate: "12/02/13",
};
cy.login(user);
... You can see the current code here, and usage examples here. Old DEPRECATED example belowIn case it helps someone else, I hacked together the below code using what @pixelass so kindly provided (thank you BTW!) to generate a valid cookie from the fixture that can be parsed by I have the below in my import hkdf from "@panva/hkdf";
import { EncryptJWT } from "jose";
// Function logic derived from https://github.com/nextauthjs/next-auth/blob/5c1826a8d1f8d8c2d26959d12375704b0a693bfc/packages/next-auth/src/jwt/index.ts#L113-L121
async function getDerivedEncryptionKey(secret: string) {
return await hkdf(
"sha256",
secret,
"",
"NextAuth.js Generated Encryption Key",
32
);
}
// Function logic derived from https://github.com/nextauthjs/next-auth/blob/5c1826a8d1f8d8c2d26959d12375704b0a693bfc/packages/next-auth/src/jwt/index.ts#L16-L25
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export async function encode(token: any, secret: string) {
const maxAge = 30 * 24 * 60 * 60; // 30 days
const encryptionSecret = await getDerivedEncryptionKey(secret);
return await new EncryptJWT(token)
.setProtectedHeader({ alg: "dir", enc: "A256GCM" })
.setIssuedAt()
.setExpirationTime(Date.now() / 1000 + maxAge)
.setJti("test")
.encrypt(encryptionSecret);
}
Cypress.Commands.add("login", () => {
cy.intercept("/api/auth/session", { fixture: "session.json" }).as("session");
// Generate and set a valid cookie from the fixture that next-auth can decrypt
cy.wrap(null)
.then(() => cy.fixture("session.json"))
.then((sessionJSON) =>
encode(sessionJSON, Cypress.env("NEXTAUTH_JWT_SECRET"))
)
.then((encryptedToken) =>
cy.setCookie("next-auth.session-token", encryptedToken)
);
Cypress.Cookies.preserveOnce("next-auth.session-token");
}); Also below are the contents of {
"user": {
"name": "Morty Smith",
"email": "test@example.com",
"image": "/path/to/your/mock/user.jpg"
},
"expires": "3000-01-01T00:00:00.000Z",
"accessToken": "abcdefghijklmnopqrst"
} |
Beta Was this translation helpful? Give feedback.
-
Hello everyone, I know this thread is a bit older, but just in case I've written something about this topic: I hope it will help someone with this |
Beta Was this translation helpful? Give feedback.
-
Many thanks @yeungalan0! Building on top of your solution and comments below it seems that it is enough to: Declare inside
Create
And then use it as follows in tests:
|
Beta Was this translation helpful? Give feedback.
-
Here you go. Working app, reproducible example and demo. |
Beta Was this translation helpful? Give feedback.
-
Anyone got an up to date solution to this? I don't really want to automatically test my OpenAuth authentication providers, they work fine already. This tutorial is complicated, depends on lots of things, and I can't get it to work. In any case I don't use username/ password authentication, and have 2FA on my personal accounts... it's just not looking like the way forward. Mocking up the authorization cookie on a development machine ought to work, but although these examples run, they don't work; my auth guards are still saying "nope, you're not logged in, mate". I guess I can stub out the auth on test machines, but that seems... a bit crude. |
Beta Was this translation helpful? Give feedback.
-
Maybe I'm late to the party. This is what I've done in my pet project. Limitation of this approach.
What I've done.in "scripts": {
....
"ci:dev": "CYPRESS_MODE=CI npm run dev",
} in import NextAuth from 'next-auth'
import Google from 'next-auth/providers/google'
import Credentials from 'next-auth/providers/credentials'
import getConfig from 'next/config'
import { DrizzleAdapter } from '@auth/drizzle-adapter'
import { database } from '@/db'
import { usersTable, accountsTable, sessionsTable } from '@/db/schema'
const { serverRuntimeConfig } = getConfig()
const isToUseJWT =
process.env.CYPRESS_MODE === 'CI' || process.env.VERCEL_ENV === 'preview'
let providers = [Google]
if (isToUseJWT) {
/**
* NOTE: This is a custom provider for Cypress tests and vercel preview links.
*/
providers.push(
// @ts-ignore
Credentials({
id: 'cypress-auth',
name: 'Cypress Test',
credentials: {
email: { label: 'Email' },
password: { label: 'Password', type: 'password' },
},
authorize: (credentials) => {
if (
credentials.email === serverRuntimeConfig.authCredentialsEmail &&
credentials.password === serverRuntimeConfig.authCredentialsPassword
) {
return {
id: 'sample-id-for-cypress',
name: serverRuntimeConfig.authCredentialsName,
email: serverRuntimeConfig.authCredentialsEmail,
image: serverRuntimeConfig.authCredentialsImage,
}
}
throw new Error('User not found.')
},
}),
)
}
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: DrizzleAdapter(database, {
usersTable,
accountsTable,
sessionsTable,
}),
providers,
basePath: '/auth',
debug: process.env.NODE_ENV !== 'production',
// NOTE: this is custom for preview and development environments
...(isToUseJWT ? { session: { strategy: 'jwt' } } : {}),
}) and the extra step I'm talking about. Instead of running this locally.
I need to explicitly run this command instead.
then on another terminal,
and my cypress command Cypress.Commands.add(
'authenticate',
(email, password, redirectTo = '/home') => {
cy.log('Authenticating using email/password')
cy.visit('/auth/signin')
cy.findByLabelText('Email').click().type(email)
cy.findByLabelText('Password').click().type(password)
cy.findByRole('button', { name: 'Sign in with Cypress Test' }).click()
cy.title().should('eq', 'sample project')
cy.visit(redirectTo)
},
) This approach only has one account that can access email/password. Mainly to access pages behind auth. my production only use OAuth and, unfortunately, can't test it on e2e because it acts as bot. I'm looking into using Keycloak https://authjs.dev/guides/testing since the core team suggests using it. |
Beta Was this translation helpful? Give feedback.
-
Not sure if this is still an issue for others but I found it to be a pain so am sharing my solution here in case anyone else gets some use from it. The issue in my case was due to the auth being tied to a DB which also either needed to use real session data or be stubbed. As I didn't want to write my own adaptor I found the following to be the simplest approach in my case:
The following is an example of combining the above steps to create an e2e test describe("Admin Dashboard", () => {
// Run the following once before we start our tests or contexts so that we can just seed the DB once.
// If we want to seed the DB between each test or suite move this to a `beforeEach` block
before(() => {
cy.task("db:seed");
});
// Like above but for resetting the DB after our test suite finishes
after(() => {
cy.task("db:reset");
});
// Example on an unauthenticated route
context("when not logged in", () => {
beforeEach(() => {
cy.visit("/v1/");
});
it("should display an auth wall", () => {
// Test cases
});
});
// Example on an authenticated route
context("when logged in as admin", () => {
// Run once before all tests in this context to generate our session fixture
before(() => {
// Authenticate as a client user
cy.task("authenticate", "admin");
});
// Run before each test in this context to setup our authenticated session
beforeEach(() => {
// Stub the session route and control the response according to the role
// passed to our authentication task <-- The user role is passed above
// to the authentication task so we do not need to repeat it here
cy.login();
// Visit our route
cy.visit("/v1");
// Wait for the session to be authenticated <-- This is necessary as we
// are stubbing the network request and we need to wait for our stub to
// return the response over the actual route handler
cy.wait("@session");
});
it("should be authenticated", () => {
// Test cases
});
});
}); |
Beta Was this translation helpful? Give feedback.
-
Question 💬
I followed this documentation and it isn't quite enough for me to figure out how to use Cypress with NextAuth:
https://next-auth.js.org/tutorials/testing-with-cypress
My home page has a link that calls the
signIn
method:When I run the test that is suggested in the documentation, I get the error (note I switched it to GitHub to see if that makes a difference, but the results are identical with Google as well)
Even creating a direct link like this on my homepage does not work either:
Is there a working example of a nextjs app using NextAuth and Cypress? I searched the Github public repositories pretty extensively, but I only found one repo and it didn't seem to work for me.
Thank you!
Beta Was this translation helpful? Give feedback.
All reactions