Skip to content

Commit

Permalink
feat(adapter-nextjs): add createAuthRouteHandlers to createServerRunner
Browse files Browse the repository at this point in the history
  • Loading branch information
HuiSF committed Sep 11, 2024
1 parent 6cbdad1 commit 8e12c20
Show file tree
Hide file tree
Showing 18 changed files with 895 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import { ResourcesConfig } from 'aws-amplify';
import {
assertOAuthConfig,
assertTokenProviderConfig,
} from '@aws-amplify/core/internals/utils';

import { createAuthRouteHandlersFactory } from '../../src/auth/createAuthRouteHandlersFactory';
import { handleAuthApiRouteRequestForAppRouter } from '../../src/auth/handleAuthApiRouteRequestForAppRouter';
import { handleAuthApiRouteRequestForPagesRouter } from '../../src/auth/handleAuthApiRouteRequestForPagesRouter';
import { NextServer } from '../../src';
import {
AuthRouteHandlers,
CreateAuthRoutesHandlersInput,
} from '../../src/auth/types';
import {
isAuthRoutesHandlersContext,
isNextApiRequest,
isNextApiResponse,
isNextRequest,
} from '../../src/auth/utils';

jest.mock('@aws-amplify/core/internals/utils');
jest.mock('../../src/auth/handleAuthApiRouteRequestForAppRouter');
jest.mock('../../src/auth/handleAuthApiRouteRequestForPagesRouter');
jest.mock('../../src/auth/utils');

const mockAmplifyConfig: ResourcesConfig = {
Auth: {
Cognito: {
identityPoolId: '123',
userPoolId: 'abc',
userPoolClientId: 'def',
loginWith: {
oauth: {
domain: 'example.com',
responseType: 'code',
redirectSignIn: ['https://example.com/signin'],
redirectSignOut: ['https://example.com/signout'],
scopes: ['openid', 'email'],
},
},
},
},
};

const mockRuntimeOptions: NextServer.CreateServerRunnerRuntimeOptions = {
cookies: {
sameSite: 'strict',
},
};
const mockAssertTokenProviderConfig = jest.mocked(assertTokenProviderConfig);
const mockAssertOAuthConfig = jest.mocked(assertOAuthConfig);
const mockHandleAuthApiRouteRequestForAppRouter = jest.mocked(
handleAuthApiRouteRequestForAppRouter,
);
const mockHandleAuthApiRouteRequestForPagesRouter = jest.mocked(
handleAuthApiRouteRequestForPagesRouter,
);
const mockIsNextApiRequest = jest.mocked(isNextApiRequest);
const mockIsNextApiResponse = jest.mocked(isNextApiResponse);
const mockIsNextRequest = jest.mocked(isNextRequest);
const mockIsAuthRoutesHandlersContext = jest.mocked(
isAuthRoutesHandlersContext,
);

