Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
aac36fc
[#152] Test: created integration test for supabase/middleware
alexappleget May 12, 2025
a7f31f2
Fix: fixed eslint errors for supabase/middleware file
alexappleget May 12, 2025
ca66076
Merge branch 'develop' into alex/152-create-integration-test-for-supa…
alexappleget May 12, 2025
b3212a1
Fix: added important comments back
alexappleget May 13, 2025
9a5a43a
Merge remote-tracking branch 'origin/develop' into alex/152-create-in…
alexappleget May 20, 2025
2a0b077
Merge branch 'develop' into alex/152-create-integration-test-for-supa…
alexappleget May 20, 2025
2664509
Merge remote-tracking branch 'origin/develop' into alex/152-create-in…
alexappleget May 21, 2025
6eddb06
test: redid tests some but still failing due to request being undefined
alexappleget May 23, 2025
f6ce60e
Test: fixed testing for integration tests
alexappleget May 29, 2025
cc7917a
Merge remote-tracking branch 'origin/develop' into alex/152-create-in…
alexappleget May 29, 2025
16815e2
test: finished supabase middleware testing
alexappleget Jun 2, 2025
0872aa4
Merge remote-tracking branch 'origin/develop' into alex/152-create-in…
alexappleget Jun 2, 2025
80928ef
test: fixed failing test
alexappleget Jun 2, 2025
111ff20
Merge branch 'develop' into alex/152-create-integration-test-for-supa…
alexappleget Jun 16, 2025
2858d91
Fix: enhanced wording of test cases
alexappleget Jun 16, 2025
56a76e3
Merge remote-tracking branch 'origin/develop' into alex/152-create-in…
alexappleget Jun 20, 2025
9ffc919
Merge branch 'develop' into alex/152-create-integration-test-for-supa…
alexappleget Jun 20, 2025
c6fa2c0
Merge remote-tracking branch 'origin/develop' into alex/152-create-in…
alexappleget Jun 24, 2025
7f574b6
Fix: created helper test function to clean up tests and to be used fo…
alexappleget Jun 24, 2025
692ade2
Merge branch 'develop' into alex/152-create-integration-test-for-supa…
alexappleget Jun 27, 2025
00690a5
Merge branch 'develop' into alex/152-create-integration-test-for-supa…
shashilo Jul 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,6 @@ lib/generateAndStoreSuggestion.ts
lib/getAmazonImage.ts
lib/getUserAvatar.ts
lib/getUserAvatar.ts
lib/supabase/client.ts
lib/supabase/middleware.ts
lib/utils.ts
61 changes: 61 additions & 0 deletions lib/supabase/middleware/middleware.test.ts
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');

Copy link
Copy Markdown
Collaborator

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?

@nickytonline nickytonline Jun 21, 2025

Copy link
Copy Markdown
Member

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.

Suggested change
const request = new NextRequest('https://localhost:4000/dashboard');
const request = new NextRequest(`${new URL('/dashboard', BASE_APP_URL)}`);

Copy link
Copy Markdown
Collaborator Author

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

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,
});
Expand All @@ -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),
Expand All @@ -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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I’ve read, @supabase/ssr doesn’t include the createMiddlewareClient helper like the older @supabase/auth-helpers-nextjs package did. So manually reading the cookie to get the access token is the correct approach here.

That said, does this mean middleware-based auth hasn’t been working since we switched to @supabase/ssr? Just wondering if we had a gap in session handling before this change.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just following up on this @alexappleget

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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 null for the user and that it couldn't find it. After digging, I found that the issue was happening here with fetching the user. So i added in that the helper function could grab the accesstoken itself, and if so then it fetches the user. Thinking about it, I do believe supabase had a gap in the auth and my test caught it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The 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') &&
Expand All @@ -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 &&
Expand Down
2 changes: 1 addition & 1 deletion middleware.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type NextRequest } from 'next/server';
import { updateSession } from './lib/supabase/middleware';
import { updateSession } from './lib/supabase/middleware/middleware';

export async function middleware(request: NextRequest) {
return await updateSession(request);
Expand Down