Skip to content

Commit

Permalink
100% test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulAsjes committed Dec 4, 2024
1 parent 33559b0 commit 72f1c2e
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 31 deletions.
14 changes: 14 additions & 0 deletions __tests__/authkit-callback-route.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ describe('authkit-callback-route', () => {
expect(data.error.message).toBe('Something went wrong');
});

it('should handle authentication failure if a non-Error object is thrown', async () => {
// Mock authentication failure
(workos.userManagement.authenticateWithCode as jest.Mock).mockRejectedValue('Auth failed');

request.nextUrl.searchParams.set('code', 'invalid-code');

const handler = handleAuth();
const response = await handler(request);

expect(response.status).toBe(500);
const data = await response.json();
expect(data.error.message).toBe('Something went wrong');
});

it('should handle missing code parameter', async () => {
const handler = handleAuth();
const response = await handler(request);
Expand Down
13 changes: 13 additions & 0 deletions __tests__/min-max-button.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ describe('MinMaxButton', () => {
expect(root).toHaveStyle({ '--wi-minimized': '1' });
});

it('does nothing if root is undefined', () => {
const { getByRole } = render(<MinMaxButton minimizedValue="1">Minimize</MinMaxButton>);

const root = document.querySelector('[data-workos-impersonation-root]');

// Mock querySelector to return null for this test
jest.spyOn(document, 'querySelector').mockReturnValue(null);

const button = getByRole('button');
fireEvent.click(button);
expect(root).not.toHaveStyle({ '--wi-minimized': '1' });
});

it('renders children correctly', () => {
const { getByText } = render(<MinMaxButton minimizedValue="0">Test Child</MinMaxButton>);

Expand Down
37 changes: 36 additions & 1 deletion __tests__/session.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,14 @@ describe('session.ts', () => {

(jwtVerify as jest.Mock).mockReset();

consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation((...args) => {
console.info(...args);
});
});

afterEach(() => {
consoleLogSpy.mockRestore();
jest.resetModules();
});

describe('withAuth', () => {
Expand Down Expand Up @@ -392,6 +395,38 @@ describe('session.ts', () => {
}).rejects.toThrow();
});

it('should throw an error if the provided regex is invalid and a non-Error object is thrown', async () => {
// Reset modules to ensure clean import state
jest.resetModules();

// Import first, then spy
const pathToRegexp = await import('path-to-regexp');
const parseSpy = jest.spyOn(pathToRegexp, 'parse').mockImplementation(() => {
throw 'invalid regex';
});

// Import session after setting up the spy
const { updateSession } = await import('../src/session.js');

const request = new NextRequest(new URL('http://example.com/invalid-regex'));

await expect(async () => {
await updateSession(
request,
false,
{
enabled: true,
unauthenticatedPaths: ['[*'],
},
process.env.NEXT_PUBLIC_WORKOS_REDIRECT_URI as string,
[],
);
}).rejects.toThrow('Error parsing routes for middleware auth. Reason: invalid regex');

// Verify the mock was called
expect(parseSpy).toHaveBeenCalled();
});

it('should default to the WORKOS_REDIRECT_URI environment variable if no redirect URI is provided', async () => {
const request = new NextRequest(new URL('http://example.com/protected'));
const result = await updateSession(
Expand Down
132 changes: 132 additions & 0 deletions __tests__/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { NextResponse } from 'next/server';
import { redirectWithFallback, errorResponseWithFallback } from '../src/utils.js';

describe('utils', () => {
afterEach(() => {
jest.resetModules();
});

describe('redirectWithFallback', () => {
it('uses NextResponse.redirect when available', () => {
const redirectUrl = 'https://example.com';
const mockRedirect = jest.fn().mockReturnValue('redirected');
const originalRedirect = NextResponse.redirect;

NextResponse.redirect = mockRedirect;

const result = redirectWithFallback(redirectUrl);

expect(mockRedirect).toHaveBeenCalledWith(redirectUrl);
expect(result).toBe('redirected');

NextResponse.redirect = originalRedirect;
});

it('falls back to standard Response when NextResponse exists but redirect is undefined', async () => {
const redirectUrl = 'https://example.com';

jest.resetModules();

jest.mock('next/server', () => ({
NextResponse: {
// exists but has no redirect method
},
}));

const { redirectWithFallback } = await import('../src/utils.js');

const result = redirectWithFallback(redirectUrl);

expect(result).toBeInstanceOf(Response);
expect(result.status).toBe(307);
expect(result.headers.get('Location')).toBe(redirectUrl);
});

it('falls back to standard Response when NextResponse is undefined', async () => {
const redirectUrl = 'https://example.com';

jest.resetModules();

// Mock with undefined NextResponse
jest.mock('next/server', () => ({
NextResponse: undefined,
}));

const { redirectWithFallback } = await import('../src/utils.js');

const result = redirectWithFallback(redirectUrl);

expect(result).toBeInstanceOf(Response);
expect(result.status).toBe(307);
expect(result.headers.get('Location')).toBe(redirectUrl);
});
});

describe('errorResponseWithFallback', () => {
const errorBody = {
error: {
message: 'Test error',
description: 'Test description',
},
};

it('uses NextResponse.json when available', () => {
const mockJson = jest.fn().mockReturnValue('error json response');
NextResponse.json = mockJson;

const result = errorResponseWithFallback(errorBody);

expect(mockJson).toHaveBeenCalledWith(errorBody, { status: 500 });
expect(result).toBe('error json response');
});

it('falls back to standard Response when NextResponse is not available', () => {
const originalJson = NextResponse.json;

// @ts-expect-error - This is to test the fallback
delete NextResponse.json;

const result = errorResponseWithFallback(errorBody);

expect(result).toBeInstanceOf(Response);
expect(result.status).toBe(500);
expect(result.headers.get('Content-Type')).toBe('application/json');

NextResponse.json = originalJson;
});

it('falls back to standard Response when NextResponse exists but json is undefined', async () => {
jest.resetModules();

jest.mock('next/server', () => ({
NextResponse: {
// exists but has no json method
},
}));

const { errorResponseWithFallback } = await import('../src/utils.js');

const result = errorResponseWithFallback(errorBody);

expect(result).toBeInstanceOf(Response);
expect(result.status).toBe(500);
expect(result.headers.get('Content-Type')).toBe('application/json');
});

it('falls back to standard Response when NextResponse is undefined', async () => {
jest.resetModules();

jest.mock('next/server', () => ({
NextResponse: undefined,
}));

const { errorResponseWithFallback } = await import('../src/utils.js');

const result = errorResponseWithFallback(errorBody);

expect(result).toBeInstanceOf(Response);
expect(result.status).toBe(500);
expect(result.headers.get('Content-Type')).toBe('application/json');
});
});
});
66 changes: 66 additions & 0 deletions __tests__/workos.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { WorkOS } from '@workos-inc/node';
import { workos, VERSION } from '../src/workos.js';

describe('workos', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('initializes WorkOS with the correct configuration', () => {
// Extracting the config to avoid a circular dependency error
const workosConfig = {
apiHostname: workos.options.apiHostname,
https: workos.options.https,
port: workos.options.port,
appInfo: workos.options.appInfo,
};

expect(workosConfig).toEqual({
apiHostname: undefined,
https: true,
port: undefined,
appInfo: {
name: 'authkit/nextjs',
version: VERSION,
},
});
});

it('exports a WorkOS instance', () => {
expect(workos).toBeInstanceOf(WorkOS);
});

describe('with custom environment variables', () => {
const originalEnv = process.env;

beforeEach(() => {
jest.resetModules();
process.env = { ...originalEnv };
});

afterEach(() => {
process.env = originalEnv;
});

it('uses custom API hostname when provided', async () => {
process.env.WORKOS_API_HOSTNAME = 'custom.workos.com';
const { workos: customWorkos } = await import('../src/workos.js');

expect(customWorkos.options.apiHostname).toEqual('custom.workos.com');
});

it('uses custom HTTPS setting when provided', async () => {
process.env.WORKOS_API_HTTPS = 'false';
const { workos: customWorkos } = await import('../src/workos.js');

expect(customWorkos.options.https).toEqual(false);
});

it('uses custom port when provided', async () => {
process.env.WORKOS_API_PORT = '8080';
const { workos: customWorkos } = await import('../src/workos.js');

expect(customWorkos.options.port).toEqual(8080);
});
});
});
9 changes: 9 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ const config: Config = {

// Optionally, add these for better TypeScript support
extensionsToTreatAsEsm: ['.ts'],

coverageThreshold: {
global: {
branches: 100,
functions: 100,
lines: 100,
statements: 100,
},
},
};

export default config;
25 changes: 5 additions & 20 deletions src/authkit-callback-route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { NextRequest, NextResponse } from 'next/server';
import { NextRequest } from 'next/server';
import { cookies } from 'next/headers';
import { workos } from './workos.js';
import { WORKOS_CLIENT_ID, WORKOS_COOKIE_NAME } from './env-variables.js';
import { encryptSession } from './session.js';
import { errorResponseWithFallback, redirectWithFallback } from './utils.js';
import { getCookieOptions } from './cookie.js';
import { HandleAuthOptions } from './interfaces.js';

Expand Down Expand Up @@ -58,14 +59,7 @@ export function handleAuth(options: HandleAuthOptions = {}) {

// Fall back to standard Response if NextResponse is not available.
// This is to support Next.js 13.
const response = NextResponse?.redirect
? NextResponse.redirect(url)
: new Response(null, {
status: 302,
headers: {
Location: url.toString(),
},
});
const response = redirectWithFallback(url.toString());

if (!accessToken || !refreshToken) throw new Error('response is missing tokens');

Expand Down Expand Up @@ -93,20 +87,11 @@ export function handleAuth(options: HandleAuthOptions = {}) {
};

function errorResponse() {
const errorBody = {
return errorResponseWithFallback({
error: {
message: 'Something went wrong',
description: "Couldn't sign in. If you are not sure what happened, please contact your organization admin.",
},
};

// Use NextResponse if available, fallback to standard Response
// This is to support Next.js 13.
return NextResponse?.json
? NextResponse.json(errorBody, { status: 500 })
: new Response(JSON.stringify(errorBody), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
});
}
}
12 changes: 3 additions & 9 deletions src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { getAuthorizationUrl } from './get-authorization-url.js';
import { AccessToken, AuthkitMiddlewareAuth, NoUserInfo, Session, UserInfo } from './interfaces.js';

import { parse, tokensToRegexp } from 'path-to-regexp';
import { redirectWithFallback } from './utils.js';

const sessionHeaderName = 'x-workos-session';
const middlewareHeaderName = 'x-workos-middleware';
Expand Down Expand Up @@ -90,7 +91,7 @@ async function updateSession(

const redirectTo = await getAuthorizationUrl({
returnPathname: getReturnPathname(request.url),
redirectUri: redirectUri ?? WORKOS_REDIRECT_URI,
redirectUri: redirectUri,
screenHint: getScreenHint(signUpPaths, request.nextUrl.pathname),
});

Expand Down Expand Up @@ -235,6 +236,7 @@ function getMiddlewareAuthPathRegex(pathGlob: string) {

return new RegExp(regex);
} catch (err) {
console.log('err', err);
const message = err instanceof Error ? err.message : String(err);

throw new Error(`Error parsing routes for middleware auth. Reason: ${message}`);
Expand Down Expand Up @@ -390,12 +392,4 @@ function getScreenHint(signUpPaths: string[] | undefined, pathname: string) {
return screenHintPaths.length > 0 ? 'sign-up' : 'sign-in';
}

function redirectWithFallback(redirectUri: string) {
// Fall back to standard Response if NextResponse is not available.
// This is to support Next.js 13.
return NextResponse?.redirect
? NextResponse.redirect(redirectUri)
: new Response(null, { status: 307, headers: { Location: redirectUri } });
}

export { encryptSession, withAuth, refreshSession, terminateSession, updateSession, getSession };
Loading

0 comments on commit 72f1c2e

Please sign in to comment.