describe('createAuthRoutesHandlersFactory', () => {
const existingProcessEnvVars = { ...process.env };
const modifiedProcessEnvVars = {
...process.env,
AMPLIFY_APP_ORIGIN: 'https://example.com',
};

beforeEach(() => {
process.env = modifiedProcessEnvVars;
});

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

it('throws an error if the AMPLIFY_APP_ORIGIN environment variable is not set', () => {
process.env = { ...existingProcessEnvVars, AMPLIFY_APP_ORIGIN: undefined };
expect(() =>
createAuthRouteHandlersFactory({
config: mockAmplifyConfig,
runtimeOptions: mockRuntimeOptions,
}),
).toThrow('Could not find the AMPLIFY_APP_ORIGIN environment variable.');
});

it('calls config assertion functions to validate the Auth configuration', () => {
createAuthRouteHandlersFactory({
config: mockAmplifyConfig,
runtimeOptions: mockRuntimeOptions,
});

expect(mockAssertTokenProviderConfig).toHaveBeenCalledWith(
mockAmplifyConfig.Auth?.Cognito,
);
expect(mockAssertOAuthConfig).toHaveBeenCalledWith(
mockAmplifyConfig.Auth!.Cognito,
);
});

describe('the created route handler function', () => {
const testCreateAuthRoutesHandlersFactoryInput = {
config: mockAmplifyConfig,
runtimeOptions: mockRuntimeOptions,
};
const testCreateAuthRoutesHandlersInput: CreateAuthRoutesHandlersInput = {
customState: 'random-state',
redirectOnSignInComplete: '/home',
redirectOnSignOutComplete: '/login',
};
let handler: AuthRouteHandlers;

beforeAll(() => {
process.env = modifiedProcessEnvVars;
const createAuthRoutesHandlers = createAuthRouteHandlersFactory(
testCreateAuthRoutesHandlersFactoryInput,
);
handler = createAuthRoutesHandlers(testCreateAuthRoutesHandlersInput);
});

afterAll(() => {
process.env = existingProcessEnvVars;
});

afterEach(() => {
mockIsAuthRoutesHandlersContext.mockReset();
mockIsNextApiRequest.mockReset();
mockIsNextApiResponse.mockReset();
mockIsNextRequest.mockReset();
});

it('calls handleAuthApiRouteRequestForPagesRouter when 1st param is a NextApiRequest and 2nd param is a NextApiResponse', async () => {
const param1 = {} as any;
const param2 = {} as any;
mockIsNextApiRequest.mockReturnValueOnce(true);
mockIsNextApiResponse.mockReturnValueOnce(true);
mockIsNextRequest.mockReturnValueOnce(false);
mockIsAuthRoutesHandlersContext.mockReturnValueOnce(false);

await handler(param1, param2);

expect(mockHandleAuthApiRouteRequestForPagesRouter).toHaveBeenCalledWith({
request: param1,
response: param2,
handlerInput: testCreateAuthRoutesHandlersInput,
oAuthConfig: mockAmplifyConfig.Auth!.Cognito!.loginWith!.oauth,
setCookieOptions: mockRuntimeOptions.cookies,
origin: 'https://example.com',
});
});

it('calls handleAuthApiRouteRequestForAppRouter when 1st param is a NextRequest and the 2nd param is a AuthRoutesHandlersContext', async () => {
const request = {} as any;
const context = {} as any;
mockIsNextApiRequest.mockReturnValueOnce(false);
mockIsNextApiResponse.mockReturnValueOnce(false);
mockIsNextRequest.mockReturnValueOnce(true);
mockIsAuthRoutesHandlersContext.mockReturnValueOnce(true);

await handler(request, context);

expect(mockHandleAuthApiRouteRequestForAppRouter).toHaveBeenCalledWith({
request,
handlerContext: context,
handlerInput: testCreateAuthRoutesHandlersInput,
oAuthConfig: mockAmplifyConfig.Auth!.Cognito!.loginWith!.oauth,
setCookieOptions: mockRuntimeOptions.cookies,
origin: 'https://example.com',
});
});

it('throws an error when the request and context/response combination is invalid', () => {
const request = {} as any;
const context = {} as any;
mockIsNextApiRequest.mockReturnValueOnce(false);
mockIsNextApiResponse.mockReturnValueOnce(false);
mockIsNextRequest.mockReturnValueOnce(false);
mockIsAuthRoutesHandlersContext.mockReturnValueOnce(false);

expect(handler(request, context)).rejects.toThrow(
'Invalid request and context/response combination. The request cannot be handled.',
);
});

it('uses default values for parameters that have values as undefined', async () => {
const createAuthRoutesHandlers = createAuthRouteHandlersFactory({
config: mockAmplifyConfig,
runtimeOptions: undefined,
});
const handlerWithDefaultParamValues =
createAuthRoutesHandlers(/* undefined */);

const request = {} as any;
const response = {} as any;

mockIsNextApiRequest.mockReturnValueOnce(true);
mockIsNextApiResponse.mockReturnValueOnce(true);
mockIsNextRequest.mockReturnValueOnce(false);
mockIsAuthRoutesHandlersContext.mockReturnValueOnce(false);

await handlerWithDefaultParamValues(request, response);

expect(handleAuthApiRouteRequestForPagesRouter).toHaveBeenCalledWith({
request,
response,
handlerInput: {},
oAuthConfig: mockAmplifyConfig.Auth!.Cognito!.loginWith!.oauth,
setCookieOptions: {},
origin: 'https://example.com',
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* @jest-environment node
*/
import { NextRequest } from 'next/server';
import { OAuthConfig } from '@aws-amplify/core';

import { handleAuthApiRouteRequestForAppRouter } from '../../src/auth/handleAuthApiRouteRequestForAppRouter';
import { CreateAuthRoutesHandlersInput } from '../../src/auth/types';

describe('handleAuthApiRouteRequestForAppRouter', () => {
const testOrigin = 'https://example.com';
const testHandlerInput: CreateAuthRoutesHandlersInput = {
redirectOnSignInComplete: '/home',
redirectOnSignOutComplete: 'sign-in',
};
const testHandlerContext = { params: { slug: 'sign-in' } };
const testOAuthConfig: OAuthConfig = {
domain: 'example.com',
redirectSignIn: ['https://example.com/signin'],
redirectSignOut: ['https://example.com/signout'],
responseType: 'code',
scopes: ['openid', 'email'],
};
const _ = handleAuthApiRouteRequestForAppRouter;

it('returns a 405 response when input.request has an unsupported method', async () => {
const request = new NextRequest(
new URL('https://example.com/api/auth/sign-in'),
{
method: 'POST',
},
);
const response = await handleAuthApiRouteRequestForAppRouter({
request,
handlerContext: testHandlerContext,
handlerInput: testHandlerInput,
oAuthConfig: testOAuthConfig,
setCookieOptions: {},
origin: testOrigin,
});

expect(response.status).toBe(405);
});

it('returns a 400 response when handlerContext.params.slug is undefined', async () => {
const request = new NextRequest(
new URL('https://example.com/api/auth/sign-in'),
{
method: 'GET',
},
);
const response = await handleAuthApiRouteRequestForAppRouter({
request,
handlerContext: { params: { slug: undefined } },
handlerInput: testHandlerInput,
oAuthConfig: testOAuthConfig,
setCookieOptions: {},
origin: testOrigin,
});

expect(response.status).toBe(400);
});

it('returns a 404 response when handlerContext.params.slug is not a supported path', async () => {
const request = new NextRequest(
new URL('https://example.com/api/auth/exchange-token'),
{
method: 'GET',
},
);
const response = await handleAuthApiRouteRequestForAppRouter({
request,
handlerContext: { params: { slug: 'exchange-token' } },
handlerInput: testHandlerInput,
oAuthConfig: testOAuthConfig,
setCookieOptions: {},
origin: testOrigin,
});

expect(response.status).toBe(404);
});

// TODO(HuiSF): add use cases tests for each supported path when implemented
it('returns a 501 response when handlerContext.params.slug is a supported path', async () => {
const request = new NextRequest(
new URL('https://example.com/api/auth/sign-in'),
{
method: 'GET',
},
);
const response = await handleAuthApiRouteRequestForAppRouter({
request,
handlerContext: { params: { slug: 'sign-in' } },
handlerInput: testHandlerInput,
oAuthConfig: testOAuthConfig,
setCookieOptions: {},
origin: testOrigin,
});

expect(response.status).toBe(501);
});
});
Loading

0 comments on commit 8e12c20

Please sign in to comment.