Skip to content
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

feat(adapter-nextjs): add createAuthRouteHandlers to createServerRunner #13801

Open
wants to merge 1 commit into
base: hui/feat/adapter-nextjs/apply-cookie-attrs
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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', () => {
const request = new NextRequest(
new URL('https://example.com/api/auth/sign-in'),
{
method: 'POST',
},
);
const response = 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', () => {
const request = new NextRequest(
new URL('https://example.com/api/auth/sign-in'),
{
method: 'GET',
},
);
const response = 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', () => {
const request = new NextRequest(
new URL('https://example.com/api/auth/exchange-token'),
{
method: 'GET',
},
);
const response = 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', () => {
const request = new NextRequest(
new URL('https://example.com/api/auth/sign-in'),
{
method: 'GET',
},
);
const response = handleAuthApiRouteRequestForAppRouter({
request,
handlerContext: { params: { slug: 'sign-in' } },
handlerInput: testHandlerInput,
oAuthConfig: testOAuthConfig,
setCookieOptions: {},
origin: testOrigin,
});

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