Skip to content

Commit

Permalink
added tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dvorakjt committed Sep 22, 2024
1 parent 1fb864c commit fc86e7a
Show file tree
Hide file tree
Showing 4 changed files with 396 additions and 0 deletions.
137 changes: 137 additions & 0 deletions src/__tests__/unit/components/guards/has-not-completed-action.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { hasNotCompletedAction } from '@/components/guards/has-not-completed-action';
import { render, screen, cleanup } from '@testing-library/react';
import '@testing-library/jest-dom';
import navigation from 'next/navigation';
import { Builder } from 'builder-pattern';
import { UserContext, type UserContextType } from '@/contexts/user-context';
import { Actions } from '@/model/enums/actions';
import type { User } from '@/model/types/user';
import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';

jest.mock('next/navigation', () => ({
useRouter: jest.fn(),
}));

describe('hasNotCompletedAction', () => {
let router: AppRouterInstance;

beforeEach(() => {
router = Builder<AppRouterInstance>().push(jest.fn()).build();
jest.spyOn(navigation, 'useRouter').mockImplementation(() => router);
});

afterEach(cleanup);

it('returns a component that can be rendered.', () => {
const user = Builder<User>()
.completedActions({
electionReminders: false,
registerToVote: false,
sharedChallenge: false,
})
.build();

const userContextValue = Builder<UserContextType>().user(user).build();

const TestComponent = hasNotCompletedAction(
function () {
return <div data-testid="test"></div>;
},
{ action: Actions.ElectionReminders, redirectTo: '/' },
);

render(
<UserContext.Provider value={userContextValue}>
<TestComponent />
</UserContext.Provider>,
);
expect(screen.queryByTestId('test')).toBeInTheDocument();
});

it('returns a component that can accept props.', () => {
const user = Builder<User>()
.completedActions({
electionReminders: false,
registerToVote: false,
sharedChallenge: false,
})
.build();

const userContextValue = Builder<UserContextType>().user(user).build();

interface TestComponentProps {
message: string;
}

const TestComponent = hasNotCompletedAction(
function ({ message }: TestComponentProps) {
return <div>{message}</div>;
},
{ action: Actions.ElectionReminders, redirectTo: '/' },
);

const message = 'test';

render(
<UserContext.Provider value={userContextValue}>
<TestComponent message={message} />
</UserContext.Provider>,
);
expect(screen.queryByText(message)).toBeInTheDocument();
});

it('blocks access to a page if the user has completed the provided action.', () => {
const user = Builder<User>()
.completedActions({
electionReminders: true,
registerToVote: false,
sharedChallenge: false,
})
.build();

const userContextValue = Builder<UserContextType>().user(user).build();

const redirectTo = '/';

const TestComponent = hasNotCompletedAction(
function () {
return null;
},
{ action: Actions.ElectionReminders, redirectTo },
);

render(
<UserContext.Provider value={userContextValue}>
<TestComponent />
</UserContext.Provider>,
);

expect(router.push).toHaveBeenCalledWith(redirectTo);
});

it('allows access to a page if the user has not completed the provided action.', () => {
const user = Builder<User>()
.completedActions({
electionReminders: true,
registerToVote: true,
sharedChallenge: false,
})
.build();

const userContextValue = Builder<UserContextType>().user(user).build();

const TestComponent = hasNotCompletedAction(
function () {
return null;
},
{ action: Actions.SharedChallenge, redirectTo: '/' },
);

render(
<UserContext.Provider value={userContextValue}>
<TestComponent />
</UserContext.Provider>,
);
expect(router.push).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { redirectIfSupabaseUserCompletedAction } from '@/services/server/redirect-if-completed-action/redirect-if-supabase-user-completed-action';
import { willBeRedirected } from '@/utils/shared/will-be-redirected';
import { resetAuthAndDatabase } from '@/utils/test/reset-auth-and-database';
import { NextRequest } from 'next/server';
import { getSignedInRequestWithUser } from '@/utils/test/get-signed-in-request-with-user';
import { SupabaseUserRecordBuilder } from '@/utils/test/supabase-user-record-builder';
import { Actions } from '@/model/enums/actions';
import { serverContainer } from '@/services/server/container';
import { SERVER_SERVICE_KEYS } from '@/services/server/keys';

describe('redirectIfSupabaseUserCompletedAction', () => {
afterEach(() => {
return resetAuthAndDatabase();
});

it(`returns a response that redirects a user if they have completed the
specified action.`, async () => {
const user = await new SupabaseUserRecordBuilder('[email protected]')
.completedActions({
electionReminders: true,
registerToVote: false,
sharedChallenge: false,
})
.build();

const request = await getSignedInRequestWithUser(
user,
'https://challenge.8by8.us/reminders',
{
method: 'GET',
},
);

const userRepo = serverContainer.get(SERVER_SERVICE_KEYS.UserRepository);

const response = await redirectIfSupabaseUserCompletedAction(
userRepo,
request,
{
action: Actions.ElectionReminders,
redirectTo: '/',
},
);

expect(willBeRedirected(response)).toBe(true);
});

it(`returns a response that does not redirect the user if they have not
completed the specified action.`, async () => {
const user = await new SupabaseUserRecordBuilder('[email protected]')
.completedActions({
electionReminders: false,
registerToVote: false,
sharedChallenge: false,
})
.build();

const request = await getSignedInRequestWithUser(
user,
'https://challenge.8by8.us/reminders',
{
method: 'GET',
},
);

const userRepo = serverContainer.get(SERVER_SERVICE_KEYS.UserRepository);

const response = await redirectIfSupabaseUserCompletedAction(
userRepo,
request,
{
action: Actions.ElectionReminders,
redirectTo: '/',
},
);

expect(willBeRedirected(response)).toBe(false);
});

it(`returns a response that does not redirect the user if they are signed out.`, async () => {
const request = new NextRequest('https://challenge.8by8.us/reminders', {
method: 'GET',
});

const userRepo = serverContainer.get(SERVER_SERVICE_KEYS.UserRepository);

const response = await redirectIfSupabaseUserCompletedAction(
userRepo,
request,
{
action: Actions.ElectionReminders,
redirectTo: '/',
},
);

expect(willBeRedirected(response)).toBe(false);
});
});
109 changes: 109 additions & 0 deletions src/__tests__/unit/utils/test/get-signed-in-request-with-user.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { getSignedInRequestWithUser } from '@/utils/test/get-signed-in-request-with-user';
import { SupabaseUserRecordBuilder } from '@/utils/test/supabase-user-record-builder';
import { resetAuthAndDatabase } from '@/utils/test/reset-auth-and-database';
import * as supabaseSSR from '@supabase/ssr';
import { AuthError } from '@supabase/supabase-js';

jest.mock('@supabase/ssr', () => ({
__esModule: true,
...jest.requireActual('@supabase/ssr'),
}));

describe('getSignedInRequest', () => {
afterEach(() => {
return resetAuthAndDatabase();
});

afterAll(() => {
jest.unmock('@supabase/ssr');
});

it('returns a request with an authentication cookie.', async () => {
const user = await new SupabaseUserRecordBuilder(
'[email protected]',
).build();

const request = await getSignedInRequestWithUser(
user,
'https://challenge.8by8.org/progress',
{
method: 'GET',
},
);

const authCookieNamePattern = /sb-[^-]+-auth-token/;

expect(
request.cookies
.getAll()
.find(cookie => authCookieNamePattern.test(cookie.name)),
).toBeDefined();
});

it('throws an error if supabase.auth.admin.generateLink() returns an error.', async () => {
const user = await new SupabaseUserRecordBuilder(
'[email protected]',
).build();

const supabaseSpy = jest
.spyOn(supabaseSSR, 'createServerClient')
.mockImplementationOnce(() => ({
auth: {
admin: {
generateLink: () => {
return Promise.resolve({
data: null,
error: new AuthError('Magic link not supported.', 502),
});
},
},
},
}));

await expect(
getSignedInRequestWithUser(user, 'https://challenge.8by8.org/progress', {
method: 'GET',
}),
).rejects.toThrow(new Error('Magic link not supported.'));

supabaseSpy.mockRestore();
});

it('throws an error if supabase.auth.verifyOtp throws an error.', async () => {
const user = await new SupabaseUserRecordBuilder(
'[email protected]',
).build();

const supabaseSpy = jest
.spyOn(supabaseSSR, 'createServerClient')
.mockImplementationOnce(() => ({
auth: {
admin: {
generateLink: () => {
return Promise.resolve({
data: {
properties: {
email_otp: '123456',
},
},
error: null,
});
},
},
verifyOtp: () => {
return Promise.resolve({
error: new AuthError('Failed to verify otp.'),
});
},
},
}));

await expect(
getSignedInRequestWithUser(user, 'https://challenge.8by8.org/progress', {
method: 'GET',
}),
).rejects.toThrow(new Error('Failed to verify otp.'));

supabaseSpy.mockRestore();
});
});
52 changes: 52 additions & 0 deletions src/utils/test/get-signed-in-request-with-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { PRIVATE_ENVIRONMENT_VARIABLES } from '@/constants/private-environment-variables';
import { PUBLIC_ENVIRONMENT_VARIABLES } from '@/constants/public-environment-variables';
import { createServerClient } from '@supabase/ssr';
import { NextRequest } from 'next/server';
import { MockNextCookies } from './mock-next-cookies';
import type { User } from '@/model/types/user';

export async function getSignedInRequestWithUser(
user: User,
...args: ConstructorParameters<typeof NextRequest>
) {
const mockCookies = new MockNextCookies();

const supabase = createServerClient(
PUBLIC_ENVIRONMENT_VARIABLES.NEXT_PUBLIC_SUPABASE_URL,
PRIVATE_ENVIRONMENT_VARIABLES.SUPABASE_SERVICE_ROLE_KEY,
{
cookies: {
getAll() {
return mockCookies.cookies().getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
mockCookies.cookies().set(name, value, options),
);
},
},
},
);

const { data, error: generateLinkError } =
await supabase.auth.admin.generateLink({
type: 'magiclink',
email: user.email,
});

if (generateLinkError) throw new Error(generateLinkError.message);

const { error: verifyOtpError } = await supabase.auth.verifyOtp({
email: user.email,
type: 'email',
token: data.properties.email_otp,
});

if (verifyOtpError) throw new Error(verifyOtpError.message);

const authCookie = mockCookies.cookies().getAll()[0];

const request = new NextRequest(...args);
request.cookies.set(authCookie.name, authCookie.value);
return request;
}

0 comments on commit fc86e7a

Please sign in to comment.