Skip to content

Commit

Permalink
feat(adapter-nextjs): support next.js 15 (#13947)
Browse files Browse the repository at this point in the history
* feat(adapter-nextjs): support next.js 15

* chore: run e2e tests aginst the changes

* chore: publish next-15 tag

* chore: enable next.js tests against v14 and v15

* chore: run e2e tests

* chore: remove e2e run trigger for merging
  • Loading branch information
HuiSF authored Dec 11, 2024
1 parent 881619d commit 341eee2
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 60 deletions.
22 changes: 16 additions & 6 deletions .github/integ-config/integ-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -875,14 +875,24 @@ tests:
# spec: duplicate-packages
# browser: *minimal_browser_list

# SSR context isolation
- test_name: integ_ssr_context_isolation
desc: 'SSR Context Isolation'
# Next.js use cases
- test_name: integ_next-use-cases-14
desc: 'Next.js use cases tests with v14'
framework: next
category: ssr-adapter
sample_name: ssr-context-isolation
spec: ssr-context-isolation
yarn_script: ci:ssr-context-isolation
sample_name: next-use-cases-14
spec: next-use-cases
yarn_script: ci:next-use-cases-test
yarn_script_args: 14
browser: [chrome]
- test_name: integ_next-use-cases-15
desc: 'Next.js use cases tests with v15'
framework: next
category: ssr-adapter
sample_name: next-use-cases-15
spec: next-use-cases
yarn_script: ci:next-use-cases-test
yarn_script_args: 15
browser: [chrome]
- test_name: integ_next_mfa_req_email
desc: 'mfa required with email sign in attribute'
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/callable-e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ on:
yarn_script:
required: false
type: string
yarn_script_args:
required: false
type: number
env:
required: false
type: string
Expand Down Expand Up @@ -124,6 +127,7 @@ jobs:
E2E_RETRY_COUNT: ${{ inputs.retry_count }}
E2E_TEST_NAME: ${{ inputs.test_name }}
E2E_YARN_SCRIPT: ${{ inputs.yarn_script }}
E2E_YARN_SCRIPT_ARGS: ${{ inputs.yarn_script_args }}
E2E_ENV: ${{ inputs.env }}
run: |
if [ -z "$E2E_YARN_SCRIPT" ]; then
Expand All @@ -141,7 +145,7 @@ jobs:
$E2E_YARN_SCRIPT \
-n $E2E_RETRY_COUNT
else
yarn "$E2E_YARN_SCRIPT"
yarn "$E2E_YARN_SCRIPT" "$E2E_YARN_SCRIPT_ARGS"
fi
- name: Upload artifact
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 https://github.com/actions/upload-artifact/commit/0b7f8abb1508181956e8e162db84b466c27e18ce
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/callable-e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jobs:
timeout_minutes: ${{ matrix.integ-config.timeout_minutes || 35 }}
retry_count: ${{ matrix.integ-config.retry_count || 3 }}
yarn_script: ${{ matrix.integ-config.yarn_script || '' }}
yarn_script_args: ${{ matrix.integ-config.yarn_script_args || 15 }}
env: ${{ matrix.integ-config.env && toJSON(matrix.integ-config.env) || '{}' }}

# e2e-test-runner-headless:
Expand Down
4 changes: 2 additions & 2 deletions packages/adapter-nextjs/__tests__/createServerRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe('createServerRunner', () => {
});

describe('when nextServerContext is not null', () => {
it('should create auth providers with cookie storage adapter', () => {
it('should create auth providers with cookie storage adapter', async () => {
const operation = jest.fn();
const mockCookieStorageAdapter = {
get: jest.fn(),
Expand All @@ -147,7 +147,7 @@ describe('createServerRunner', () => {
const { runWithAmplifyServerContext } = createServerRunner({
config: mockAmplifyConfig,
});
runWithAmplifyServerContext({
await runWithAmplifyServerContext({
operation,
nextServerContext:
mockNextServerContext as unknown as NextServer.Context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Socket } from 'net';
import { enableFetchMocks } from 'jest-fetch-mock';
import { NextRequest, NextResponse } from 'next/server.js';
import { cookies } from 'next/headers.js';
import { CookieStorage } from 'aws-amplify/adapter-core';

import {
DATE_IN_THE_PAST,
Expand Down Expand Up @@ -46,29 +47,32 @@ describe('createCookieStorageAdapterFromNextServerContext', () => {
describe('cookieStorageAdapter created from NextRequest and NextResponse', () => {
const request = new NextRequest(new URL('https://example.com'));
const response = NextResponse.next();

jest.spyOn(request, 'cookies', 'get').mockImplementation(
() =>
({
get: mockGetFunc,
getAll: mockGetAllFunc,
}) as any,
);

jest.spyOn(response, 'cookies', 'get').mockImplementation(() => ({
set: mockSetFunc,
delete: mockDeleteFunc,
get: jest.fn(),
getAll: jest.fn(),
has: jest.fn(),
}));

const mockContext = {
request,
response,
} as any;
let result: CookieStorage.Adapter;

beforeAll(async () => {
jest.spyOn(request, 'cookies', 'get').mockImplementation(
() =>
({
get: mockGetFunc,
getAll: mockGetAllFunc,
}) as any,
);

const result = createCookieStorageAdapterFromNextServerContext(mockContext);
jest.spyOn(response, 'cookies', 'get').mockImplementation(() => ({
set: mockSetFunc,
delete: mockDeleteFunc,
get: jest.fn(),
getAll: jest.fn(),
has: jest.fn(),
}));

result =
await createCookieStorageAdapterFromNextServerContext(mockContext);
});

it('gets cookie by calling `get` method of the underlying cookie store', () => {
result.get(mockKey);
Expand Down Expand Up @@ -121,26 +125,32 @@ describe('createCookieStorageAdapterFromNextServerContext', () => {
describe('cookieStorageAdapter created from NextRequest and Response', () => {
const request = new NextRequest(new URL('https://example.com'));
const response = new Response();

jest.spyOn(request, 'cookies', 'get').mockImplementation(
() =>
({
get: mockGetFunc,
getAll: mockGetAllFunc,
}) as any,
);
jest.spyOn(response, 'headers', 'get').mockImplementation(
() =>
({
append: mockAppend,
}) as any,
);

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

let result: CookieStorage.Adapter;

beforeAll(async () => {
jest.spyOn(request, 'cookies', 'get').mockImplementation(
() =>
({
get: mockGetFunc,
getAll: mockGetAllFunc,
}) as any,
);
jest.spyOn(response, 'headers', 'get').mockImplementation(
() =>
({
append: mockAppend,
}) as any,
);

result =
await createCookieStorageAdapterFromNextServerContext(mockContext);
});

const mockSerializeOptions = {
domain: 'example.com',
expires: new Date('2023-08-22'),
Expand All @@ -150,8 +160,6 @@ describe('createCookieStorageAdapterFromNextServerContext', () => {
path: '/a-path',
};

const result = createCookieStorageAdapterFromNextServerContext(mockContext);

it('gets cookie by calling `get` method of the underlying cookie store', () => {
result.get(mockKey);
expect(mockGetFunc).toHaveBeenCalledWith(mockKey);
Expand Down Expand Up @@ -233,9 +241,15 @@ describe('createCookieStorageAdapterFromNextServerContext', () => {
});

describe('cookieStorageAdapter created from Next cookies function', () => {
mockNextCookiesFunc.mockReturnValueOnce(mockNextCookiesFuncReturn);
let result: CookieStorage.Adapter;

beforeAll(async () => {
mockNextCookiesFunc.mockReturnValueOnce(mockNextCookiesFuncReturn);

const result = createCookieStorageAdapterFromNextServerContext({ cookies });
result = await createCookieStorageAdapterFromNextServerContext({
cookies,
});
});

it('gets cookie by calling `get` method of the underlying cookie store', () => {
result.get(mockKey);
Expand Down Expand Up @@ -286,7 +300,7 @@ describe('createCookieStorageAdapterFromNextServerContext', () => {
});

describe('cookieStorageAdapter created from IncomingMessage and ServerResponse as the Pages Router context', () => {
it('operates with the underlying cookie store', () => {
it('operates with the underlying cookie store', async () => {
const mockCookies = {
key1: 'value1',
key2: 'value2',
Expand All @@ -302,7 +316,7 @@ describe('createCookieStorageAdapterFromNextServerContext', () => {
},
});

const result = createCookieStorageAdapterFromNextServerContext({
const result = await createCookieStorageAdapterFromNextServerContext({
request: request as any,
response,
});
Expand Down Expand Up @@ -341,7 +355,7 @@ describe('createCookieStorageAdapterFromNextServerContext', () => {
]);
});

it('operates with the underlying cookie store with encoded cookie names', () => {
it('operates with the underlying cookie store with encoded cookie names', async () => {
// these the auth keys generated by Amplify
const encodedCookieName1 = encodeURIComponent('test@email.com.idToken');
const encodedCookieName2 = encodeURIComponent(
Expand All @@ -364,7 +378,7 @@ describe('createCookieStorageAdapterFromNextServerContext', () => {
},
});

const result = createCookieStorageAdapterFromNextServerContext({
const result = await createCookieStorageAdapterFromNextServerContext({
request: request as any,
response,
});
Expand Down Expand Up @@ -413,7 +427,7 @@ describe('createCookieStorageAdapterFromNextServerContext', () => {
]);
});

it('does not add duplicate cookies when the cookies are defined in the response Set-Cookie headers', () => {
it('does not add duplicate cookies when the cookies are defined in the response Set-Cookie headers', async () => {
const mockExistingSetCookieValues = [
'CognitoIdentityServiceProvider.1234.accessToken=1234;Path=/',
'CognitoIdentityServiceProvider.1234.refreshToken=1234;Path=/',
Expand All @@ -433,7 +447,7 @@ describe('createCookieStorageAdapterFromNextServerContext', () => {

getHeaderSpy.mockReturnValue(mockExistingSetCookieValues);

const result = createCookieStorageAdapterFromNextServerContext({
const result = await createCookieStorageAdapterFromNextServerContext({
request: request as any,
response,
});
Expand All @@ -455,6 +469,6 @@ describe('createCookieStorageAdapterFromNextServerContext', () => {
request: undefined,
response: new ServerResponse({} as any),
} as any),
).toThrow();
).rejects.toThrow();
});
});
2 changes: 1 addition & 1 deletion packages/adapter-nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"description": "The adapter for the supporting of using Amplify APIs in Next.js.",
"peerDependencies": {
"aws-amplify": "^6.0.7",
"next": ">=13.5.0 <15.0.0"
"next": ">=13.5.0 <16.0.0"
},
"dependencies": {
"aws-jwt-verify": "^4.0.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { NextServer } from '../types';

export const DATE_IN_THE_PAST = new Date(0);

export const createCookieStorageAdapterFromNextServerContext = (
export const createCookieStorageAdapterFromNextServerContext = async (
context: NextServer.Context,
): CookieStorage.Adapter => {
): Promise<CookieStorage.Adapter> => {
const { request: req, response: res } =
context as Partial<NextServer.GetServerSidePropsContext>;

Expand Down Expand Up @@ -110,10 +110,10 @@ const createCookieStorageAdapterFromNextRequestAndHttpResponse = (
};
};

const createCookieStorageAdapterFromNextCookies = (
const createCookieStorageAdapterFromNextCookies = async (
cookies: NextServer.ServerComponentContext['cookies'],
): CookieStorage.Adapter => {
const cookieStore = cookies();
): Promise<CookieStorage.Adapter> => {
const cookieStore = await cookies();

// When Next cookies() is called in a server component, it returns a readonly
// cookie store. Hence calling set and delete throws an error. However,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const createRunWithAmplifyServerContext = ({
nextServerContext === null
? sharedInMemoryStorage
: createKeyValueStorageFromCookieStorageAdapter(
createCookieStorageAdapterFromNextServerContext(
await createCookieStorageAdapterFromNextServerContext(
nextServerContext,
),
createTokenValidator({
Expand Down

0 comments on commit 341eee2

Please sign in to comment.