-
Notifications
You must be signed in to change notification settings - Fork 4
[#152] Test: created integration test for supabase/middleware #402
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
aac36fc
a7f31f2
ca66076
b3212a1
9a5a43a
2a0b077
2664509
6eddb06
f6ce60e
cc7917a
16815e2
0872aa4
80928ef
111ff20
2858d91
56a76e3
9ffc919
c6fa2c0
7f574b6
692ade2
00690a5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| /** | ||
| * @jest-environment node | ||
| */ | ||
|
|
||
| import { NextRequest } from 'next/server'; | ||
| import { createAuthedRequest, updateSession } from './middleware'; | ||
|
|
||
| describe('Middleware', () => { | ||
| it('redirects unauthenticated user back to /', async () => { | ||
| const request = new NextRequest('https://localhost:4000/dashboard'); | ||
| const response = await updateSession(request); | ||
| expect(response.status).toBe(307); | ||
| expect(response.headers.get('location')).toBe('https://localhost:4000/'); | ||
| }); | ||
| it('redirects user to go to /onboarding if their onboarding is set as false', async () => { | ||
| const response = await createAuthedRequest({ | ||
| email: 'onboardingfalse@test.com', | ||
| password: 'password', | ||
| path: '/dashboard', | ||
| }); | ||
| expect(response.status).toBe(307); | ||
| expect(response.headers.get('location')).toBe( | ||
| 'https://localhost:4000/onboarding', | ||
| ); | ||
| }); | ||
|
|
||
| it('allows user to go to /dashboard if their onboarding is set as true', async () => { | ||
| const response = await createAuthedRequest({ | ||
| email: 'onboardingtrue@test.com', | ||
| password: 'password', | ||
| path: '/dashboard', | ||
| }); | ||
| expect(response.status).toBe(200); | ||
| }); | ||
|
|
||
| it('does not redirect user away from /onboarding if they want to go back and edit their profile', async () => { | ||
| const response = await createAuthedRequest({ | ||
| email: 'onboardingtrue@test.com', | ||
| password: 'password', | ||
| path: '/onboarding', | ||
| }); | ||
| expect(response.status).toBe(200); | ||
| }); | ||
|
|
||
| it('allows unauthenticated user to access /gift-exchanges to sign up and join the group via invite link', async () => { | ||
| const request = new NextRequest('https://localhost:4000/gift-exchanges'); | ||
| const response = await updateSession(request); | ||
| expect(response.status).toBe(200); | ||
| }); | ||
|
|
||
| it('throws an error if Supabase fails to fetch the user', async () => { | ||
| const invalidToken = 'sb-access-token=invalid.token.value'; | ||
| const request = new NextRequest('https://localhost:4000/dashboard', { | ||
| headers: new Headers({ | ||
| cookie: invalidToken, | ||
| }), | ||
| }); | ||
|
|
||
| await expect(updateSession(request)).rejects.toThrow(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,58 @@ | ||
| // Copyright (c) Gridiron Survivor. | ||
| // Licensed under the MIT License. | ||
|
|
||
| import { createServerClient } from '@supabase/ssr'; | ||
| import { NextResponse, type NextRequest } from 'next/server'; | ||
| import { createClient } from '@supabase/supabase-js'; | ||
| import { NextRequest, NextResponse } from 'next/server'; | ||
|
|
||
| /** | ||
| * Creates an authenticated request to the specified path using the provided email and password. | ||
| * @param {object} props - The parameters for authentication and request. | ||
| * @param {string} props.email - The user's email address. | ||
| * @param {string} props.password - The user's password. | ||
| * @param {string} props.path - The path to send the request to. | ||
| * @returns {Promise<NextResponse>} A promise that resolves to a NextResponse object. | ||
| */ | ||
| export const createAuthedRequest = async ({ | ||
| email, | ||
| password, | ||
| path, | ||
| }: { | ||
| email: string; | ||
| password: string; | ||
| path: string; | ||
| }): Promise<NextResponse> => { | ||
| const supabase = createClient( | ||
| process.env.NEXT_PUBLIC_SUPABASE_URL!, | ||
| process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, | ||
| ); | ||
|
|
||
| const { | ||
| data: { session }, | ||
| } = await supabase.auth.signInWithPassword({ | ||
| email, | ||
| password, | ||
| }); | ||
|
|
||
| const cookieHeader = `sb-access-token=${session?.access_token}; sb-refresh-token=${session?.refresh_token}`; | ||
|
|
||
| export async function updateSession(request: NextRequest) { | ||
| const request = new NextRequest(`https://localhost:4000${path}`, { | ||
| headers: new Headers({ | ||
| cookie: cookieHeader, | ||
| }), | ||
| }); | ||
|
|
||
| return await updateSession(request); | ||
| }; | ||
|
|
||
| /** | ||
| * Updates the session based on the incoming request and redirects if necessary. | ||
| * @param {NextRequest} request - The incoming request object. | ||
| * @returns {Promise<NextResponse>} A promise that resolves to a NextResponse object. | ||
| */ | ||
| export async function updateSession( | ||
| request: NextRequest, | ||
| ): Promise<NextResponse> { | ||
| let supabaseResponse = NextResponse.next({ | ||
| request, | ||
| }); | ||
|
|
@@ -11,9 +62,17 @@ export async function updateSession(request: NextRequest) { | |
| process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, | ||
| { | ||
| cookies: { | ||
| /** | ||
| * Retrieves all cookies from the request. | ||
| * @returns {Array} An array of cookies. | ||
| */ | ||
| getAll() { | ||
| return request.cookies.getAll(); | ||
| }, | ||
| /** | ||
| * Sets all cookies provided in the cookiesToSet array. | ||
| * @param {Array} cookiesToSet - An array of cookies to set, each containing name, value, and options. | ||
| */ | ||
| setAll(cookiesToSet) { | ||
| cookiesToSet.forEach(({ name, value }) => | ||
| request.cookies.set(name, value), | ||
|
|
@@ -33,11 +92,22 @@ export async function updateSession(request: NextRequest) { | |
| // supabase.auth.getUser(). A simple mistake could make it very hard to debug | ||
| // issues with users being randomly logged out. | ||
|
|
||
| const { | ||
| data: { user }, | ||
| } = await supabase.auth.getUser(); | ||
| const accessToken = request.cookies.get('sb-access-token')?.value; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From what I’ve read, That said, does this mean middleware-based auth hasn’t been working since we switched to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just following up on this @alexappleget
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When I was writing the test, I had to log in via the email way so that way we can use test users and not the google auth. When doing so, I noticed that the test kept throwing
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. auth was working for google auth, but i believe we need to clean up our auth context as well. it's not handling things correctly |
||
|
|
||
| let user = null; | ||
|
|
||
| if (accessToken) { | ||
| const { | ||
| data: { user: fetchedUser }, | ||
| error, | ||
| } = await supabase.auth.getUser(accessToken); | ||
| if (error) { | ||
| throw error; | ||
| } | ||
|
|
||
| user = fetchedUser; | ||
| } | ||
|
|
||
| // If the user is not authenticated redirect the user to the login page | ||
| if ( | ||
| !user && | ||
| !request.nextUrl.pathname.startsWith('/auth/login') && | ||
|
|
@@ -46,23 +116,18 @@ export async function updateSession(request: NextRequest) { | |
| request.nextUrl.pathname !== '/' && | ||
| !request.nextUrl.pathname.startsWith('/gift-exchanges') | ||
| ) { | ||
| // no user, potentially respond by redirecting the user to the login page | ||
| const url = request.nextUrl.clone(); | ||
| url.pathname = '/'; | ||
| return NextResponse.redirect(url); | ||
| // Todo: Uncomment this to redirect unauthenticated users to the login page | ||
| } | ||
|
|
||
| // if the user is authenticated check if they have already onboarded | ||
| if (user) { | ||
| // Get the user's profile | ||
| const { data: profile } = await supabase | ||
| .from('profiles') | ||
| .select('onboarding_complete') | ||
| .eq('id', user.id) | ||
| .single(); | ||
|
|
||
| // If user is not onboarded and not already on the onboarding page, redirect to onboarding | ||
| if ( | ||
| profile && | ||
| !profile.onboarding_complete && | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're testing local or staging?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also make the base URL an environment variable or at a minimum a constant and then you can do something like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't make this change yet as the staging/production url has not been set up yet on vercel