From 1a21f532de50a26db8b800541706b34220e7ae41 Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Wed, 20 Nov 2024 13:44:12 -0600 Subject: [PATCH 01/16] poc --- jest.setup.js | 16 ++-- package.json | 3 +- .../api-graphql/__tests__/GraphQLAPI.test.ts | 94 +++++++++++++++++++ .../src/internals/InternalGraphQLAPI.ts | 35 +++++-- .../src/internals/generateClient.ts | 36 +++---- packages/api-graphql/src/internals/types.ts | 1 + packages/api-graphql/src/types/index.ts | 4 + 7 files changed, 155 insertions(+), 34 deletions(-) diff --git a/jest.setup.js b/jest.setup.js index 05fbee97db1..a0d54a2b09f 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,13 +1,13 @@ // Suppress console messages printing during unit tests. // Comment out log level as necessary (e.g. while debugging tests) -global.console = { - ...console, - log: jest.fn(), - debug: jest.fn(), - info: jest.fn(), - warn: jest.fn(), - error: jest.fn(), -}; +// global.console = { +// ...console, +// log: jest.fn(), +// debug: jest.fn(), +// info: jest.fn(), +// warn: jest.fn(), +// error: jest.fn(), +// }; // React Native global global['__DEV__'] = true; diff --git a/package.json b/package.json index b50ecce61d2..0f6c401c4b0 100644 --- a/package.json +++ b/package.json @@ -141,5 +141,6 @@ }, "overrides": { "tar": "6.2.1" - } + }, + "packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447" } diff --git a/packages/api-graphql/__tests__/GraphQLAPI.test.ts b/packages/api-graphql/__tests__/GraphQLAPI.test.ts index 8f2e953d906..b802bf1926b 100644 --- a/packages/api-graphql/__tests__/GraphQLAPI.test.ts +++ b/packages/api-graphql/__tests__/GraphQLAPI.test.ts @@ -1559,6 +1559,99 @@ describe('API test', () => { }); }); + describe.only('custom endpoints', () => { + describe.only('query operations', () => { + test.only('sets authMode to "none" by default', async () => { + // just need to fill this test out to call POC done and start designing + // the behavior for the other auth modes. + Amplify.configure({ + API: { + GraphQL: { + defaultAuthMode: 'apiKey', + apiKey: 'FAKE-KEY', + endpoint: 'https://a-default-appsync-endpoint.local/graphql', + region: 'north-pole-7', + }, + }, + }); + + const threadToGet = { + id: 'some-id', + topic: 'something reasonably interesting', + }; + + const graphqlVariables = { id: 'some-id' }; + + const graphqlResponse = { + data: { + getThread: { + __typename: 'Thread', + ...serverManagedFields, + ...threadToGet, + }, + }, + }; + + const client = { + [__amplify]: Amplify, + graphql, + cancel, + isCancelError, + } as unknown as V6Client; + + const spy = jest + .spyOn((raw.GraphQLAPI as any)._api, 'post') + .mockReturnValue({ + body: { + json: () => graphqlResponse, + }, + }); + + const result: GraphQLResult = await client.graphql({ + query: typedQueries.getThread, + variables: graphqlVariables, + endpoint: 'https://some-custom-endpoint.local/path/to/graphql', + }); + + console.log('spy call', JSON.stringify(spy.mock.calls, null, 2)); + + const thread: GetThreadQuery['getThread'] = result.data?.getThread; + const errors = result.errors; + + expectGet(spy, 'getThread', graphqlVariables); + expect(errors).toBe(undefined); + expect(thread).toEqual(graphqlResponse.data.getThread); + }); + + test('something something CUP auth', async () => {}); + + test('something something IAM auth', async () => {}); + + test('something something apiKey auth', async () => {}); + + test('something something tokenAuth auth', async () => {}); + + test('something something tokenAuth with custom token auth', async () => {}); + }); + + describe('subscription operations', () => { + test('sets authMode to "none" by default', async () => { + // just need to fill this test out to call POC done and start designing + // the behavior for the other auth modes. + }); + + test('something something CUP auth', async () => {}); + + test('something something IAM auth', async () => {}); + + test('something something apiKey auth', async () => {}); + + test('something something tokenAuth auth', async () => {}); + + test('something something tokenAuth with custom token auth', async () => {}); + }); + }); + test('request level custom headers are applied to query string', async () => { Amplify.configure({ API: { @@ -1614,6 +1707,7 @@ describe('API test', () => { const subscribeOptions = spyon_appsync_realtime.mock.calls[0][0]; expect(subscribeOptions).toBe(resolvedUrl); }); + test('graphql method handles INTERNAL_USER_AGENT_OVERRIDE correctly', async () => { Amplify.configure({ API: { diff --git a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts index f7d1a60d556..4abd9d85165 100644 --- a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts +++ b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts @@ -88,7 +88,13 @@ export class InternalGraphQLAPIClass { amplify: | AmplifyClassV6 | ((fn: (amplify: any) => Promise) => Promise), - { query: paramQuery, variables = {}, authMode, authToken }: GraphQLOptions, + { + query: paramQuery, + variables = {}, + authMode: givenAuthMode, + authToken, + endpoint, + }: GraphQLOptions, additionalHeaders?: CustomHeaders, customUserAgentDetails?: CustomUserAgentDetails, ): Observable> | Promise> { @@ -105,6 +111,11 @@ export class InternalGraphQLAPIClass { const headers = additionalHeaders || {}; + // If a custom endpoint is provided, we don't want to inadvertently expose + // any auth tokens, keys, etc. to a third party. However, if a customer + // sets `authMode` explicitly, we will respect it. + const authMode = endpoint && !givenAuthMode ? 'none' : givenAuthMode; + switch (operationType) { case 'query': case 'mutation': { @@ -115,7 +126,7 @@ export class InternalGraphQLAPIClass { if (isAmplifyInstance(amplify)) { responsePromise = this._graphql( amplify, - { query, variables, authMode }, + { query, variables, authMode, endpoint }, headers, abortController, customUserAgentDetails, @@ -127,7 +138,7 @@ export class InternalGraphQLAPIClass { const wrapper = async (amplifyInstance: AmplifyClassV6) => { const result = await this._graphql( amplifyInstance, - { query, variables, authMode }, + { query, variables, authMode, endpoint }, headers, abortController, customUserAgentDetails, @@ -152,7 +163,7 @@ export class InternalGraphQLAPIClass { case 'subscription': return this._graphqlSubscribe( amplify as AmplifyClassV6, - { query, variables, authMode }, + { query, variables, authMode, endpoint }, headers, customUserAgentDetails, authToken, @@ -164,7 +175,12 @@ export class InternalGraphQLAPIClass { private async _graphql( amplify: AmplifyClassV6, - { query, variables, authMode: explicitAuthMode }: GraphQLOptions, + { + query, + variables, + authMode: explicitAuthMode, + endpoint: endpointOverride, + }: GraphQLOptions, additionalHeaders: CustomHeaders = {}, abortController: AbortController, customUserAgentDetails?: CustomUserAgentDetails, @@ -205,7 +221,7 @@ export class InternalGraphQLAPIClass { const requestOptions: RequestOptions = { method: 'POST', url: new AmplifyUrl( - customEndpoint || appSyncGraphqlEndpoint || '', + endpointOverride || customEndpoint || appSyncGraphqlEndpoint || '', ).toString(), queryString: print(query as DocumentNode), }; @@ -282,7 +298,8 @@ export class InternalGraphQLAPIClass { }; } - const endpoint = customEndpoint || appSyncGraphqlEndpoint; + const endpoint = + endpointOverride || customEndpoint || appSyncGraphqlEndpoint; if (!endpoint) { throw createGraphQLResultWithError(new GraphQLApiError(NO_ENDPOINT)); @@ -341,7 +358,7 @@ export class InternalGraphQLAPIClass { private _graphqlSubscribe( amplify: AmplifyClassV6, - { query, variables, authMode: explicitAuthMode }: GraphQLOptions, + { query, variables, authMode: explicitAuthMode, endpoint }: GraphQLOptions, additionalHeaders: CustomHeaders = {}, customUserAgentDetails?: CustomUserAgentDetails, authToken?: string, @@ -369,7 +386,7 @@ export class InternalGraphQLAPIClass { { query: print(query as DocumentNode), variables, - appSyncGraphqlEndpoint: config?.endpoint, + appSyncGraphqlEndpoint: endpoint ?? config?.endpoint, region: config?.region, authenticationType: authMode, apiKey: config?.apiKey, diff --git a/packages/api-graphql/src/internals/generateClient.ts b/packages/api-graphql/src/internals/generateClient.ts index 2831753424b..0eafcf27f3f 100644 --- a/packages/api-graphql/src/internals/generateClient.ts +++ b/packages/api-graphql/src/internals/generateClient.ts @@ -15,6 +15,7 @@ import { __amplify, __authMode, __authToken, + __endpoint, __headers, getInternals, } from '../types'; @@ -40,6 +41,7 @@ export function generateClient = never>( [__amplify]: params.amplify, [__authMode]: params.authMode, [__authToken]: params.authToken, + [__endpoint]: params.endpoint, [__headers]: params.headers, graphql, cancel, @@ -53,22 +55,24 @@ export function generateClient = never>( const apiGraphqlConfig = params.amplify.getConfig().API?.GraphQL; - if (isApiGraphQLConfig(apiGraphqlConfig)) { - addSchemaToClient(client, apiGraphqlConfig, getInternals); - } else { - // This happens when the `Amplify.configure()` call gets evaluated after the `generateClient()` call. - // - // Cause: when the `generateClient()` and the `Amplify.configure()` calls are located in - // different source files, script bundlers may randomly arrange their orders in the production - // bundle. - // - // With the current implementation, the `client.models` instance created by `generateClient()` - // will be rebuilt on every `Amplify.configure()` call that's provided with a valid GraphQL - // provider configuration. - // - // TODO: revisit, and reverify this approach when enabling multiple clients for multi-endpoints - // configuration. - generateModelsPropertyOnAmplifyConfigure(client); + if (!params.endpoint) { + if (isApiGraphQLConfig(apiGraphqlConfig)) { + addSchemaToClient(client, apiGraphqlConfig, getInternals); + } else { + // This happens when the `Amplify.configure()` call gets evaluated after the `generateClient()` call. + // + // Cause: when the `generateClient()` and the `Amplify.configure()` calls are located in + // different source files, script bundlers may randomly arrange their orders in the production + // bundle. + // + // With the current implementation, the `client.models` instance created by `generateClient()` + // will be rebuilt on every `Amplify.configure()` call that's provided with a valid GraphQL + // provider configuration. + // + // TODO: revisit, and reverify this approach when enabling multiple clients for multi-endpoints + // configuration. + generateModelsPropertyOnAmplifyConfigure(client); + } } return client as any; diff --git a/packages/api-graphql/src/internals/types.ts b/packages/api-graphql/src/internals/types.ts index edb0ab2599f..86cda1a2f78 100644 --- a/packages/api-graphql/src/internals/types.ts +++ b/packages/api-graphql/src/internals/types.ts @@ -20,4 +20,5 @@ export interface CommonPublicClientOptions { authMode?: GraphQLAuthMode; authToken?: string; headers?: CustomHeaders; + endpoint?: string; } diff --git a/packages/api-graphql/src/types/index.ts b/packages/api-graphql/src/types/index.ts index 0ecac34369a..a3299139709 100644 --- a/packages/api-graphql/src/types/index.ts +++ b/packages/api-graphql/src/types/index.ts @@ -31,6 +31,7 @@ export { CommonPublicClientOptions } from '../internals/types'; */ export interface GraphQLOptions { query: string | DocumentNode; + endpoint?: string; variables?: Record; authMode?: GraphQLAuthMode; authToken?: string; @@ -214,6 +215,7 @@ export interface GraphQLOptionsV6< TYPED_GQL_STRING extends string = string, > { query: TYPED_GQL_STRING | DocumentNode; + endpoint?: string; variables?: GraphQLVariablesV6; authMode?: GraphQLAuthMode; authToken?: string; @@ -370,6 +372,7 @@ export const __amplify = Symbol('amplify'); export const __authMode = Symbol('authMode'); export const __authToken = Symbol('authToken'); export const __headers = Symbol('headers'); +export const __endpoint = Symbol('endpoint'); export function getInternals(client: BaseClient): ClientInternals { const c = client as any; @@ -378,6 +381,7 @@ export function getInternals(client: BaseClient): ClientInternals { amplify: c[__amplify], authMode: c[__authMode], authToken: c[__authToken], + endpoint: c[__endpoint], headers: c[__headers], } as any; } From 49156c1ba59dbd6fedb9f4e164837a08c396bfc8 Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Wed, 20 Nov 2024 16:15:24 -0600 Subject: [PATCH 02/16] testing pattern for endpoint override --- .../api-graphql/__tests__/GraphQLAPI.test.ts | 7 +- .../api-graphql/__tests__/utils/expects.ts | 72 +++++++++++++++---- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/packages/api-graphql/__tests__/GraphQLAPI.test.ts b/packages/api-graphql/__tests__/GraphQLAPI.test.ts index b802bf1926b..63b8d55f1e3 100644 --- a/packages/api-graphql/__tests__/GraphQLAPI.test.ts +++ b/packages/api-graphql/__tests__/GraphQLAPI.test.ts @@ -1618,7 +1618,12 @@ describe('API test', () => { const thread: GetThreadQuery['getThread'] = result.data?.getThread; const errors = result.errors; - expectGet(spy, 'getThread', graphqlVariables); + expectGet(spy, 'getThread', graphqlVariables, { + endpoint: 'https://some-custom-endpoint.local/path/to/graphql', + headers: { + 'x-amz-user-agent': expect.any(String), + }, + }); expect(errors).toBe(undefined); expect(thread).toEqual(graphqlResponse.data.getThread); }); diff --git a/packages/api-graphql/__tests__/utils/expects.ts b/packages/api-graphql/__tests__/utils/expects.ts index e0085cc4980..53c228d8d4f 100644 --- a/packages/api-graphql/__tests__/utils/expects.ts +++ b/packages/api-graphql/__tests__/utils/expects.ts @@ -2,6 +2,18 @@ import { parse, print, DocumentNode } from 'graphql'; import { CustomHeaders } from '@aws-amplify/data-schema-types'; import { Amplify } from 'aws-amplify'; +type SpecialRequestVariations = { + /** + * A string or jest matcher. + */ + endpoint?: any; + + /** + * Object or jest matcher. + */ + headers?: any; +}; + /** * Performs an `expect()` on a jest spy with some basic nested argument checks * based on the given mutation `opName` and `item`. @@ -14,12 +26,16 @@ export function expectMutation( spy: jest.SpyInstance, opName: string, item: Record, + { + endpoint = 'https://localhost/graphql', + headers = expect.objectContaining({ 'X-Api-Key': 'FAKE-KEY' }), + }: SpecialRequestVariations = {}, ) { expect(spy).toHaveBeenCalledWith({ abortController: expect.any(AbortController), - url: new URL('https://localhost/graphql'), + url: new URL(endpoint), options: expect.objectContaining({ - headers: expect.objectContaining({ 'X-Api-Key': 'FAKE-KEY' }), + headers, body: expect.objectContaining({ query: expect.stringContaining( `${opName}(input: $input, condition: $condition)`, @@ -44,6 +60,10 @@ export function expectGet( spy: jest.SpyInstance, opName: string, item: Record, + { + endpoint = 'https://localhost/graphql', + headers = expect.objectContaining({ 'X-Api-Key': 'FAKE-KEY' }), + }: SpecialRequestVariations = {}, ) { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ @@ -53,9 +73,9 @@ export function expectGet( }), { abortController: expect.any(AbortController), - url: new URL('https://localhost/graphql'), + url: new URL(endpoint), options: expect.objectContaining({ - headers: expect.objectContaining({ 'X-Api-Key': 'FAKE-KEY' }), + headers, body: expect.objectContaining({ query: expect.stringContaining(`${opName}(id: $id)`), variables: expect.objectContaining(item), @@ -77,12 +97,16 @@ export function expectList( spy: jest.SpyInstance, opName: string, item: Record, + { + endpoint = 'https://localhost/graphql', + headers = expect.objectContaining({ 'X-Api-Key': 'FAKE-KEY' }), + }: SpecialRequestVariations = {}, ) { expect(spy).toHaveBeenCalledWith({ abortController: expect.any(AbortController), - url: new URL('https://localhost/graphql'), + url: new URL(endpoint), options: expect.objectContaining({ - headers: expect.objectContaining({ 'X-Api-Key': 'FAKE-KEY' }), + headers, body: expect.objectContaining({ query: expect.stringContaining( `${opName}(filter: $filter, limit: $limit, nextToken: $nextToken)`, @@ -105,12 +129,20 @@ export function expectSub( spy: jest.SpyInstance, opName: string, item: Record, + { + endpoint = 'https://localhost/graphql', + authenticationType = 'apiKey', + apiKey = 'FAKE-KEY', + }: SpecialRequestVariations & { + authenticationType?: string; + apiKey?: string; + } = {}, ) { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - authenticationType: 'apiKey', - apiKey: 'FAKE-KEY', - appSyncGraphqlEndpoint: 'https://localhost/graphql', + authenticationType, + apiKey, + appSyncGraphqlEndpoint: endpoint, // Code-gen'd queries have an owner param; TypeBeast queries don't: query: expect.stringContaining(`${opName}(filter: $filter`), variables: expect.objectContaining(item), @@ -136,13 +168,21 @@ export function expectSubWithHeaders( spy: jest.SpyInstance, opName: string, item: Record, - headers?: CustomHeaders, + { + endpoint = 'https://localhost/graphql', + authenticationType = 'apiKey', + apiKey = 'FAKE-KEY', + headers = {}, + }: SpecialRequestVariations & { + authenticationType?: string; + apiKey?: string; + } = {}, ) { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - authenticationType: 'apiKey', - apiKey: 'FAKE-KEY', - appSyncGraphqlEndpoint: 'https://localhost/graphql', + authenticationType, + apiKey, + appSyncGraphqlEndpoint: endpoint, // Code-gen'd queries have an owner param; TypeBeast queries don't: query: expect.stringContaining(`${opName}(filter: $filter`), variables: expect.objectContaining(item), @@ -168,12 +208,16 @@ export function expectSubWithHeadersFn( spy: jest.SpyInstance, opName: string, item: Record, + { + endpoint = 'https://localhost/graphql', + headers = expect.objectContaining({ 'X-Api-Key': 'FAKE-KEY' }), + }: SpecialRequestVariations = {}, ) { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ authenticationType: 'apiKey', apiKey: 'FAKE-KEY', - appSyncGraphqlEndpoint: 'https://localhost/graphql', + appSyncGraphqlEndpoint: endpoint, // Code-gen'd queries have an owner param; TypeBeast queries don't: query: expect.stringContaining(`${opName}(filter: $filter`), variables: expect.objectContaining(item), From 6e94ddac9f58c069816ec5ba907a6d8711d932b7 Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Fri, 6 Dec 2024 16:49:20 -0600 Subject: [PATCH 03/16] added testing; fixes to custom endpoint and authMode logic --- .../api-graphql/__tests__/GraphQLAPI.test.ts | 4 +- packages/api-graphql/src/internals/v6.ts | 47 +++- .../@aws-amplify/api-rest/internals/index.ts | 1 - packages/api/__tests__/API.test.ts | 219 +++++++++++++++++- 4 files changed, 257 insertions(+), 14 deletions(-) delete mode 100644 packages/api/__mocks__/@aws-amplify/api-rest/internals/index.ts diff --git a/packages/api-graphql/__tests__/GraphQLAPI.test.ts b/packages/api-graphql/__tests__/GraphQLAPI.test.ts index 63b8d55f1e3..6a79ae47fa8 100644 --- a/packages/api-graphql/__tests__/GraphQLAPI.test.ts +++ b/packages/api-graphql/__tests__/GraphQLAPI.test.ts @@ -1560,7 +1560,7 @@ describe('API test', () => { }); describe.only('custom endpoints', () => { - describe.only('query operations', () => { + describe.only('non-subscription operations', () => { test.only('sets authMode to "none" by default', async () => { // just need to fill this test out to call POC done and start designing // the behavior for the other auth modes. @@ -1628,6 +1628,8 @@ describe('API test', () => { expect(thread).toEqual(graphqlResponse.data.getThread); }); + test.only('', async () => {}); + test('something something CUP auth', async () => {}); test('something something IAM auth', async () => {}); diff --git a/packages/api-graphql/src/internals/v6.ts b/packages/api-graphql/src/internals/v6.ts index c5d362908c8..d3720a3ce25 100644 --- a/packages/api-graphql/src/internals/v6.ts +++ b/packages/api-graphql/src/internals/v6.ts @@ -104,7 +104,47 @@ export function graphql< ): GraphQLResponseV6 { // inject client-level auth const internals = getInternals(this as any); - options.authMode = options.authMode || internals.authMode; + + /** + * The custom `endpoint` (or `undefined`) specific to `generateClient()`. + */ + const clientEndpoint: string = (internals as any).endpoint; + + /** + * The `authMode` requested by the individual GraphQL request. + * + * If an `endpoint` is present in the request, we create a "gate" at the request + * level to prevent "more general" `authMode` settings (from the client or config) + * from being exposed unintentionally to an unrelated API. + */ + const requestAuthMode = + options.authMode ?? (options.endpoint ? 'none' : undefined); + + /** + * The `authMode` requested by the generated client. + * + * If an `endpoint` is present on the client, we create a "gate" around at the + * client level to prevent "more general" `authMode` settings (from the config) + * from being exposed unintentionally to an unrelated API. + */ + const clientAuthMode = + internals.authMode ?? (clientEndpoint ? 'none' : undefined); + + /** + * The most specific `authMode` wins. Setting an `endpoint` value without also + * setting an `authMode` value is treated as explicitly setting `authMode` to "none". + * + * E.g., if `.graphql({ endpoint })`, `authMode` is treated as explicitly 'none' at + * the request level, and any `authMode` provided to `generateClient()` or to + * `Amplify.configure()` is ignored. + * + * Reiterating, this serves as a gating mechanism to ensure auth details are not + * unexpected sent to API's they don't belong to. However, if `authMode` has been + * explicitly set alongside `endpoint`, we will assume this was intentional and + * use the normal/configured auth details for the endpoint. + */ + options.authMode = requestAuthMode || clientAuthMode; + options.authToken = options.authToken || internals.authToken; const headers = additionalHeaders || internals.headers; @@ -116,7 +156,10 @@ export function graphql< const result = GraphQLAPI.graphql( // TODO: move V6Client back into this package? internals.amplify as any, - options, + { + ...options, + endpoint: options.endpoint || clientEndpoint, + }, headers, ); diff --git a/packages/api/__mocks__/@aws-amplify/api-rest/internals/index.ts b/packages/api/__mocks__/@aws-amplify/api-rest/internals/index.ts deleted file mode 100644 index e53f52b1f93..00000000000 --- a/packages/api/__mocks__/@aws-amplify/api-rest/internals/index.ts +++ /dev/null @@ -1 +0,0 @@ -export const cancel = jest.fn(() => true); diff --git a/packages/api/__tests__/API.test.ts b/packages/api/__tests__/API.test.ts index ab95bf9dcc9..70bfa85c8f3 100644 --- a/packages/api/__tests__/API.test.ts +++ b/packages/api/__tests__/API.test.ts @@ -1,17 +1,53 @@ import { ResourcesConfig } from 'aws-amplify'; -import { InternalGraphQLAPIClass } from '@aws-amplify/api-graphql/internals'; +import { GraphQLAPI } from '@aws-amplify/api-graphql'; import { generateClient, CONNECTION_STATE_CHANGE } from '@aws-amplify/api'; import { AmplifyClassV6 } from '@aws-amplify/core'; -// import { runWithAmplifyServerContext } from 'aws-amplify/internals/adapter-core'; -const serverManagedFields = { - id: 'some-id', - owner: 'wirejobviously', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), -}; +const API_KEY = 'FAKE-KEY'; -describe('API generateClient', () => { +const DEFAULT_ENDPOINT = 'https://a-default-appsync-endpoint.local/graphql'; +const CUSTOM_ENDPOINT = 'https://a-custom-appsync-endpoint.local/graphql'; + +const _postSpy = jest.spyOn((GraphQLAPI as any)._api, 'post'); +const _subspy = jest.spyOn((GraphQLAPI as any).appSyncRealTime, 'subscribe'); + +/** + * Validates that a "post" occurred (against `_postSpy`) to the given endpoint URL + * specifically with or without the given `apiKey` (defaults to globally configured + * `API_KEY`) depending on the given `withApiKey` argument. + * + * @param options + */ +function expectPost({ + endpoint, + withApiKey, + apiKey = API_KEY, +}: { + endpoint: string; + withApiKey: boolean; + apiKey?: string; +}) { + expect(_postSpy).toHaveBeenCalledWith( + expect.anything(), // amplify instance + expect.objectContaining({ + options: expect.objectContaining({ + headers: withApiKey + ? expect.objectContaining({ + 'X-Api-Key': apiKey, + }) + : expect.not.objectContaining({ + 'X-Api-Key': apiKey, + }), + }), + // `url` is an instance of `URL` + url: expect.objectContaining({ + href: endpoint, + }), + }), + ); +} + +describe.skip('API generateClient', () => { afterEach(() => { jest.clearAllMocks(); }); @@ -23,7 +59,7 @@ describe('API generateClient', () => { }; }); const spy = jest - .spyOn(InternalGraphQLAPIClass.prototype, 'graphql') + .spyOn(GraphQLAPI, 'graphql') .mockResolvedValue('grapqhqlResponse' as any); const client = generateClient(); expect(await client.graphql({ query: 'query' })).toBe('grapqhqlResponse'); @@ -79,3 +115,166 @@ describe('API generateClient', () => { // ); // }); }); + +describe.only('Custom Endpoints', () => { + beforeEach(() => { + jest.spyOn(AmplifyClassV6.prototype, 'getConfig').mockImplementation(() => { + return { + API: { + GraphQL: { + defaultAuthMode: 'apiKey', + apiKey: API_KEY, + endpoint: DEFAULT_ENDPOINT, + region: 'north-pole-7', + }, + }, + }; + }); + _postSpy.mockReturnValue({ + body: { + json() { + return JSON.stringify({ + data: { + someOperation: { + someField: 'some value', + }, + }, + }); + }, + }, + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test('client { endpoint: N, authMode: N } + op { endpoint: N, authMode: N } -> config.authMode', async () => { + const client = generateClient(); + await client.graphql({ query: 'query A { queryA { a b c } }' }); + + expectPost({ + endpoint: DEFAULT_ENDPOINT, + withApiKey: true, + }); + }); + + test('client { endpoint: N, authMode: N } + op { endpoint: N, authMode: Y } -> op.authMode', async () => { + const client = generateClient(); + await client.graphql({ + query: 'query A { queryA { a b c } }', + authMode: 'none', + }); + + expectPost({ + endpoint: DEFAULT_ENDPOINT, + withApiKey: false, // from op.authMode = none + }); + }); + + test('client { endpoint: N, authMode: N } + op { endpoint: Y, authMode: N } -> none (defaulted)', async () => { + const client = generateClient(); + await client.graphql({ + query: 'query A { queryA { a b c } }', + endpoint: CUSTOM_ENDPOINT, + }); + + expectPost({ + endpoint: CUSTOM_ENDPOINT, + withApiKey: false, // from op.endpoint -> op.authMode default = none + }); + }); + + test('client { endpoint: N, authMode: N } + op { endpoint: Y, authMode: Y } -> op.authMode', async () => { + const client = generateClient(); + await client.graphql({ + query: 'query A { queryA { a b c } }', + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + }); + + expectPost({ + endpoint: CUSTOM_ENDPOINT, + withApiKey: true, // from op.authMode = apiKey + }); + }); + + test('client { endpoint: N, authMode: Y } + op { endpoint: M, authMode: N } -> client.authMode', async () => { + const client = generateClient({ + authMode: 'none', + }); + + await client.graphql({ + query: 'query A { queryA { a b c } }', + }); + + expectPost({ + endpoint: DEFAULT_ENDPOINT, + withApiKey: false, // from client.authMode = none + }); + }); + + test('client { endpoint: N, authMode: Y } + op { endpoint: M, authMode: Y } -> op.authMode', async () => { + const client = generateClient({ + authMode: 'apiKey', + }); + + await client.graphql({ + query: 'query A { queryA { a b c } }', + authMode: 'none', + }); + + expectPost({ + endpoint: DEFAULT_ENDPOINT, + withApiKey: false, // from op.authMode = none + }); + }); + + test('client { endpoint: N, authMode: Y } + op { endpoint: Y, authMode: N } -> none (defaulted)', async () => { + const client = generateClient({ + authMode: 'apiKey', + }); + + await client.graphql({ + query: 'query A { queryA { a b c } }', + endpoint: CUSTOM_ENDPOINT, + }); + + expectPost({ + endpoint: CUSTOM_ENDPOINT, + withApiKey: false, // from op.endpoint -> op.authMode default = none + }); + }); + + test('client { endpoint: N, authMode: Y } + op { endpoint: Y, authMode: Y } -> op.authMode', async () => { + const client = generateClient({ + authMode: 'none', + }); + + await client.graphql({ + query: 'query A { queryA { a b c } }', + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + }); + + expectPost({ + endpoint: CUSTOM_ENDPOINT, + withApiKey: true, // from op.authMode = apiKey + }); + }); + + test('client { endpoint: Y, authMode: N } + op { endpoint: N, authMode: N } -> none (defaulted)', async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + }); + + await client.graphql({ + query: 'query A { queryA { a b c } }', + }); + + expectPost({ + endpoint: CUSTOM_ENDPOINT, + withApiKey: false, // from client.endpoint -> default = none + }); + }); +}); From b97e941e4cea7800ce3413c6a74f3ee6349f2d2b Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Tue, 10 Dec 2024 09:25:54 -0600 Subject: [PATCH 04/16] tests for endpoint nt and andauthmode derivation with custom endpoints --- packages/api/__tests__/API.test.ts | 117 +++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/packages/api/__tests__/API.test.ts b/packages/api/__tests__/API.test.ts index 70bfa85c8f3..e006ef85f93 100644 --- a/packages/api/__tests__/API.test.ts +++ b/packages/api/__tests__/API.test.ts @@ -277,4 +277,121 @@ describe.only('Custom Endpoints', () => { withApiKey: false, // from client.endpoint -> default = none }); }); + + test('client { endpoint: Y, authMode: N } + op { endpoint: N, authMode: Y } -> op.authMode', async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + }); + + await client.graphql({ + query: 'query A { queryA { a b c } }', + authMode: 'apiKey', + }); + + expectPost({ + endpoint: CUSTOM_ENDPOINT, + withApiKey: true, // from op.authMode = apiKey + }); + }); + + test('client { endpoint: Y, authMode: N } + op { endpoint: Y, authMode: N } -> none (defaulted)', async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + }); + + await client.graphql({ + query: 'query A { queryA { a b c } }', + endpoint: CUSTOM_ENDPOINT + '-from-op', + }); + + expectPost({ + endpoint: CUSTOM_ENDPOINT + '-from-op', + withApiKey: false, // from op.endpoint -> default = none + }); + }); + + test('client { endpoint: Y, authMode: N } + op { endpoint: Y, authMode: Y } -> op.authMode', async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + }); + + await client.graphql({ + query: 'query A { queryA { a b c } }', + endpoint: CUSTOM_ENDPOINT + '-from-op', + authMode: 'apiKey', + }); + + expectPost({ + endpoint: CUSTOM_ENDPOINT + '-from-op', + withApiKey: true, // from op.authMode = apiKey + }); + }); + + test('client { endpoint: Y, authMode: Y } + op { endpoint: N, authMode: N } -> client.authMode', async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + }); + + await client.graphql({ + query: 'query A { queryA { a b c } }', + }); + + expectPost({ + endpoint: CUSTOM_ENDPOINT, + withApiKey: true, // from client.authMode = apiKey + }); + }); + + test('client { endpoint: Y, authMode: Y } + op { endpoint: N, authMode: Y } -> op.authMode', async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + await client.graphql({ + query: 'query A { queryA { a b c } }', + authMode: 'apiKey', + }); + + expectPost({ + endpoint: CUSTOM_ENDPOINT, + withApiKey: true, // from op.authMode = apiKey + }); + }); + + test('client { endpoint: Y, authMode: Y } + op { endpoint: Y, authMode: N } -> none (defaulted)', async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + }); + + await client.graphql({ + query: 'query A { queryA { a b c } }', + endpoint: CUSTOM_ENDPOINT + '-from-op', + }); + + expectPost({ + endpoint: CUSTOM_ENDPOINT + '-from-op', + withApiKey: false, // from op.endpoint -> default = none + }); + }); + + test('client { endpoint: Y, authMode: Y } + op { endpoint: Y, authMode: Y } -> none (defaulted)', async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + await client.graphql({ + query: 'query A { queryA { a b c } }', + endpoint: CUSTOM_ENDPOINT + '-from-op', + authMode: 'apiKey', + }); + + expectPost({ + endpoint: CUSTOM_ENDPOINT + '-from-op', + withApiKey: true, // from op.authMode = apiKey + }); + }); }); From 1fc9c244190c5605b84dfcb25357b9218b5d6562 Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Tue, 10 Dec 2024 09:41:07 -0600 Subject: [PATCH 05/16] fix test name --- packages/api/__tests__/API.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/api/__tests__/API.test.ts b/packages/api/__tests__/API.test.ts index e006ef85f93..8ee0e75e017 100644 --- a/packages/api/__tests__/API.test.ts +++ b/packages/api/__tests__/API.test.ts @@ -151,6 +151,7 @@ describe.only('Custom Endpoints', () => { test('client { endpoint: N, authMode: N } + op { endpoint: N, authMode: N } -> config.authMode', async () => { const client = generateClient(); + await client.graphql({ query: 'query A { queryA { a b c } }' }); expectPost({ @@ -161,6 +162,7 @@ describe.only('Custom Endpoints', () => { test('client { endpoint: N, authMode: N } + op { endpoint: N, authMode: Y } -> op.authMode', async () => { const client = generateClient(); + await client.graphql({ query: 'query A { queryA { a b c } }', authMode: 'none', @@ -174,6 +176,7 @@ describe.only('Custom Endpoints', () => { test('client { endpoint: N, authMode: N } + op { endpoint: Y, authMode: N } -> none (defaulted)', async () => { const client = generateClient(); + await client.graphql({ query: 'query A { queryA { a b c } }', endpoint: CUSTOM_ENDPOINT, @@ -187,6 +190,7 @@ describe.only('Custom Endpoints', () => { test('client { endpoint: N, authMode: N } + op { endpoint: Y, authMode: Y } -> op.authMode', async () => { const client = generateClient(); + await client.graphql({ query: 'query A { queryA { a b c } }', endpoint: CUSTOM_ENDPOINT, @@ -377,7 +381,7 @@ describe.only('Custom Endpoints', () => { }); }); - test('client { endpoint: Y, authMode: Y } + op { endpoint: Y, authMode: Y } -> none (defaulted)', async () => { + test('client { endpoint: Y, authMode: Y } + op { endpoint: Y, authMode: Y } -> op.authMode', async () => { const client = generateClient({ endpoint: CUSTOM_ENDPOINT, authMode: 'none', From 2eee713f1a93f2b57d9892596e2b69b591ffda7a Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Tue, 10 Dec 2024 09:43:17 -0600 Subject: [PATCH 06/16] remove graphql package tests in favor of api package tests --- .../api-graphql/__tests__/GraphQLAPI.test.ts | 100 ------------------ 1 file changed, 100 deletions(-) diff --git a/packages/api-graphql/__tests__/GraphQLAPI.test.ts b/packages/api-graphql/__tests__/GraphQLAPI.test.ts index 6a79ae47fa8..75a06619e73 100644 --- a/packages/api-graphql/__tests__/GraphQLAPI.test.ts +++ b/packages/api-graphql/__tests__/GraphQLAPI.test.ts @@ -1559,106 +1559,6 @@ describe('API test', () => { }); }); - describe.only('custom endpoints', () => { - describe.only('non-subscription operations', () => { - test.only('sets authMode to "none" by default', async () => { - // just need to fill this test out to call POC done and start designing - // the behavior for the other auth modes. - Amplify.configure({ - API: { - GraphQL: { - defaultAuthMode: 'apiKey', - apiKey: 'FAKE-KEY', - endpoint: 'https://a-default-appsync-endpoint.local/graphql', - region: 'north-pole-7', - }, - }, - }); - - const threadToGet = { - id: 'some-id', - topic: 'something reasonably interesting', - }; - - const graphqlVariables = { id: 'some-id' }; - - const graphqlResponse = { - data: { - getThread: { - __typename: 'Thread', - ...serverManagedFields, - ...threadToGet, - }, - }, - }; - - const client = { - [__amplify]: Amplify, - graphql, - cancel, - isCancelError, - } as unknown as V6Client; - - const spy = jest - .spyOn((raw.GraphQLAPI as any)._api, 'post') - .mockReturnValue({ - body: { - json: () => graphqlResponse, - }, - }); - - const result: GraphQLResult = await client.graphql({ - query: typedQueries.getThread, - variables: graphqlVariables, - endpoint: 'https://some-custom-endpoint.local/path/to/graphql', - }); - - console.log('spy call', JSON.stringify(spy.mock.calls, null, 2)); - - const thread: GetThreadQuery['getThread'] = result.data?.getThread; - const errors = result.errors; - - expectGet(spy, 'getThread', graphqlVariables, { - endpoint: 'https://some-custom-endpoint.local/path/to/graphql', - headers: { - 'x-amz-user-agent': expect.any(String), - }, - }); - expect(errors).toBe(undefined); - expect(thread).toEqual(graphqlResponse.data.getThread); - }); - - test.only('', async () => {}); - - test('something something CUP auth', async () => {}); - - test('something something IAM auth', async () => {}); - - test('something something apiKey auth', async () => {}); - - test('something something tokenAuth auth', async () => {}); - - test('something something tokenAuth with custom token auth', async () => {}); - }); - - describe('subscription operations', () => { - test('sets authMode to "none" by default', async () => { - // just need to fill this test out to call POC done and start designing - // the behavior for the other auth modes. - }); - - test('something something CUP auth', async () => {}); - - test('something something IAM auth', async () => {}); - - test('something something apiKey auth', async () => {}); - - test('something something tokenAuth auth', async () => {}); - - test('something something tokenAuth with custom token auth', async () => {}); - }); - }); - test('request level custom headers are applied to query string', async () => { Amplify.configure({ API: { From 3223579699f03f6b93053758614b6516c38d28ed Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Wed, 11 Dec 2024 12:12:09 -0600 Subject: [PATCH 07/16] sub tests to mirror query tests --- packages/api-graphql/src/internals/InternalGraphQLAPI.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts index 4abd9d85165..1681f24604e 100644 --- a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts +++ b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts @@ -91,7 +91,7 @@ export class InternalGraphQLAPIClass { { query: paramQuery, variables = {}, - authMode: givenAuthMode, + authMode, authToken, endpoint, }: GraphQLOptions, @@ -111,11 +111,6 @@ export class InternalGraphQLAPIClass { const headers = additionalHeaders || {}; - // If a custom endpoint is provided, we don't want to inadvertently expose - // any auth tokens, keys, etc. to a third party. However, if a customer - // sets `authMode` explicitly, we will respect it. - const authMode = endpoint && !givenAuthMode ? 'none' : givenAuthMode; - switch (operationType) { case 'query': case 'mutation': { From d78ed864b343df72a2d6c5732d79c10cf9b49168 Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Wed, 11 Dec 2024 12:27:48 -0600 Subject: [PATCH 08/16] sub tests to mirror query tests for real this time --- packages/api/__tests__/API.test.ts | 463 ++++++++++++++++------------- 1 file changed, 253 insertions(+), 210 deletions(-) diff --git a/packages/api/__tests__/API.test.ts b/packages/api/__tests__/API.test.ts index 8ee0e75e017..2590a6c98a6 100644 --- a/packages/api/__tests__/API.test.ts +++ b/packages/api/__tests__/API.test.ts @@ -2,6 +2,7 @@ import { ResourcesConfig } from 'aws-amplify'; import { GraphQLAPI } from '@aws-amplify/api-graphql'; import { generateClient, CONNECTION_STATE_CHANGE } from '@aws-amplify/api'; import { AmplifyClassV6 } from '@aws-amplify/core'; +import { Observable } from 'rxjs'; const API_KEY = 'FAKE-KEY'; @@ -47,6 +48,42 @@ function expectPost({ ); } +/** + * Validates that a "post" occurred (against `_postSpy`) to the given endpoint URL + * specifically with or without the given `apiKey` (defaults to globally configured + * `API_KEY`) depending on the given `withApiKey` argument. + * + * @param options + */ +function expectSubscription({ + endpoint, + withApiKey, + apiKey = API_KEY, +}: { + endpoint: string; + withApiKey: boolean; + apiKey?: string; +}) { + expect(_subspy).toHaveBeenCalledWith( + expect.objectContaining({ + appSyncGraphqlEndpoint: endpoint, + }), + expect.anything(), + ); + + if (apiKey) { + expect(_subspy).toHaveBeenCalledWith( + expect.objectContaining({ apiKey }), + expect.anything(), + ); + } else { + expect(_subspy).toHaveBeenCalledWith( + expect.not.objectContaining({ apiKey }), + expect.anything(), + ); + } +} + describe.skip('API generateClient', () => { afterEach(() => { jest.clearAllMocks(); @@ -143,259 +180,265 @@ describe.only('Custom Endpoints', () => { }, }, }); + _subspy.mockReturnValue(new Observable()); }); afterEach(() => { jest.resetAllMocks(); }); - test('client { endpoint: N, authMode: N } + op { endpoint: N, authMode: N } -> config.authMode', async () => { - const client = generateClient(); - - await client.graphql({ query: 'query A { queryA { a b c } }' }); - - expectPost({ - endpoint: DEFAULT_ENDPOINT, - withApiKey: true, - }); - }); - - test('client { endpoint: N, authMode: N } + op { endpoint: N, authMode: Y } -> op.authMode', async () => { - const client = generateClient(); - - await client.graphql({ - query: 'query A { queryA { a b c } }', - authMode: 'none', - }); - - expectPost({ - endpoint: DEFAULT_ENDPOINT, - withApiKey: false, // from op.authMode = none - }); - }); - - test('client { endpoint: N, authMode: N } + op { endpoint: Y, authMode: N } -> none (defaulted)', async () => { - const client = generateClient(); - - await client.graphql({ - query: 'query A { queryA { a b c } }', - endpoint: CUSTOM_ENDPOINT, - }); - - expectPost({ - endpoint: CUSTOM_ENDPOINT, - withApiKey: false, // from op.endpoint -> op.authMode default = none - }); - }); - - test('client { endpoint: N, authMode: N } + op { endpoint: Y, authMode: Y } -> op.authMode', async () => { - const client = generateClient(); - - await client.graphql({ - query: 'query A { queryA { a b c } }', - endpoint: CUSTOM_ENDPOINT, - authMode: 'apiKey', - }); - - expectPost({ - endpoint: CUSTOM_ENDPOINT, - withApiKey: true, // from op.authMode = apiKey - }); - }); - - test('client { endpoint: N, authMode: Y } + op { endpoint: M, authMode: N } -> client.authMode', async () => { - const client = generateClient({ - authMode: 'none', - }); + for (const op of ['query', 'subscription']) { + const expectOp = op === 'subscription' ? expectSubscription : expectPost; + const opType = op === 'subscription' ? 'sub' : 'qry'; - await client.graphql({ - query: 'query A { queryA { a b c } }', - }); + test(`client { endpoint: N, authMode: N } + ${opType} { endpoint: N, authMode: N } -> config.authMode`, async () => { + const client = generateClient(); - expectPost({ - endpoint: DEFAULT_ENDPOINT, - withApiKey: false, // from client.authMode = none - }); - }); + await client.graphql({ query: `${op} A { queryA { a b c } }` }); - test('client { endpoint: N, authMode: Y } + op { endpoint: M, authMode: Y } -> op.authMode', async () => { - const client = generateClient({ - authMode: 'apiKey', + expectOp({ + endpoint: DEFAULT_ENDPOINT, + withApiKey: true, + }); }); - await client.graphql({ - query: 'query A { queryA { a b c } }', - authMode: 'none', - }); + test(`client { endpoint: N, authMode: N } + ${opType} { endpoint: N, authMode: Y } -> op.authMode`, async () => { + const client = generateClient(); - expectPost({ - endpoint: DEFAULT_ENDPOINT, - withApiKey: false, // from op.authMode = none - }); - }); + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + authMode: 'none', + }); - test('client { endpoint: N, authMode: Y } + op { endpoint: Y, authMode: N } -> none (defaulted)', async () => { - const client = generateClient({ - authMode: 'apiKey', + expectOp({ + endpoint: DEFAULT_ENDPOINT, + withApiKey: false, // from op.authMode = none + }); }); - await client.graphql({ - query: 'query A { queryA { a b c } }', - endpoint: CUSTOM_ENDPOINT, - }); + test(`client { endpoint: N, authMode: N } + ${opType} { endpoint: Y, authMode: N } -> none (defaulted)`, async () => { + const client = generateClient(); - expectPost({ - endpoint: CUSTOM_ENDPOINT, - withApiKey: false, // from op.endpoint -> op.authMode default = none - }); - }); + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + endpoint: CUSTOM_ENDPOINT, + }); - test('client { endpoint: N, authMode: Y } + op { endpoint: Y, authMode: Y } -> op.authMode', async () => { - const client = generateClient({ - authMode: 'none', + expectOp({ + endpoint: CUSTOM_ENDPOINT, + withApiKey: false, // from op.endpoint -> op.authMode default = none + }); }); - await client.graphql({ - query: 'query A { queryA { a b c } }', - endpoint: CUSTOM_ENDPOINT, - authMode: 'apiKey', - }); + test(`client { endpoint: N, authMode: N } + ${opType} { endpoint: Y, authMode: Y } -> op.authMode`, async () => { + const client = generateClient(); - expectPost({ - endpoint: CUSTOM_ENDPOINT, - withApiKey: true, // from op.authMode = apiKey - }); - }); + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + }); - test('client { endpoint: Y, authMode: N } + op { endpoint: N, authMode: N } -> none (defaulted)', async () => { - const client = generateClient({ - endpoint: CUSTOM_ENDPOINT, + expectOp({ + endpoint: CUSTOM_ENDPOINT, + withApiKey: true, // from op.authMode = apiKey + }); }); - await client.graphql({ - query: 'query A { queryA { a b c } }', - }); + test(`client { endpoint: N, authMode: Y } + ${opType} { endpoint: M, authMode: N } -> client.authMode`, async () => { + const client = generateClient({ + authMode: 'none', + }); - expectPost({ - endpoint: CUSTOM_ENDPOINT, - withApiKey: false, // from client.endpoint -> default = none - }); - }); + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + }); - test('client { endpoint: Y, authMode: N } + op { endpoint: N, authMode: Y } -> op.authMode', async () => { - const client = generateClient({ - endpoint: CUSTOM_ENDPOINT, + expectOp({ + endpoint: DEFAULT_ENDPOINT, + withApiKey: false, // from client.authMode = none + }); }); - await client.graphql({ - query: 'query A { queryA { a b c } }', - authMode: 'apiKey', - }); + test(`client { endpoint: N, authMode: Y } + ${opType} { endpoint: M, authMode: Y } -> op.authMode`, async () => { + const client = generateClient({ + authMode: 'apiKey', + }); - expectPost({ - endpoint: CUSTOM_ENDPOINT, - withApiKey: true, // from op.authMode = apiKey - }); - }); + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + authMode: 'none', + }); - test('client { endpoint: Y, authMode: N } + op { endpoint: Y, authMode: N } -> none (defaulted)', async () => { - const client = generateClient({ - endpoint: CUSTOM_ENDPOINT, + expectOp({ + endpoint: DEFAULT_ENDPOINT, + withApiKey: false, // from op.authMode = none + }); }); - await client.graphql({ - query: 'query A { queryA { a b c } }', - endpoint: CUSTOM_ENDPOINT + '-from-op', - }); + test(`client { endpoint: N, authMode: Y } + ${opType} { endpoint: Y, authMode: N } -> none (defaulted)`, async () => { + const client = generateClient({ + authMode: 'apiKey', + }); - expectPost({ - endpoint: CUSTOM_ENDPOINT + '-from-op', - withApiKey: false, // from op.endpoint -> default = none - }); - }); - - test('client { endpoint: Y, authMode: N } + op { endpoint: Y, authMode: Y } -> op.authMode', async () => { - const client = generateClient({ - endpoint: CUSTOM_ENDPOINT, - }); + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + endpoint: CUSTOM_ENDPOINT, + }); - await client.graphql({ - query: 'query A { queryA { a b c } }', - endpoint: CUSTOM_ENDPOINT + '-from-op', - authMode: 'apiKey', + expectOp({ + endpoint: CUSTOM_ENDPOINT, + withApiKey: false, // from op.endpoint -> op.authMode default = none + }); }); - expectPost({ - endpoint: CUSTOM_ENDPOINT + '-from-op', - withApiKey: true, // from op.authMode = apiKey - }); - }); + test(`client { endpoint: N, authMode: Y } + ${opType} { endpoint: Y, authMode: Y } -> op.authMode`, async () => { + const client = generateClient({ + authMode: 'none', + }); - test('client { endpoint: Y, authMode: Y } + op { endpoint: N, authMode: N } -> client.authMode', async () => { - const client = generateClient({ - endpoint: CUSTOM_ENDPOINT, - authMode: 'apiKey', - }); + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + }); - await client.graphql({ - query: 'query A { queryA { a b c } }', + expectOp({ + endpoint: CUSTOM_ENDPOINT, + withApiKey: true, // from op.authMode = apiKey + }); }); - expectPost({ - endpoint: CUSTOM_ENDPOINT, - withApiKey: true, // from client.authMode = apiKey - }); - }); + test(`client { endpoint: Y, authMode: N } + ${opType} { endpoint: N, authMode: N } -> none (defaulted)`, async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + }); - test('client { endpoint: Y, authMode: Y } + op { endpoint: N, authMode: Y } -> op.authMode', async () => { - const client = generateClient({ - endpoint: CUSTOM_ENDPOINT, - authMode: 'none', - }); + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + }); - await client.graphql({ - query: 'query A { queryA { a b c } }', - authMode: 'apiKey', + expectOp({ + endpoint: CUSTOM_ENDPOINT, + withApiKey: false, // from client.endpoint -> default = none + }); }); - expectPost({ - endpoint: CUSTOM_ENDPOINT, - withApiKey: true, // from op.authMode = apiKey - }); - }); + test(`client { endpoint: Y, authMode: N } + ${opType} { endpoint: N, authMode: Y } -> op.authMode`, async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + }); - test('client { endpoint: Y, authMode: Y } + op { endpoint: Y, authMode: N } -> none (defaulted)', async () => { - const client = generateClient({ - endpoint: CUSTOM_ENDPOINT, - authMode: 'apiKey', - }); + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + authMode: 'apiKey', + }); + + expectOp({ + endpoint: CUSTOM_ENDPOINT, + withApiKey: true, // from op.authMode = apiKey + }); + }); + + test(`client { endpoint: Y, authMode: N } + ${opType} { endpoint: Y, authMode: N } -> none (defaulted)`, async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + endpoint: CUSTOM_ENDPOINT + '-from-op', + }); + + expectOp({ + endpoint: CUSTOM_ENDPOINT + '-from-op', + withApiKey: false, // from op.endpoint -> default = none + }); + }); + + test(`client { endpoint: Y, authMode: N } + ${opType} { endpoint: Y, authMode: Y } -> op.authMode`, async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + endpoint: CUSTOM_ENDPOINT + '-from-op', + authMode: 'apiKey', + }); + + expectOp({ + endpoint: CUSTOM_ENDPOINT + '-from-op', + withApiKey: true, // from op.authMode = apiKey + }); + }); + + test(`client { endpoint: Y, authMode: Y } + ${opType} { endpoint: N, authMode: N } -> client.authMode`, async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + }); + + expectOp({ + endpoint: CUSTOM_ENDPOINT, + withApiKey: true, // from client.authMode = apiKey + }); + }); + + test(`client { endpoint: Y, authMode: Y } + ${opType} { endpoint: N, authMode: Y } -> op.authMode`, async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + authMode: 'apiKey', + }); + + expectOp({ + endpoint: CUSTOM_ENDPOINT, + withApiKey: true, // from op.authMode = apiKey + }); + }); + + test(`client { endpoint: Y, authMode: Y } + ${opType} { endpoint: Y, authMode: N } -> none (defaulted)`, async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + endpoint: CUSTOM_ENDPOINT + '-from-op', + }); + + expectOp({ + endpoint: CUSTOM_ENDPOINT + '-from-op', + withApiKey: false, // from op.endpoint -> default = none + }); + }); - await client.graphql({ - query: 'query A { queryA { a b c } }', - endpoint: CUSTOM_ENDPOINT + '-from-op', - }); - - expectPost({ - endpoint: CUSTOM_ENDPOINT + '-from-op', - withApiKey: false, // from op.endpoint -> default = none - }); - }); - - test('client { endpoint: Y, authMode: Y } + op { endpoint: Y, authMode: Y } -> op.authMode', async () => { - const client = generateClient({ - endpoint: CUSTOM_ENDPOINT, - authMode: 'none', - }); - - await client.graphql({ - query: 'query A { queryA { a b c } }', - endpoint: CUSTOM_ENDPOINT + '-from-op', - authMode: 'apiKey', - }); - - expectPost({ - endpoint: CUSTOM_ENDPOINT + '-from-op', - withApiKey: true, // from op.authMode = apiKey - }); - }); + test(`client { endpoint: Y, authMode: Y } + ${opType} { endpoint: Y, authMode: Y } -> op.authMode`, async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + endpoint: CUSTOM_ENDPOINT + '-from-op', + authMode: 'apiKey', + }); + + expectOp({ + endpoint: CUSTOM_ENDPOINT + '-from-op', + withApiKey: true, // from op.authMode = apiKey + }); + }); + } }); From bef8b51557e0753f65a700dc6ff5ed1adeadab04 Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Wed, 11 Dec 2024 12:55:19 -0600 Subject: [PATCH 09/16] remove endpoint option from .graphql method --- packages/api-graphql/src/internals/v6.ts | 14 +-- packages/api-graphql/src/types/index.ts | 1 - packages/api/__tests__/API.test.ts | 146 ++--------------------- 3 files changed, 10 insertions(+), 151 deletions(-) diff --git a/packages/api-graphql/src/internals/v6.ts b/packages/api-graphql/src/internals/v6.ts index d3720a3ce25..ea14d18b8cf 100644 --- a/packages/api-graphql/src/internals/v6.ts +++ b/packages/api-graphql/src/internals/v6.ts @@ -110,16 +110,6 @@ export function graphql< */ const clientEndpoint: string = (internals as any).endpoint; - /** - * The `authMode` requested by the individual GraphQL request. - * - * If an `endpoint` is present in the request, we create a "gate" at the request - * level to prevent "more general" `authMode` settings (from the client or config) - * from being exposed unintentionally to an unrelated API. - */ - const requestAuthMode = - options.authMode ?? (options.endpoint ? 'none' : undefined); - /** * The `authMode` requested by the generated client. * @@ -143,7 +133,7 @@ export function graphql< * explicitly set alongside `endpoint`, we will assume this was intentional and * use the normal/configured auth details for the endpoint. */ - options.authMode = requestAuthMode || clientAuthMode; + options.authMode = options.authMode || clientAuthMode; options.authToken = options.authToken || internals.authToken; const headers = additionalHeaders || internals.headers; @@ -158,7 +148,7 @@ export function graphql< internals.amplify as any, { ...options, - endpoint: options.endpoint || clientEndpoint, + endpoint: clientEndpoint, }, headers, ); diff --git a/packages/api-graphql/src/types/index.ts b/packages/api-graphql/src/types/index.ts index a3299139709..6b4b676d0c1 100644 --- a/packages/api-graphql/src/types/index.ts +++ b/packages/api-graphql/src/types/index.ts @@ -215,7 +215,6 @@ export interface GraphQLOptionsV6< TYPED_GQL_STRING extends string = string, > { query: TYPED_GQL_STRING | DocumentNode; - endpoint?: string; variables?: GraphQLVariablesV6; authMode?: GraphQLAuthMode; authToken?: string; diff --git a/packages/api/__tests__/API.test.ts b/packages/api/__tests__/API.test.ts index 2590a6c98a6..230be242768 100644 --- a/packages/api/__tests__/API.test.ts +++ b/packages/api/__tests__/API.test.ts @@ -191,7 +191,7 @@ describe.only('Custom Endpoints', () => { const expectOp = op === 'subscription' ? expectSubscription : expectPost; const opType = op === 'subscription' ? 'sub' : 'qry'; - test(`client { endpoint: N, authMode: N } + ${opType} { endpoint: N, authMode: N } -> config.authMode`, async () => { + test(`client { endpoint: N, authMode: N } + ${opType} { authMode: N } -> config.authMode`, async () => { const client = generateClient(); await client.graphql({ query: `${op} A { queryA { a b c } }` }); @@ -202,7 +202,7 @@ describe.only('Custom Endpoints', () => { }); }); - test(`client { endpoint: N, authMode: N } + ${opType} { endpoint: N, authMode: Y } -> op.authMode`, async () => { + test(`client { endpoint: N, authMode: N } + ${opType} { authMode: Y } -> op.authMode`, async () => { const client = generateClient(); await client.graphql({ @@ -216,36 +216,7 @@ describe.only('Custom Endpoints', () => { }); }); - test(`client { endpoint: N, authMode: N } + ${opType} { endpoint: Y, authMode: N } -> none (defaulted)`, async () => { - const client = generateClient(); - - await client.graphql({ - query: `${op} A { queryA { a b c } }`, - endpoint: CUSTOM_ENDPOINT, - }); - - expectOp({ - endpoint: CUSTOM_ENDPOINT, - withApiKey: false, // from op.endpoint -> op.authMode default = none - }); - }); - - test(`client { endpoint: N, authMode: N } + ${opType} { endpoint: Y, authMode: Y } -> op.authMode`, async () => { - const client = generateClient(); - - await client.graphql({ - query: `${op} A { queryA { a b c } }`, - endpoint: CUSTOM_ENDPOINT, - authMode: 'apiKey', - }); - - expectOp({ - endpoint: CUSTOM_ENDPOINT, - withApiKey: true, // from op.authMode = apiKey - }); - }); - - test(`client { endpoint: N, authMode: Y } + ${opType} { endpoint: M, authMode: N } -> client.authMode`, async () => { + test(`client { endpoint: N, authMode: Y } + ${opType} { authMode: N } -> client.authMode`, async () => { const client = generateClient({ authMode: 'none', }); @@ -260,7 +231,7 @@ describe.only('Custom Endpoints', () => { }); }); - test(`client { endpoint: N, authMode: Y } + ${opType} { endpoint: M, authMode: Y } -> op.authMode`, async () => { + test(`client { endpoint: N, authMode: Y } + ${opType} { authMode: Y } -> op.authMode`, async () => { const client = generateClient({ authMode: 'apiKey', }); @@ -276,40 +247,7 @@ describe.only('Custom Endpoints', () => { }); }); - test(`client { endpoint: N, authMode: Y } + ${opType} { endpoint: Y, authMode: N } -> none (defaulted)`, async () => { - const client = generateClient({ - authMode: 'apiKey', - }); - - await client.graphql({ - query: `${op} A { queryA { a b c } }`, - endpoint: CUSTOM_ENDPOINT, - }); - - expectOp({ - endpoint: CUSTOM_ENDPOINT, - withApiKey: false, // from op.endpoint -> op.authMode default = none - }); - }); - - test(`client { endpoint: N, authMode: Y } + ${opType} { endpoint: Y, authMode: Y } -> op.authMode`, async () => { - const client = generateClient({ - authMode: 'none', - }); - - await client.graphql({ - query: `${op} A { queryA { a b c } }`, - endpoint: CUSTOM_ENDPOINT, - authMode: 'apiKey', - }); - - expectOp({ - endpoint: CUSTOM_ENDPOINT, - withApiKey: true, // from op.authMode = apiKey - }); - }); - - test(`client { endpoint: Y, authMode: N } + ${opType} { endpoint: N, authMode: N } -> none (defaulted)`, async () => { + test(`client { endpoint: Y, authMode: N } + ${opType} { authMode: N } -> none (defaulted)`, async () => { const client = generateClient({ endpoint: CUSTOM_ENDPOINT, }); @@ -324,7 +262,7 @@ describe.only('Custom Endpoints', () => { }); }); - test(`client { endpoint: Y, authMode: N } + ${opType} { endpoint: N, authMode: Y } -> op.authMode`, async () => { + test(`client { endpoint: Y, authMode: N } + ${opType} { authMode: Y } -> op.authMode`, async () => { const client = generateClient({ endpoint: CUSTOM_ENDPOINT, }); @@ -340,40 +278,7 @@ describe.only('Custom Endpoints', () => { }); }); - test(`client { endpoint: Y, authMode: N } + ${opType} { endpoint: Y, authMode: N } -> none (defaulted)`, async () => { - const client = generateClient({ - endpoint: CUSTOM_ENDPOINT, - }); - - await client.graphql({ - query: `${op} A { queryA { a b c } }`, - endpoint: CUSTOM_ENDPOINT + '-from-op', - }); - - expectOp({ - endpoint: CUSTOM_ENDPOINT + '-from-op', - withApiKey: false, // from op.endpoint -> default = none - }); - }); - - test(`client { endpoint: Y, authMode: N } + ${opType} { endpoint: Y, authMode: Y } -> op.authMode`, async () => { - const client = generateClient({ - endpoint: CUSTOM_ENDPOINT, - }); - - await client.graphql({ - query: `${op} A { queryA { a b c } }`, - endpoint: CUSTOM_ENDPOINT + '-from-op', - authMode: 'apiKey', - }); - - expectOp({ - endpoint: CUSTOM_ENDPOINT + '-from-op', - withApiKey: true, // from op.authMode = apiKey - }); - }); - - test(`client { endpoint: Y, authMode: Y } + ${opType} { endpoint: N, authMode: N } -> client.authMode`, async () => { + test(`client { endpoint: Y, authMode: Y } + ${opType} { authMode: N } -> client.authMode`, async () => { const client = generateClient({ endpoint: CUSTOM_ENDPOINT, authMode: 'apiKey', @@ -389,7 +294,7 @@ describe.only('Custom Endpoints', () => { }); }); - test(`client { endpoint: Y, authMode: Y } + ${opType} { endpoint: N, authMode: Y } -> op.authMode`, async () => { + test(`client { endpoint: Y, authMode: Y } + ${opType} { authMode: Y } -> op.authMode`, async () => { const client = generateClient({ endpoint: CUSTOM_ENDPOINT, authMode: 'none', @@ -405,40 +310,5 @@ describe.only('Custom Endpoints', () => { withApiKey: true, // from op.authMode = apiKey }); }); - - test(`client { endpoint: Y, authMode: Y } + ${opType} { endpoint: Y, authMode: N } -> none (defaulted)`, async () => { - const client = generateClient({ - endpoint: CUSTOM_ENDPOINT, - authMode: 'apiKey', - }); - - await client.graphql({ - query: `${op} A { queryA { a b c } }`, - endpoint: CUSTOM_ENDPOINT + '-from-op', - }); - - expectOp({ - endpoint: CUSTOM_ENDPOINT + '-from-op', - withApiKey: false, // from op.endpoint -> default = none - }); - }); - - test(`client { endpoint: Y, authMode: Y } + ${opType} { endpoint: Y, authMode: Y } -> op.authMode`, async () => { - const client = generateClient({ - endpoint: CUSTOM_ENDPOINT, - authMode: 'none', - }); - - await client.graphql({ - query: `${op} A { queryA { a b c } }`, - endpoint: CUSTOM_ENDPOINT + '-from-op', - authMode: 'apiKey', - }); - - expectOp({ - endpoint: CUSTOM_ENDPOINT + '-from-op', - withApiKey: true, // from op.authMode = apiKey - }); - }); } }); From a9d5d6e1c9f81783df426617573d1184dcbfc7b0 Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Thu, 12 Dec 2024 14:47:56 -0600 Subject: [PATCH 10/16] checkpoint: basic generateClient types, auth config --- .../src/internals/generateClient.ts | 4 +- packages/api-graphql/src/internals/types.ts | 26 +++++++--- packages/api/__tests__/API.test.ts | 52 ++++++++++++++++--- 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/packages/api-graphql/src/internals/generateClient.ts b/packages/api-graphql/src/internals/generateClient.ts index 0eafcf27f3f..67763529c28 100644 --- a/packages/api-graphql/src/internals/generateClient.ts +++ b/packages/api-graphql/src/internals/generateClient.ts @@ -41,7 +41,7 @@ export function generateClient = never>( [__amplify]: params.amplify, [__authMode]: params.authMode, [__authToken]: params.authToken, - [__endpoint]: params.endpoint, + [__endpoint]: 'endpoint' in params ? params.endpoint : undefined, [__headers]: params.headers, graphql, cancel, @@ -55,7 +55,7 @@ export function generateClient = never>( const apiGraphqlConfig = params.amplify.getConfig().API?.GraphQL; - if (!params.endpoint) { + if (!client[__endpoint]) { if (isApiGraphQLConfig(apiGraphqlConfig)) { addSchemaToClient(client, apiGraphqlConfig, getInternals); } else { diff --git a/packages/api-graphql/src/internals/types.ts b/packages/api-graphql/src/internals/types.ts index 86cda1a2f78..bbbe8110906 100644 --- a/packages/api-graphql/src/internals/types.ts +++ b/packages/api-graphql/src/internals/types.ts @@ -16,9 +16,23 @@ export type ClientGenerationParams = { /** * Common options that can be used on public `generateClient()` interfaces. */ -export interface CommonPublicClientOptions { - authMode?: GraphQLAuthMode; - authToken?: string; - headers?: CustomHeaders; - endpoint?: string; -} +export type CommonPublicClientOptions = + | { + endpoint?: never; + authMode?: GraphQLAuthMode; + authToken?: string; + headers?: CustomHeaders; + } + | { + endpoint: string; + authMode: 'apiKey'; + apiKey: string; + authToken?: string; + headers?: CustomHeaders; + } + | { + endpoint: string; + authMode: Exclude; + authToken?: string; + headers?: CustomHeaders; + }; diff --git a/packages/api/__tests__/API.test.ts b/packages/api/__tests__/API.test.ts index 230be242768..212ef90fdd5 100644 --- a/packages/api/__tests__/API.test.ts +++ b/packages/api/__tests__/API.test.ts @@ -1,14 +1,22 @@ -import { ResourcesConfig } from 'aws-amplify'; +import { Amplify, ResourcesConfig } from 'aws-amplify'; import { GraphQLAPI } from '@aws-amplify/api-graphql'; import { generateClient, CONNECTION_STATE_CHANGE } from '@aws-amplify/api'; import { AmplifyClassV6 } from '@aws-amplify/core'; import { Observable } from 'rxjs'; +import { decodeJWT } from '@aws-amplify/core'; const API_KEY = 'FAKE-KEY'; +const CUSTOM_API_KEY = 'CUSTOM-API-KEY'; const DEFAULT_ENDPOINT = 'https://a-default-appsync-endpoint.local/graphql'; const CUSTOM_ENDPOINT = 'https://a-custom-appsync-endpoint.local/graphql'; +/** + * Valid JWT string, borrowed from Auth tests + */ +const JWT_STRING = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzB9.YzDpgJsrB3z-ZU1XxMcXSQsMbgCzwH_e-_76rnfehh0'; + const _postSpy = jest.spyOn((GraphQLAPI as any)._api, 'post'); const _subspy = jest.spyOn((GraphQLAPI as any).appSyncRealTime, 'subscribe'); @@ -155,8 +163,8 @@ describe.skip('API generateClient', () => { describe.only('Custom Endpoints', () => { beforeEach(() => { - jest.spyOn(AmplifyClassV6.prototype, 'getConfig').mockImplementation(() => { - return { + Amplify.configure( + { API: { GraphQL: { defaultAuthMode: 'apiKey', @@ -165,8 +173,36 @@ describe.only('Custom Endpoints', () => { region: 'north-pole-7', }, }, - }; - }); + Auth: { + Cognito: { + userPoolId: 'north-pole-7:santas-little-helpers', + identityPoolId: 'north-pole-7:santas-average-sized-helpers', + userPoolClientId: 'the-mrs-claus-oversight-committee', + }, + }, + }, + { + Auth: { + credentialsProvider: { + getCredentialsAndIdentityId: async arg => ({ + credentials: { + accessKeyId: 'accessKeyIdValue', + secretAccessKey: 'secretAccessKeyValue', + sessionToken: 'sessionTokenValue', + expiration: new Date(123), + }, + identityId: 'mrs-clause-naturally', + }), + clearCredentialsAndIdentityId: async () => {}, + }, + tokenProvider: { + getTokens: async () => ({ + accessToken: decodeJWT(JWT_STRING), + }), + }, + }, + }, + ); _postSpy.mockReturnValue({ body: { json() { @@ -247,9 +283,10 @@ describe.only('Custom Endpoints', () => { }); }); - test(`client { endpoint: Y, authMode: N } + ${opType} { authMode: N } -> none (defaulted)`, async () => { + test.only(`client { endpoint: Y, authMode: N } + ${opType} { authMode: N } -> none (defaulted)`, async () => { const client = generateClient({ endpoint: CUSTOM_ENDPOINT, + authMode: 'lambda', }); await client.graphql({ @@ -265,6 +302,7 @@ describe.only('Custom Endpoints', () => { test(`client { endpoint: Y, authMode: N } + ${opType} { authMode: Y } -> op.authMode`, async () => { const client = generateClient({ endpoint: CUSTOM_ENDPOINT, + authMode: 'lambda', }); await client.graphql({ @@ -282,6 +320,7 @@ describe.only('Custom Endpoints', () => { const client = generateClient({ endpoint: CUSTOM_ENDPOINT, authMode: 'apiKey', + apiKey: CUSTOM_API_KEY, }); await client.graphql({ @@ -291,6 +330,7 @@ describe.only('Custom Endpoints', () => { expectOp({ endpoint: CUSTOM_ENDPOINT, withApiKey: true, // from client.authMode = apiKey + apiKey: CUSTOM_API_KEY, }); }); From d836e35c609dfa9504ef1a286105be8b3ca2f498 Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Fri, 13 Dec 2024 11:19:01 -0600 Subject: [PATCH 11/16] checkpoint --- .../src/internals/InternalGraphQLAPI.ts | 26 ++- .../src/internals/generateClient.ts | 2 + packages/api-graphql/src/internals/v6.ts | 6 +- packages/api-graphql/src/types/index.ts | 4 + packages/api/__tests__/API.test.ts | 213 +++++++++++++----- 5 files changed, 190 insertions(+), 61 deletions(-) diff --git a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts index 1681f24604e..c7ba064b013 100644 --- a/packages/api-graphql/src/internals/InternalGraphQLAPI.ts +++ b/packages/api-graphql/src/internals/InternalGraphQLAPI.ts @@ -94,6 +94,7 @@ export class InternalGraphQLAPIClass { authMode, authToken, endpoint, + apiKey, }: GraphQLOptions, additionalHeaders?: CustomHeaders, customUserAgentDetails?: CustomUserAgentDetails, @@ -121,7 +122,7 @@ export class InternalGraphQLAPIClass { if (isAmplifyInstance(amplify)) { responsePromise = this._graphql( amplify, - { query, variables, authMode, endpoint }, + { query, variables, authMode, apiKey, endpoint }, headers, abortController, customUserAgentDetails, @@ -133,7 +134,7 @@ export class InternalGraphQLAPIClass { const wrapper = async (amplifyInstance: AmplifyClassV6) => { const result = await this._graphql( amplifyInstance, - { query, variables, authMode, endpoint }, + { query, variables, authMode, apiKey, endpoint }, headers, abortController, customUserAgentDetails, @@ -158,7 +159,7 @@ export class InternalGraphQLAPIClass { case 'subscription': return this._graphqlSubscribe( amplify as AmplifyClassV6, - { query, variables, authMode, endpoint }, + { query, variables, authMode, apiKey, endpoint }, headers, customUserAgentDetails, authToken, @@ -173,8 +174,9 @@ export class InternalGraphQLAPIClass { { query, variables, - authMode: explicitAuthMode, + authMode: authModeOverride, endpoint: endpointOverride, + apiKey: apiKeyOverride, }: GraphQLOptions, additionalHeaders: CustomHeaders = {}, abortController: AbortController, @@ -190,7 +192,7 @@ export class InternalGraphQLAPIClass { defaultAuthMode, } = resolveConfig(amplify); - const initialAuthMode = explicitAuthMode || defaultAuthMode || 'iam'; + const initialAuthMode = authModeOverride || defaultAuthMode || 'iam'; // identityPool is an alias for iam. TODO: remove 'iam' in v7 const authMode = initialAuthMode === 'identityPool' ? 'iam' : initialAuthMode; @@ -237,7 +239,7 @@ export class InternalGraphQLAPIClass { const authHeaders = await headerBasedAuth( amplify, authMode, - apiKey, + apiKeyOverride ?? apiKey, additionalCustomHeaders, ); @@ -353,7 +355,13 @@ export class InternalGraphQLAPIClass { private _graphqlSubscribe( amplify: AmplifyClassV6, - { query, variables, authMode: explicitAuthMode, endpoint }: GraphQLOptions, + { + query, + variables, + authMode: authModeOverride, + apiKey: apiKeyOverride, + endpoint, + }: GraphQLOptions, additionalHeaders: CustomHeaders = {}, customUserAgentDetails?: CustomUserAgentDetails, authToken?: string, @@ -361,7 +369,7 @@ export class InternalGraphQLAPIClass { const config = resolveConfig(amplify); const initialAuthMode = - explicitAuthMode || config?.defaultAuthMode || 'iam'; + authModeOverride || config?.defaultAuthMode || 'iam'; // identityPool is an alias for iam. TODO: remove 'iam' in v7 const authMode = initialAuthMode === 'identityPool' ? 'iam' : initialAuthMode; @@ -384,7 +392,7 @@ export class InternalGraphQLAPIClass { appSyncGraphqlEndpoint: endpoint ?? config?.endpoint, region: config?.region, authenticationType: authMode, - apiKey: config?.apiKey, + apiKey: apiKeyOverride ?? config?.apiKey, additionalHeaders, authToken, libraryConfigHeaders, diff --git a/packages/api-graphql/src/internals/generateClient.ts b/packages/api-graphql/src/internals/generateClient.ts index 67763529c28..afeec2feb72 100644 --- a/packages/api-graphql/src/internals/generateClient.ts +++ b/packages/api-graphql/src/internals/generateClient.ts @@ -13,6 +13,7 @@ import { import { V6Client, __amplify, + __apiKey, __authMode, __authToken, __endpoint, @@ -41,6 +42,7 @@ export function generateClient = never>( [__amplify]: params.amplify, [__authMode]: params.authMode, [__authToken]: params.authToken, + [__apiKey]: 'apiKey' in params ? params.apiKey : undefined, [__endpoint]: 'endpoint' in params ? params.endpoint : undefined, [__headers]: params.headers, graphql, diff --git a/packages/api-graphql/src/internals/v6.ts b/packages/api-graphql/src/internals/v6.ts index ea14d18b8cf..55514682723 100644 --- a/packages/api-graphql/src/internals/v6.ts +++ b/packages/api-graphql/src/internals/v6.ts @@ -106,9 +106,13 @@ export function graphql< const internals = getInternals(this as any); /** - * The custom `endpoint` (or `undefined`) specific to `generateClient()`. + * The custom `endpoint` + `apiKey` (or `undefined`) specific to `generateClient()`. + * + * Q: Can we safely add these to the internals type and remove the cast? Or, does this + * force updates to the data-schema package? */ const clientEndpoint: string = (internals as any).endpoint; + options.apiKey = options.apiKey ?? (internals as any).apiKey; /** * The `authMode` requested by the generated client. diff --git a/packages/api-graphql/src/types/index.ts b/packages/api-graphql/src/types/index.ts index 6b4b676d0c1..46a17b8fe61 100644 --- a/packages/api-graphql/src/types/index.ts +++ b/packages/api-graphql/src/types/index.ts @@ -35,6 +35,7 @@ export interface GraphQLOptions { variables?: Record; authMode?: GraphQLAuthMode; authToken?: string; + apiKey?: string; /** * @deprecated This property should not be used */ @@ -217,6 +218,7 @@ export interface GraphQLOptionsV6< query: TYPED_GQL_STRING | DocumentNode; variables?: GraphQLVariablesV6; authMode?: GraphQLAuthMode; + apiKey?: string; authToken?: string; /** * @deprecated This property should not be used @@ -370,6 +372,7 @@ export type GeneratedSubscription = string & { export const __amplify = Symbol('amplify'); export const __authMode = Symbol('authMode'); export const __authToken = Symbol('authToken'); +export const __apiKey = Symbol('apiKey'); export const __headers = Symbol('headers'); export const __endpoint = Symbol('endpoint'); @@ -378,6 +381,7 @@ export function getInternals(client: BaseClient): ClientInternals { return { amplify: c[__amplify], + apiKey: c[__apiKey], authMode: c[__authMode], authToken: c[__authToken], endpoint: c[__endpoint], diff --git a/packages/api/__tests__/API.test.ts b/packages/api/__tests__/API.test.ts index 212ef90fdd5..3695abc50e8 100644 --- a/packages/api/__tests__/API.test.ts +++ b/packages/api/__tests__/API.test.ts @@ -5,7 +5,17 @@ import { AmplifyClassV6 } from '@aws-amplify/core'; import { Observable } from 'rxjs'; import { decodeJWT } from '@aws-amplify/core'; -const API_KEY = 'FAKE-KEY'; +type AuthMode = + | 'apiKey' + | 'oidc' + | 'userPool' + | 'iam' + | 'identityPool' + | 'lambda' + | 'none'; + +const DEFAULT_AUTH_MODE = 'apiKey'; +const DEFAULT_API_KEY = 'FAKE-KEY'; const CUSTOM_API_KEY = 'CUSTOM-API-KEY'; const DEFAULT_ENDPOINT = 'https://a-default-appsync-endpoint.local/graphql'; @@ -14,82 +24,160 @@ const CUSTOM_ENDPOINT = 'https://a-custom-appsync-endpoint.local/graphql'; /** * Valid JWT string, borrowed from Auth tests */ -const JWT_STRING = +const DEFAULT_AUTH_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzB9.YzDpgJsrB3z-ZU1XxMcXSQsMbgCzwH_e-_76rnfehh0'; const _postSpy = jest.spyOn((GraphQLAPI as any)._api, 'post'); const _subspy = jest.spyOn((GraphQLAPI as any).appSyncRealTime, 'subscribe'); /** - * Validates that a "post" occurred (against `_postSpy`) to the given endpoint URL - * specifically with or without the given `apiKey` (defaults to globally configured - * `API_KEY`) depending on the given `withApiKey` argument. + * Validates that a specific "post" occurred (against `_postSpy`). * * @param options */ function expectPost({ endpoint, - withApiKey, - apiKey = API_KEY, + authMode, + apiKey, + authToken, }: { endpoint: string; - withApiKey: boolean; - apiKey?: string; + authMode: AuthMode; + apiKey: string | undefined; + authToken: string | undefined; }) { + if (authMode === 'apiKey') expect(apiKey).not.toBeFalsy(); // sanity check + expect(_postSpy).toHaveBeenCalledWith( + expect.anything(), // amplify instance + expect.objectContaining({ + // `url` is an instance of `URL` + url: expect.objectContaining({ + href: endpoint, + }), + }), + ); expect(_postSpy).toHaveBeenCalledWith( expect.anything(), // amplify instance expect.objectContaining({ options: expect.objectContaining({ - headers: withApiKey + headers: + authMode === 'apiKey' + ? expect.objectContaining({ + 'X-Api-Key': apiKey, + }) + : expect.not.objectContaining({ + 'X-Api-Key': expect.anything(), + }), + }), + }), + ); + expect(_postSpy).toHaveBeenCalledWith( + expect.anything(), // amplify instance + expect.objectContaining({ + options: expect.objectContaining({ + headers: ['oidc', 'userPool', 'lambda'].includes(authMode) ? expect.objectContaining({ - 'X-Api-Key': apiKey, + Authorization: authToken || DEFAULT_AUTH_TOKEN, }) : expect.not.objectContaining({ - 'X-Api-Key': apiKey, + Authorization: expect.anything(), }), }), - // `url` is an instance of `URL` - url: expect.objectContaining({ - href: endpoint, - }), }), ); } /** - * Validates that a "post" occurred (against `_postSpy`) to the given endpoint URL - * specifically with or without the given `apiKey` (defaults to globally configured - * `API_KEY`) depending on the given `withApiKey` argument. + * Validates that a specific subscription occurred (against `_subSpy`). * * @param options */ function expectSubscription({ endpoint, - withApiKey, - apiKey = API_KEY, + authMode, + apiKey, + authToken, }: { endpoint: string; - withApiKey: boolean; - apiKey?: string; + authMode: AuthMode; + apiKey: string | undefined; + authToken: string | undefined; }) { + if (authMode === 'apiKey') expect(apiKey).not.toBeFalsy(); // sanity check + + // `authMode` is provided to subs, which then determine how to handle it. + // subs always receives `apiKey`, because `.graphql()` determines which to use. + // subs only receive `authMode` when it is provided to `generateClient()` or + // `.graphql()` directly. expect(_subspy).toHaveBeenCalledWith( expect.objectContaining({ appSyncGraphqlEndpoint: endpoint, + authenticationType: authMode, + authToken, + apiKey, }), expect.anything(), ); +} - if (apiKey) { - expect(_subspy).toHaveBeenCalledWith( - expect.objectContaining({ apiKey }), - expect.anything(), - ); - } else { - expect(_subspy).toHaveBeenCalledWith( - expect.not.objectContaining({ apiKey }), - expect.anything(), - ); - } +/** + * Validates that a specific operation was submitted to the correct underlying + * execution mechanism (post or AppSyncRealtime). + * + * --- + * + * ## IMPORTANT! + * + * ### You MUST omit `authToken` in most cases. + * + * Like this: + * + * ```ts + * expectOp({ + * // ... + * authToken: undefined, // or omit entirely + * }) + * ``` + * + * *(This is due to difference in the way queries/subs mocks are handled, which is + * done the way it is to avoid mocking the deep guts of AppSyncRealtime.)* + * + * --- + * + * ### When *should* you provide `authToken`? + * + * When it has been provided to either `generateClient()` or `client.graphql()` directly. + * + * ```ts + * // like this + * const client = generateClient({ authToken: SOME_TOKEN } + * + * // or this + * client.graphql({ + * // ... + * authToken: SOME_TOKEN + * }) + * ``` + * + * Otherwise, `expectOp` will use the config-mocked `authToken` appropriately. + * + * @param param0 + */ +function expectOp({ + op, + endpoint, + authMode, + apiKey, + authToken, +}: { + op: 'subscription' | 'query'; + endpoint: string; + authMode: AuthMode; + apiKey: string | undefined; + authToken?: string | undefined; +}) { + const expecto = op === 'subscription' ? expectSubscription : expectPost; + expecto({ endpoint, authMode, apiKey, authToken }); // test pass ... umm ... } describe.skip('API generateClient', () => { @@ -167,8 +255,8 @@ describe.only('Custom Endpoints', () => { { API: { GraphQL: { - defaultAuthMode: 'apiKey', - apiKey: API_KEY, + defaultAuthMode: DEFAULT_AUTH_MODE, + apiKey: DEFAULT_API_KEY, endpoint: DEFAULT_ENDPOINT, region: 'north-pole-7', }, @@ -197,7 +285,7 @@ describe.only('Custom Endpoints', () => { }, tokenProvider: { getTokens: async () => ({ - accessToken: decodeJWT(JWT_STRING), + accessToken: decodeJWT(DEFAULT_AUTH_TOKEN), }), }, }, @@ -223,8 +311,7 @@ describe.only('Custom Endpoints', () => { jest.resetAllMocks(); }); - for (const op of ['query', 'subscription']) { - const expectOp = op === 'subscription' ? expectSubscription : expectPost; + for (const op of ['query', 'subscription'] as const) { const opType = op === 'subscription' ? 'sub' : 'qry'; test(`client { endpoint: N, authMode: N } + ${opType} { authMode: N } -> config.authMode`, async () => { @@ -233,8 +320,11 @@ describe.only('Custom Endpoints', () => { await client.graphql({ query: `${op} A { queryA { a b c } }` }); expectOp({ + op, endpoint: DEFAULT_ENDPOINT, - withApiKey: true, + authMode: DEFAULT_AUTH_MODE, + apiKey: DEFAULT_API_KEY, + authToken: undefined, }); }); @@ -247,8 +337,11 @@ describe.only('Custom Endpoints', () => { }); expectOp({ + op, endpoint: DEFAULT_ENDPOINT, - withApiKey: false, // from op.authMode = none + authMode: 'none', + apiKey: DEFAULT_API_KEY, + authToken: undefined, }); }); @@ -262,8 +355,11 @@ describe.only('Custom Endpoints', () => { }); expectOp({ + op, endpoint: DEFAULT_ENDPOINT, - withApiKey: false, // from client.authMode = none + authMode: 'none', + apiKey: DEFAULT_API_KEY, + authToken: undefined, }); }); @@ -278,15 +374,18 @@ describe.only('Custom Endpoints', () => { }); expectOp({ + op, endpoint: DEFAULT_ENDPOINT, - withApiKey: false, // from op.authMode = none + authMode: 'none', + apiKey: DEFAULT_API_KEY, + authToken: undefined, }); }); - test.only(`client { endpoint: Y, authMode: N } + ${opType} { authMode: N } -> none (defaulted)`, async () => { + test(`client { endpoint: Y, authMode: N } + ${opType} { authMode: N } -> none (defaulted)`, async () => { const client = generateClient({ endpoint: CUSTOM_ENDPOINT, - authMode: 'lambda', + authMode: 'userPool', }); await client.graphql({ @@ -294,25 +393,32 @@ describe.only('Custom Endpoints', () => { }); expectOp({ + op, endpoint: CUSTOM_ENDPOINT, - withApiKey: false, // from client.endpoint -> default = none + authMode: 'userPool', + apiKey: DEFAULT_API_KEY, + authToken: undefined, }); }); test(`client { endpoint: Y, authMode: N } + ${opType} { authMode: Y } -> op.authMode`, async () => { const client = generateClient({ endpoint: CUSTOM_ENDPOINT, - authMode: 'lambda', + authMode: 'apiKey', + apiKey: CUSTOM_API_KEY, }); await client.graphql({ query: `${op} A { queryA { a b c } }`, - authMode: 'apiKey', + authMode: 'userPool', }); expectOp({ + op, endpoint: CUSTOM_ENDPOINT, - withApiKey: true, // from op.authMode = apiKey + authMode: 'userPool', + apiKey: CUSTOM_API_KEY, + authToken: undefined, }); }); @@ -328,9 +434,11 @@ describe.only('Custom Endpoints', () => { }); expectOp({ + op, endpoint: CUSTOM_ENDPOINT, - withApiKey: true, // from client.authMode = apiKey + authMode: 'apiKey', apiKey: CUSTOM_API_KEY, + authToken: undefined, }); }); @@ -346,8 +454,11 @@ describe.only('Custom Endpoints', () => { }); expectOp({ + op, endpoint: CUSTOM_ENDPOINT, - withApiKey: true, // from op.authMode = apiKey + authMode: 'apiKey', + apiKey: DEFAULT_API_KEY, + authToken: undefined, }); }); } From d5bb5da0c71a05a37b0d0bdb74f3a8ddb3cd3c02 Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Fri, 13 Dec 2024 11:54:55 -0600 Subject: [PATCH 12/16] test refactor for clarity --- packages/api/__tests__/API.test.ts | 167 ++++++++++------------------- 1 file changed, 54 insertions(+), 113 deletions(-) diff --git a/packages/api/__tests__/API.test.ts b/packages/api/__tests__/API.test.ts index 3695abc50e8..d8fd35fc1e5 100644 --- a/packages/api/__tests__/API.test.ts +++ b/packages/api/__tests__/API.test.ts @@ -38,53 +38,44 @@ const _subspy = jest.spyOn((GraphQLAPI as any).appSyncRealTime, 'subscribe'); function expectPost({ endpoint, authMode, - apiKey, - authToken, + apiKeyOverride, + authTokenOverride, }: { endpoint: string; authMode: AuthMode; - apiKey: string | undefined; - authToken: string | undefined; + apiKeyOverride: string | undefined; + authTokenOverride: string | undefined; }) { - if (authMode === 'apiKey') expect(apiKey).not.toBeFalsy(); // sanity check - expect(_postSpy).toHaveBeenCalledWith( - expect.anything(), // amplify instance - expect.objectContaining({ - // `url` is an instance of `URL` - url: expect.objectContaining({ - href: endpoint, - }), - }), - ); - expect(_postSpy).toHaveBeenCalledWith( - expect.anything(), // amplify instance - expect.objectContaining({ - options: expect.objectContaining({ - headers: - authMode === 'apiKey' - ? expect.objectContaining({ - 'X-Api-Key': apiKey, - }) - : expect.not.objectContaining({ - 'X-Api-Key': expect.anything(), - }), - }), - }), - ); - expect(_postSpy).toHaveBeenCalledWith( - expect.anything(), // amplify instance - expect.objectContaining({ - options: expect.objectContaining({ - headers: ['oidc', 'userPool', 'lambda'].includes(authMode) - ? expect.objectContaining({ - Authorization: authToken || DEFAULT_AUTH_TOKEN, - }) - : expect.not.objectContaining({ - Authorization: expect.anything(), - }), - }), - }), - ); + // Grabbing the call and asserting on the object is significantly simpler for some + // of the is-unknown-or-absent types of assertions we need. + // + // It is also incidentally much simpler for most the other assertions too ... + // + const postOptions = _postSpy.mock.calls[0][1] as { + // just the things we care about + url: URL; + options: { + headers: Record; + }; + }; + + expect(postOptions.url.toString()).toEqual(endpoint); + + if (authMode === 'apiKey') { + expect(postOptions.options.headers['X-Api-Key']).toEqual( + apiKeyOverride ?? DEFAULT_API_KEY, + ); + } else { + expect(postOptions.options.headers['X-Api-Key']).toBeUndefined(); + } + + if (['oidc', 'userPool'].includes(authMode)) { + expect(postOptions.options.headers['Authorization']).toEqual( + authTokenOverride ?? DEFAULT_AUTH_TOKEN, + ); + } else { + expect(postOptions.options.headers['Authorization']).toBeUndefined(); + } } /** @@ -95,26 +86,27 @@ function expectPost({ function expectSubscription({ endpoint, authMode, - apiKey, - authToken, + apiKeyOverride, + authTokenOverride, }: { endpoint: string; authMode: AuthMode; - apiKey: string | undefined; - authToken: string | undefined; + apiKeyOverride: string | undefined; + authTokenOverride: string | undefined; }) { - if (authMode === 'apiKey') expect(apiKey).not.toBeFalsy(); // sanity check - - // `authMode` is provided to subs, which then determine how to handle it. - // subs always receives `apiKey`, because `.graphql()` determines which to use. - // subs only receive `authMode` when it is provided to `generateClient()` or - // `.graphql()` directly. + // `authMode` is provided to appsync provider, which then determines how to + // handle auth internally. expect(_subspy).toHaveBeenCalledWith( expect.objectContaining({ appSyncGraphqlEndpoint: endpoint, authenticationType: authMode, - authToken, - apiKey, + + // appsync provider only receive an authToken if it has been explicitly overridden. + authToken: authTokenOverride, + + // appsync provider already receive an apiKey. + // (but it should not send it unless authMode is apiKey.) + apiKey: apiKeyOverride ?? DEFAULT_API_KEY, }), expect.anything(), ); @@ -124,60 +116,23 @@ function expectSubscription({ * Validates that a specific operation was submitted to the correct underlying * execution mechanism (post or AppSyncRealtime). * - * --- - * - * ## IMPORTANT! - * - * ### You MUST omit `authToken` in most cases. - * - * Like this: - * - * ```ts - * expectOp({ - * // ... - * authToken: undefined, // or omit entirely - * }) - * ``` - * - * *(This is due to difference in the way queries/subs mocks are handled, which is - * done the way it is to avoid mocking the deep guts of AppSyncRealtime.)* - * - * --- - * - * ### When *should* you provide `authToken`? - * - * When it has been provided to either `generateClient()` or `client.graphql()` directly. - * - * ```ts - * // like this - * const client = generateClient({ authToken: SOME_TOKEN } - * - * // or this - * client.graphql({ - * // ... - * authToken: SOME_TOKEN - * }) - * ``` - * - * Otherwise, `expectOp` will use the config-mocked `authToken` appropriately. - * * @param param0 */ function expectOp({ op, endpoint, authMode, - apiKey, - authToken, + apiKeyOverride, + authTokenOverride, }: { op: 'subscription' | 'query'; endpoint: string; authMode: AuthMode; - apiKey: string | undefined; - authToken?: string | undefined; + apiKeyOverride?: string | undefined; + authTokenOverride?: string | undefined; }) { const expecto = op === 'subscription' ? expectSubscription : expectPost; - expecto({ endpoint, authMode, apiKey, authToken }); // test pass ... umm ... + expecto({ endpoint, authMode, apiKeyOverride, authTokenOverride }); // test pass ... umm ... } describe.skip('API generateClient', () => { @@ -323,8 +278,6 @@ describe.only('Custom Endpoints', () => { op, endpoint: DEFAULT_ENDPOINT, authMode: DEFAULT_AUTH_MODE, - apiKey: DEFAULT_API_KEY, - authToken: undefined, }); }); @@ -340,8 +293,6 @@ describe.only('Custom Endpoints', () => { op, endpoint: DEFAULT_ENDPOINT, authMode: 'none', - apiKey: DEFAULT_API_KEY, - authToken: undefined, }); }); @@ -358,8 +309,6 @@ describe.only('Custom Endpoints', () => { op, endpoint: DEFAULT_ENDPOINT, authMode: 'none', - apiKey: DEFAULT_API_KEY, - authToken: undefined, }); }); @@ -377,8 +326,6 @@ describe.only('Custom Endpoints', () => { op, endpoint: DEFAULT_ENDPOINT, authMode: 'none', - apiKey: DEFAULT_API_KEY, - authToken: undefined, }); }); @@ -396,8 +343,6 @@ describe.only('Custom Endpoints', () => { op, endpoint: CUSTOM_ENDPOINT, authMode: 'userPool', - apiKey: DEFAULT_API_KEY, - authToken: undefined, }); }); @@ -417,8 +362,7 @@ describe.only('Custom Endpoints', () => { op, endpoint: CUSTOM_ENDPOINT, authMode: 'userPool', - apiKey: CUSTOM_API_KEY, - authToken: undefined, + apiKeyOverride: CUSTOM_API_KEY, }); }); @@ -437,8 +381,7 @@ describe.only('Custom Endpoints', () => { op, endpoint: CUSTOM_ENDPOINT, authMode: 'apiKey', - apiKey: CUSTOM_API_KEY, - authToken: undefined, + apiKeyOverride: CUSTOM_API_KEY, }); }); @@ -457,8 +400,6 @@ describe.only('Custom Endpoints', () => { op, endpoint: CUSTOM_ENDPOINT, authMode: 'apiKey', - apiKey: DEFAULT_API_KEY, - authToken: undefined, }); }); } From 309cef3010aa7c1bdcd34f4a2ae89ad5d5a696ba Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Mon, 16 Dec 2024 12:01:44 -0600 Subject: [PATCH 13/16] conditional types for apikey on client.graphql, propagated from client endpoint and auth config --- .../src/internals/generateClient.ts | 10 +- .../generateClientWithAmplifyInstance.ts | 19 ++- packages/api-graphql/src/internals/types.ts | 52 ++++--- .../api-graphql/src/server/generateClient.ts | 30 +++- packages/api-graphql/src/types/index.ts | 140 +++++++++++++----- packages/api/__tests__/API.test.ts | 15 +- packages/api/src/API.ts | 24 ++- 7 files changed, 220 insertions(+), 70 deletions(-) diff --git a/packages/api-graphql/src/internals/generateClient.ts b/packages/api-graphql/src/internals/generateClient.ts index afeec2feb72..fceb147137c 100644 --- a/packages/api-graphql/src/internals/generateClient.ts +++ b/packages/api-graphql/src/internals/generateClient.ts @@ -35,9 +35,13 @@ import { ClientGenerationParams } from './types'; * @param params * @returns */ -export function generateClient = never>( - params: ClientGenerationParams, -): V6Client { +export function generateClient< + T extends Record = never, + WithCustomEndpoint extends boolean = false, + WithApiKey extends boolean = false, +>( + params: ClientGenerationParams, +): V6Client { const client = { [__amplify]: params.amplify, [__authMode]: params.authMode, diff --git a/packages/api-graphql/src/internals/server/generateClientWithAmplifyInstance.ts b/packages/api-graphql/src/internals/server/generateClientWithAmplifyInstance.ts index 8a9927d543c..219a07b570e 100644 --- a/packages/api-graphql/src/internals/server/generateClientWithAmplifyInstance.ts +++ b/packages/api-graphql/src/internals/server/generateClientWithAmplifyInstance.ts @@ -9,8 +9,10 @@ import { V6ClientSSRCookies, V6ClientSSRRequest, __amplify, + __apiKey, __authMode, __authToken, + __endpoint, __headers, getInternals, } from '../../types'; @@ -30,16 +32,25 @@ import { cancel, graphql, isCancelError } from '..'; */ export function generateClientWithAmplifyInstance< T extends Record = never, + WithCustomEndpoint extends boolean = false, + WithApiKey extends boolean = false, ClientType extends - | V6ClientSSRRequest - | V6ClientSSRCookies = V6ClientSSRCookies, + | V6ClientSSRRequest + | V6ClientSSRCookies< + T, + WithCustomEndpoint, + WithApiKey + > = V6ClientSSRCookies, >( - params: ServerClientGenerationParams & CommonPublicClientOptions, + params: ServerClientGenerationParams & + CommonPublicClientOptions, ): ClientType { const client = { [__amplify]: params.amplify, [__authMode]: params.authMode, [__authToken]: params.authToken, + [__apiKey]: 'apiKey' in params ? params.apiKey : undefined, + [__endpoint]: 'endpoint' in params ? params.endpoint : undefined, [__headers]: params.headers, graphql, cancel, @@ -48,7 +59,7 @@ export function generateClientWithAmplifyInstance< const apiGraphqlConfig = params.config?.API?.GraphQL; - if (isApiGraphQLConfig(apiGraphqlConfig)) { + if (!client[__endpoint] && isApiGraphQLConfig(apiGraphqlConfig)) { addSchemaToClientWithInstance(client, params, getInternals); } diff --git a/packages/api-graphql/src/internals/types.ts b/packages/api-graphql/src/internals/types.ts index bbbe8110906..3c31ad025f2 100644 --- a/packages/api-graphql/src/internals/types.ts +++ b/packages/api-graphql/src/internals/types.ts @@ -9,30 +9,46 @@ import { CustomHeaders } from '@aws-amplify/data-schema/runtime'; * * The knobs available for configuring `generateClient` internally. */ -export type ClientGenerationParams = { +export type ClientGenerationParams< + WithCustomEndpoint extends boolean, + WithApiKey extends boolean, +> = { amplify: AmplifyClassV6; -} & CommonPublicClientOptions; +} & CommonPublicClientOptions; /** * Common options that can be used on public `generateClient()` interfaces. */ -export type CommonPublicClientOptions = - | { +export type CommonPublicClientOptions< + WithCustomEndpoint extends boolean, + WithApiKey extends boolean, +> = WithCustomEndpoint extends true + ? WithApiKey extends true + ? + | { + endpoint: string; + authMode: 'apiKey'; + apiKey: string; + authToken?: string; + headers?: CustomHeaders; + } + | { + endpoint: string; + apiKey: string; + authMode: Exclude; + authToken?: string; + headers?: CustomHeaders; + } + : { + endpoint: string; + authMode: Exclude; + apiKey?: never; + authToken?: string; + headers?: CustomHeaders; + } + : { endpoint?: never; authMode?: GraphQLAuthMode; authToken?: string; headers?: CustomHeaders; - } - | { - endpoint: string; - authMode: 'apiKey'; - apiKey: string; - authToken?: string; - headers?: CustomHeaders; - } - | { - endpoint: string; - authMode: Exclude; - authToken?: string; - headers?: CustomHeaders; - }; + }; diff --git a/packages/api-graphql/src/server/generateClient.ts b/packages/api-graphql/src/server/generateClient.ts index 09a60595231..9ea767635e8 100644 --- a/packages/api-graphql/src/server/generateClient.ts +++ b/packages/api-graphql/src/server/generateClient.ts @@ -33,14 +33,26 @@ import { * }), * }); */ -export function generateClient = never>({ +export function generateClient< + T extends Record = never, + WithCustomEndpoint extends boolean = false, + WithApiKey extends boolean = false, +>({ config, authMode, authToken, -}: GenerateServerClientParams): V6ClientSSRRequest { +}: GenerateServerClientParams< + WithCustomEndpoint, + WithApiKey +>): V6ClientSSRRequest { // passing `null` instance because each (future model) method must retrieve a valid instance // from server context - const client = generateClientWithAmplifyInstance>({ + const client = generateClientWithAmplifyInstance< + T, + any, + any, + V6ClientSSRRequest + >({ amplify: null, config, authMode, @@ -48,7 +60,10 @@ export function generateClient = never>({ }); // TODO: improve this and the next type - const prevGraphql = client.graphql as unknown as GraphQLMethod; + const prevGraphql = client.graphql as unknown as GraphQLMethod< + WithCustomEndpoint, + WithApiKey + >; const wrappedGraphql = ( contextSpec: AmplifyServer.ContextSpec, @@ -59,12 +74,15 @@ export function generateClient = never>({ return prevGraphql.call( { [__amplify]: amplifyInstance }, - options, + options as any, additionalHeaders as any, ); }; - client.graphql = wrappedGraphql as unknown as GraphQLMethodSSR; + client.graphql = wrappedGraphql as unknown as GraphQLMethodSSR< + WithCustomEndpoint, + WithApiKey + >; return client; } diff --git a/packages/api-graphql/src/types/index.ts b/packages/api-graphql/src/types/index.ts index 46a17b8fe61..a2df9091285 100644 --- a/packages/api-graphql/src/types/index.ts +++ b/packages/api-graphql/src/types/index.ts @@ -18,13 +18,15 @@ import { } from '@aws-amplify/core/internals/utils'; import { AmplifyServer } from '@aws-amplify/core/internals/adapter-core'; +import { CommonPublicClientOptions } from '../internals/types'; + export { OperationTypeNode } from 'graphql'; export { CONTROL_MSG, ConnectionState } from './PubSub'; export { SelectionSet } from '@aws-amplify/data-schema/runtime'; -export { CommonPublicClientOptions } from '../internals/types'; +export { CommonPublicClientOptions }; /** * Loose/Unknown options for raw GraphQLAPICategory `graphql()`. @@ -211,20 +213,69 @@ export type GraphQLOperation = Source | string; * API V6 `graphql({options})` type that can leverage branded graphql `query` * objects and fallback types. */ -export interface GraphQLOptionsV6< +export type GraphQLOptionsV6< FALLBACK_TYPES = unknown, TYPED_GQL_STRING extends string = string, -> { - query: TYPED_GQL_STRING | DocumentNode; - variables?: GraphQLVariablesV6; - authMode?: GraphQLAuthMode; - apiKey?: string; - authToken?: string; - /** - * @deprecated This property should not be used - */ - userAgentSuffix?: string; -} + WithCustomEndpoint extends boolean = false, + WithApiKey extends boolean = false, // i.e., The client already has apiKey configured. +> = WithCustomEndpoint extends true + ? WithApiKey extends true + ? { + query: TYPED_GQL_STRING | DocumentNode; + variables?: GraphQLVariablesV6; + authMode?: GraphQLAuthMode; + apiKey?: string; + authToken?: string; + /** + * @deprecated This property should not be used + */ + userAgentSuffix?: string; + } + : + | { + query: TYPED_GQL_STRING | DocumentNode; + variables?: GraphQLVariablesV6; + authMode?: never; + apiKey?: never; + authToken?: string; + /** + * @deprecated This property should not be used + */ + userAgentSuffix?: string; + } + | { + query: TYPED_GQL_STRING | DocumentNode; + variables?: GraphQLVariablesV6; + authMode: 'apiKey'; + apiKey: string; + authToken?: string; + /** + * @deprecated This property should not be used + */ + userAgentSuffix?: string; + } + | { + query: TYPED_GQL_STRING | DocumentNode; + variables?: GraphQLVariablesV6; + authMode: Exclude; + apiKey?: never; + authToken?: string; + /** + * @deprecated This property should not be used + */ + userAgentSuffix?: string; + } + : { + query: TYPED_GQL_STRING | DocumentNode; + variables?: GraphQLVariablesV6; + authMode?: GraphQLAuthMode; + apiKey?: string; + authToken?: string; + /** + * @deprecated This property should not be used + */ + userAgentSuffix?: string; + }; /** * Result type for `graphql()` operations that don't include any specific @@ -394,38 +445,60 @@ export type ClientWithModels = | V6ClientSSRRequest | V6ClientSSRCookies; -export type V6Client = never> = { - graphql: GraphQLMethod; +export type V6Client< + T extends Record = never, + WithCustomEndpoint extends boolean = false, + WithApiKey extends boolean = false, +> = { + graphql: GraphQLMethod; cancel(promise: Promise, message?: string): boolean; isCancelError(error: any): boolean; } & ClientExtensions; -export type V6ClientSSRRequest = never> = { - graphql: GraphQLMethodSSR; +export type V6ClientSSRRequest< + T extends Record = never, + WithCustomEndpoint extends boolean = false, + WithApiKey extends boolean = false, +> = { + graphql: GraphQLMethodSSR; cancel(promise: Promise, message?: string): boolean; isCancelError(error: any): boolean; } & ClientExtensionsSSRRequest; -export type V6ClientSSRCookies = never> = { - graphql: GraphQLMethod; +export type V6ClientSSRCookies< + T extends Record = never, + WithCustomEndpoint extends boolean = false, + WithApiKey extends boolean = false, +> = { + graphql: GraphQLMethod; cancel(promise: Promise, message?: string): boolean; isCancelError(error: any): boolean; } & ClientExtensionsSSRCookies; -export type GraphQLMethod = < - FALLBACK_TYPES = unknown, - TYPED_GQL_STRING extends string = string, ->( - options: GraphQLOptionsV6, +export type GraphQLMethod< + WithCustomEndpoint extends boolean, + WithApiKey extends boolean, +> = ( + options: GraphQLOptionsV6< + FALLBACK_TYPES, + TYPED_GQL_STRING, + WithCustomEndpoint, + WithApiKey + >, additionalHeaders?: CustomHeaders | undefined, ) => GraphQLResponseV6; -export type GraphQLMethodSSR = < - FALLBACK_TYPES = unknown, - TYPED_GQL_STRING extends string = string, ->( +export type GraphQLMethodSSR< + WithCustomEndpoint extends boolean, + WithApiKey extends boolean, +> = ( contextSpec: AmplifyServer.ContextSpec, - options: GraphQLOptionsV6, + options: GraphQLOptionsV6< + FALLBACK_TYPES, + TYPED_GQL_STRING, + WithCustomEndpoint, + WithApiKey + >, additionalHeaders?: CustomHeaders | undefined, ) => GraphQLResponseV6; @@ -457,8 +530,9 @@ export interface AuthModeParams extends Record { authToken?: string; } -export interface GenerateServerClientParams { +export type GenerateServerClientParams< + WithCustomEndpoint extends boolean, + WithApiKey extends boolean, +> = { config: ResourcesConfig; - authMode?: GraphQLAuthMode; - authToken?: string; -} +} & CommonPublicClientOptions; diff --git a/packages/api/__tests__/API.test.ts b/packages/api/__tests__/API.test.ts index d8fd35fc1e5..a4aea44ddeb 100644 --- a/packages/api/__tests__/API.test.ts +++ b/packages/api/__tests__/API.test.ts @@ -388,19 +388,30 @@ describe.only('Custom Endpoints', () => { test(`client { endpoint: Y, authMode: Y } + ${opType} { authMode: Y } -> op.authMode`, async () => { const client = generateClient({ endpoint: CUSTOM_ENDPOINT, - authMode: 'none', + authMode: 'userPool', }); await client.graphql({ query: `${op} A { queryA { a b c } }`, authMode: 'apiKey', - }); + apiKey: CUSTOM_API_KEY + }) expectOp({ op, endpoint: CUSTOM_ENDPOINT, authMode: 'apiKey', + apiKeyOverride: CUSTOM_API_KEY }); }); } }); + + +// // because the client has an `endpoint` but no `apiKey`, using `apiKey` auth +// // in `.graphql()` also requires `apiKey` to be specified in the call +// // @ts-expect-error +// await client.graphql({ +// query: `${op} A { queryA { a b c } }`, +// authMode: 'apiKey', +// }); \ No newline at end of file diff --git a/packages/api/src/API.ts b/packages/api/src/API.ts index 8aee0fc3334..bcf338a98bd 100644 --- a/packages/api/src/API.ts +++ b/packages/api/src/API.ts @@ -11,10 +11,26 @@ import { Amplify } from '@aws-amplify/core'; * @throws {@link Error} - Throws error when client cannot be generated due to configuration issues. */ export function generateClient = never>( - options: CommonPublicClientOptions = {}, -): V6Client { + options?: CommonPublicClientOptions, +): V6Client; +export function generateClient = never>( + options?: CommonPublicClientOptions, +): V6Client; +export function generateClient = never>( + options?: CommonPublicClientOptions, +): V6Client; +export function generateClient = never>( + options?: CommonPublicClientOptions, +): V6Client; +export function generateClient< + WithCustomEndpoint extends boolean, + WithApiKey extends boolean, + T extends Record = never, +>( + options?: CommonPublicClientOptions, +): V6Client { return internalGenerateClient({ - ...options, + ...(options || ({} as any)), amplify: Amplify, - }) as unknown as V6Client; + }) as unknown as V6Client; } From 6b15b2e09269744b1f57fe855406f581433add30 Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Mon, 16 Dec 2024 14:49:43 -0600 Subject: [PATCH 14/16] add validation errors for customer endpoint, authmode, apikey auth --- .../src/internals/generateClient.ts | 13 + packages/api-graphql/src/internals/v6.ts | 45 +- packages/api/__tests__/API.test.ts | 412 ++++++++++-------- 3 files changed, 275 insertions(+), 195 deletions(-) diff --git a/packages/api-graphql/src/internals/generateClient.ts b/packages/api-graphql/src/internals/generateClient.ts index fceb147137c..e208a400f2a 100644 --- a/packages/api-graphql/src/internals/generateClient.ts +++ b/packages/api-graphql/src/internals/generateClient.ts @@ -61,6 +61,19 @@ export function generateClient< const apiGraphqlConfig = params.amplify.getConfig().API?.GraphQL; + if (client[__endpoint]) { + if (!client[__authMode]) { + throw new Error( + 'generateClient() requires an explicit `authMode` when `endpoint` is provided.', + ); + } + if (client[__authMode] === 'apiKey' && !client[__apiKey]) { + throw new Error( + "generateClient() requires an explicit `apiKey` when `endpoint` is provided and `authMode = 'apiKey'`.", + ); + } + } + if (!client[__endpoint]) { if (isApiGraphQLConfig(apiGraphqlConfig)) { addSchemaToClient(client, apiGraphqlConfig, getInternals); diff --git a/packages/api-graphql/src/internals/v6.ts b/packages/api-graphql/src/internals/v6.ts index 55514682723..793b3c8bd28 100644 --- a/packages/api-graphql/src/internals/v6.ts +++ b/packages/api-graphql/src/internals/v6.ts @@ -106,40 +106,41 @@ export function graphql< const internals = getInternals(this as any); /** - * The custom `endpoint` + `apiKey` (or `undefined`) specific to `generateClient()`. - * - * Q: Can we safely add these to the internals type and remove the cast? Or, does this - * force updates to the data-schema package? + * The custom `endpoint` specific to the client */ const clientEndpoint: string = (internals as any).endpoint; - options.apiKey = options.apiKey ?? (internals as any).apiKey; /** - * The `authMode` requested by the generated client. - * - * If an `endpoint` is present on the client, we create a "gate" around at the - * client level to prevent "more general" `authMode` settings (from the config) - * from being exposed unintentionally to an unrelated API. + * The `authMode` specific to the client. + */ + const clientAuthMode = internals.authMode; + + /** + * The `apiKey` specific to the client. */ - const clientAuthMode = - internals.authMode ?? (clientEndpoint ? 'none' : undefined); + const clientApiKey = (internals as any).apiKey; /** * The most specific `authMode` wins. Setting an `endpoint` value without also - * setting an `authMode` value is treated as explicitly setting `authMode` to "none". + * setting an `authMode` value is invalid. This helps to prevent customers apps + * from unexpectedly sending auth details to endpoints the auth details do not + * belong to. * - * E.g., if `.graphql({ endpoint })`, `authMode` is treated as explicitly 'none' at - * the request level, and any `authMode` provided to `generateClient()` or to - * `Amplify.configure()` is ignored. - * - * Reiterating, this serves as a gating mechanism to ensure auth details are not - * unexpected sent to API's they don't belong to. However, if `authMode` has been - * explicitly set alongside `endpoint`, we will assume this was intentional and - * use the normal/configured auth details for the endpoint. + * This is especially pronounced for `apiKey`. When both an `endpoint` *and* + * `authMode: 'apiKey'` are provided, an explicit `apiKey` override is required + * to prevent inadvertent sending of an API's `apiKey` to an endpoint is does + * not belong to. */ options.authMode = options.authMode || clientAuthMode; - + options.apiKey = options.apiKey ?? clientApiKey; options.authToken = options.authToken || internals.authToken; + + if (clientEndpoint && options.authMode === 'apiKey' && !options.apiKey) { + throw new Error( + "graphql() requires an explicit `apiKey` for a custom `endpoint` when `authMode = 'apiKey'`.", + ); + } + const headers = additionalHeaders || internals.headers; /** diff --git a/packages/api/__tests__/API.test.ts b/packages/api/__tests__/API.test.ts index a4aea44ddeb..add59f9f293 100644 --- a/packages/api/__tests__/API.test.ts +++ b/packages/api/__tests__/API.test.ts @@ -22,7 +22,7 @@ const DEFAULT_ENDPOINT = 'https://a-default-appsync-endpoint.local/graphql'; const CUSTOM_ENDPOINT = 'https://a-custom-appsync-endpoint.local/graphql'; /** - * Valid JWT string, borrowed from Auth tests + * Validly parsable JWT string. (Borrowed from Auth tests.) */ const DEFAULT_AUTH_TOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3MTAyOTMxMzB9.YzDpgJsrB3z-ZU1XxMcXSQsMbgCzwH_e-_76rnfehh0'; @@ -46,6 +46,7 @@ function expectPost({ apiKeyOverride: string | undefined; authTokenOverride: string | undefined; }) { + // // Grabbing the call and asserting on the object is significantly simpler for some // of the is-unknown-or-absent types of assertions we need. // @@ -135,6 +136,63 @@ function expectOp({ expecto({ endpoint, authMode, apiKeyOverride, authTokenOverride }); // test pass ... umm ... } +function prepareMocks() { + Amplify.configure( + { + API: { + GraphQL: { + defaultAuthMode: DEFAULT_AUTH_MODE, + apiKey: DEFAULT_API_KEY, + endpoint: DEFAULT_ENDPOINT, + region: 'north-pole-7', + }, + }, + Auth: { + Cognito: { + userPoolId: 'north-pole-7:santas-little-helpers', + identityPoolId: 'north-pole-7:santas-average-sized-helpers', + userPoolClientId: 'the-mrs-claus-oversight-committee', + }, + }, + }, + { + Auth: { + credentialsProvider: { + getCredentialsAndIdentityId: async arg => ({ + credentials: { + accessKeyId: 'accessKeyIdValue', + secretAccessKey: 'secretAccessKeyValue', + sessionToken: 'sessionTokenValue', + expiration: new Date(123), + }, + identityId: 'mrs-clause-naturally', + }), + clearCredentialsAndIdentityId: async () => {}, + }, + tokenProvider: { + getTokens: async () => ({ + accessToken: decodeJWT(DEFAULT_AUTH_TOKEN), + }), + }, + }, + }, + ); + _postSpy.mockReturnValue({ + body: { + json() { + return JSON.stringify({ + data: { + someOperation: { + someField: 'some value', + }, + }, + }); + }, + }, + }); + _subspy.mockReturnValue(new Observable()); +} + describe.skip('API generateClient', () => { afterEach(() => { jest.clearAllMocks(); @@ -204,62 +262,9 @@ describe.skip('API generateClient', () => { // }); }); -describe.only('Custom Endpoints', () => { +describe.only('generateClient', () => { beforeEach(() => { - Amplify.configure( - { - API: { - GraphQL: { - defaultAuthMode: DEFAULT_AUTH_MODE, - apiKey: DEFAULT_API_KEY, - endpoint: DEFAULT_ENDPOINT, - region: 'north-pole-7', - }, - }, - Auth: { - Cognito: { - userPoolId: 'north-pole-7:santas-little-helpers', - identityPoolId: 'north-pole-7:santas-average-sized-helpers', - userPoolClientId: 'the-mrs-claus-oversight-committee', - }, - }, - }, - { - Auth: { - credentialsProvider: { - getCredentialsAndIdentityId: async arg => ({ - credentials: { - accessKeyId: 'accessKeyIdValue', - secretAccessKey: 'secretAccessKeyValue', - sessionToken: 'sessionTokenValue', - expiration: new Date(123), - }, - identityId: 'mrs-clause-naturally', - }), - clearCredentialsAndIdentityId: async () => {}, - }, - tokenProvider: { - getTokens: async () => ({ - accessToken: decodeJWT(DEFAULT_AUTH_TOKEN), - }), - }, - }, - }, - ); - _postSpy.mockReturnValue({ - body: { - json() { - return JSON.stringify({ - data: { - someOperation: { - someField: 'some value', - }, - }, - }); - }, - }, - }); - _subspy.mockReturnValue(new Observable()); + prepareMocks() }); afterEach(() => { @@ -269,149 +274,210 @@ describe.only('Custom Endpoints', () => { for (const op of ['query', 'subscription'] as const) { const opType = op === 'subscription' ? 'sub' : 'qry'; - test(`client { endpoint: N, authMode: N } + ${opType} { authMode: N } -> config.authMode`, async () => { - const client = generateClient(); - - await client.graphql({ query: `${op} A { queryA { a b c } }` }); - - expectOp({ - op, - endpoint: DEFAULT_ENDPOINT, - authMode: DEFAULT_AUTH_MODE, - }); - }); - - test(`client { endpoint: N, authMode: N } + ${opType} { authMode: Y } -> op.authMode`, async () => { - const client = generateClient(); - - await client.graphql({ - query: `${op} A { queryA { a b c } }`, - authMode: 'none', - }); - - expectOp({ - op, - endpoint: DEFAULT_ENDPOINT, - authMode: 'none', - }); - }); - - test(`client { endpoint: N, authMode: Y } + ${opType} { authMode: N } -> client.authMode`, async () => { - const client = generateClient({ - authMode: 'none', - }); - - await client.graphql({ - query: `${op} A { queryA { a b c } }`, - }); - - expectOp({ - op, - endpoint: DEFAULT_ENDPOINT, - authMode: 'none', + describe(`[${opType}] without a custom endpoint`, () => { + test("does not require `authMode` or `apiKey` override", () => { + expect(() => generateClient()).not.toThrow(); }); - }); - test(`client { endpoint: N, authMode: Y } + ${opType} { authMode: Y } -> op.authMode`, async () => { - const client = generateClient({ - authMode: 'apiKey', - }); + test("does not require `authMode` or `apiKey` override in client.graphql()", async () => { + const client = generateClient(); - await client.graphql({ - query: `${op} A { queryA { a b c } }`, - authMode: 'none', + await client.graphql({ query: `${op} A { queryA { a b c } }` }); + + expectOp({ + op, + endpoint: DEFAULT_ENDPOINT, + authMode: DEFAULT_AUTH_MODE, + }); }); - expectOp({ - op, - endpoint: DEFAULT_ENDPOINT, - authMode: 'none', + test("allows `authMode` override in client", async () => { + const client = generateClient({ + authMode: 'userPool', + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + }); + + expectOp({ + op, + endpoint: DEFAULT_ENDPOINT, + authMode: 'userPool', + }); }); - }); - test(`client { endpoint: Y, authMode: N } + ${opType} { authMode: N } -> none (defaulted)`, async () => { - const client = generateClient({ - endpoint: CUSTOM_ENDPOINT, - authMode: 'userPool', + test("allows `authMode` override in `client.graphql()`", async () => { + const client = generateClient(); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + authMode: 'userPool', + }); + + expectOp({ + op, + endpoint: DEFAULT_ENDPOINT, + authMode: 'userPool', + }); }); - await client.graphql({ - query: `${op} A { queryA { a b c } }`, + test("allows `apiKey` override in `client.graphql()`", async () => { + const client = generateClient(); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + apiKey: CUSTOM_API_KEY, + }); + + expectOp({ + op, + endpoint: DEFAULT_ENDPOINT, + authMode: 'apiKey', + apiKeyOverride: CUSTOM_API_KEY + }); }); - expectOp({ - op, - endpoint: CUSTOM_ENDPOINT, - authMode: 'userPool', + test("allows `authMode` + `apiKey` override in `client.graphql()`", async () => { + const client = generateClient({ + authMode: 'userPool' + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + authMode: 'apiKey', + apiKey: CUSTOM_API_KEY, + }); + + expectOp({ + op, + endpoint: DEFAULT_ENDPOINT, + authMode: 'apiKey', + apiKeyOverride: CUSTOM_API_KEY + }); }); }); - test(`client { endpoint: Y, authMode: N } + ${opType} { authMode: Y } -> op.authMode`, async () => { - const client = generateClient({ - endpoint: CUSTOM_ENDPOINT, - authMode: 'apiKey', - apiKey: CUSTOM_API_KEY, - }); + describe(`[${opType}] with a custom endpoint`, () => { + test("requires `authMode` override", () => { + // @ts-expect-error + expect(() => generateClient({ + endpoint: CUSTOM_ENDPOINT + })).toThrow() + }) - await client.graphql({ - query: `${op} A { queryA { a b c } }`, - authMode: 'userPool', + test("requires `apiKey` with `authMode: 'apiKey'` override in client", async () => { + expect(() => generateClient({ + endpoint: CUSTOM_ENDPOINT, + // @ts-ignore + authMode: 'apiKey', + })).toThrow(); }); - expectOp({ - op, - endpoint: CUSTOM_ENDPOINT, - authMode: 'userPool', - apiKeyOverride: CUSTOM_API_KEY, + test("allows `authMode` override in client", async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + authMode: 'userPool', + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + }); + + expectOp({ + op, + endpoint: CUSTOM_ENDPOINT, + authMode: 'userPool', + }); }); - }); - test(`client { endpoint: Y, authMode: Y } + ${opType} { authMode: N } -> client.authMode`, async () => { - const client = generateClient({ - endpoint: CUSTOM_ENDPOINT, - authMode: 'apiKey', - apiKey: CUSTOM_API_KEY, + test("allows `authMode: 'none'` override in client.graphql()", async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + }); + + expectOp({ + op, + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); }); - await client.graphql({ - query: `${op} A { queryA { a b c } }`, + test("allows `authMode: 'apiKey'` + `apiKey` override in client", async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + apiKey: CUSTOM_API_KEY + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + }); + + expectOp({ + op, + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + apiKeyOverride: CUSTOM_API_KEY + }); }); - expectOp({ - op, - endpoint: CUSTOM_ENDPOINT, - authMode: 'apiKey', - apiKeyOverride: CUSTOM_API_KEY, + test("allows `authMode` override in client.graphql()", async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + authMode: 'userPool' + }); + + expectOp({ + op, + endpoint: CUSTOM_ENDPOINT, + authMode: 'userPool', + }); }); - }); - test(`client { endpoint: Y, authMode: Y } + ${opType} { authMode: Y } -> op.authMode`, async () => { - const client = generateClient({ - endpoint: CUSTOM_ENDPOINT, - authMode: 'userPool', + test("requires `apiKey` with `authMode: 'apiKey'` override in client.graphql()", async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + // @ts-expect-error + expect(() => client.graphql({ + query: `${op} A { queryA { a b c } }`, + authMode: 'apiKey' + })).toThrow() }); - await client.graphql({ - query: `${op} A { queryA { a b c } }`, - authMode: 'apiKey', - apiKey: CUSTOM_API_KEY - }) - - expectOp({ - op, - endpoint: CUSTOM_ENDPOINT, - authMode: 'apiKey', - apiKeyOverride: CUSTOM_API_KEY + test("allows `authMode: 'apiKey'` + `apiKey` override in client.graphql()", async () => { + const client = generateClient({ + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + authMode: 'apiKey', + apiKey: CUSTOM_API_KEY + }); + + expectOp({ + op, + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + apiKeyOverride: CUSTOM_API_KEY + }); }); - }); - } + }) + }; + }); - - -// // because the client has an `endpoint` but no `apiKey`, using `apiKey` auth -// // in `.graphql()` also requires `apiKey` to be specified in the call -// // @ts-expect-error -// await client.graphql({ -// query: `${op} A { queryA { a b c } }`, -// authMode: 'apiKey', -// }); \ No newline at end of file From 0242a4d75aa5742c9249f3e91514a9f67f0a9e05 Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Tue, 17 Dec 2024 10:09:51 -0600 Subject: [PATCH 15/16] shoring up SSR; wip --- .../src/api/generateServerClient.ts | 106 ++- .../generateClientWithAmplifyInstance.test.ts | 30 +- .../generateClientWithAmplifyInstance.ts | 26 +- .../api-graphql/src/server/generateClient.ts | 21 +- packages/api/__tests__/API.test.ts | 808 ++++++++++++++++-- packages/api/__tests__/SRR.test.ts | 106 +++ packages/api/package.json | 3 +- packages/api/src/API.ts | 4 + 8 files changed, 945 insertions(+), 159 deletions(-) create mode 100644 packages/api/__tests__/SRR.test.ts diff --git a/packages/adapter-nextjs/src/api/generateServerClient.ts b/packages/adapter-nextjs/src/api/generateServerClient.ts index e1c5ab09816..f99ac60e90b 100644 --- a/packages/adapter-nextjs/src/api/generateServerClient.ts +++ b/packages/adapter-nextjs/src/api/generateServerClient.ts @@ -8,30 +8,34 @@ import { getAmplifyServerContext, } from '@aws-amplify/core/internals/adapter-core'; import { + CommonPublicClientOptions, V6ClientSSRCookies, V6ClientSSRRequest, } from '@aws-amplify/api-graphql'; -import { - GraphQLAuthMode, - parseAmplifyConfig, -} from '@aws-amplify/core/internals/utils'; +import { parseAmplifyConfig } from '@aws-amplify/core/internals/utils'; import { NextServer } from '../types'; import { createServerRunnerForAPI } from './createServerRunnerForAPI'; -interface CookiesClientParams { +type CookiesClientParams< + WithEndpoint extends boolean, + WithApiKey extends boolean, +> = { cookies: NextServer.ServerComponentContext['cookies']; config: NextServer.CreateServerRunnerInput['config']; - authMode?: GraphQLAuthMode; - authToken?: string; -} +} & CommonPublicClientOptions; -interface ReqClientParams { +type ReqClientParams< + WithEndpoint extends boolean, + WithApiKey extends boolean, +> = { config: NextServer.CreateServerRunnerInput['config']; - authMode?: GraphQLAuthMode; - authToken?: string; -} +} & CommonPublicClientOptions; + +// NOTE: The type narrowing on CommonPublicClientOptions seems to hinge on +// defining these signatures separately. Not sure why offhand. This is worth +// some investigation later. /** * Generates an API client that can be used inside a Next.js Server Component with Dynamic Rendering @@ -44,13 +48,30 @@ interface ReqClientParams { */ export function generateServerClientUsingCookies< T extends Record = never, ->({ - config, - cookies, - authMode, - authToken, -}: CookiesClientParams): V6ClientSSRCookies { - if (typeof cookies !== 'function') { +>( + options: CookiesClientParams, +): V6ClientSSRCookies; +export function generateServerClientUsingCookies< + T extends Record = never, +>( + options: CookiesClientParams, +): V6ClientSSRCookies; +export function generateServerClientUsingCookies< + T extends Record = never, +>( + options: CookiesClientParams, +): V6ClientSSRCookies; +export function generateServerClientUsingCookies< + T extends Record = never, +>(options: CookiesClientParams): V6ClientSSRCookies; +export function generateServerClientUsingCookies< + T extends Record = never, + WithCustomEndpoint extends boolean = false, + WithApiKey extends boolean = false, +>( + options: CookiesClientParams, +): V6ClientSSRCookies { + if (typeof options.cookies !== 'function') { throw new AmplifyServerContextError({ message: 'generateServerClientUsingCookies is only compatible with the `cookies` Dynamic Function available in Server Components.', @@ -61,24 +82,30 @@ export function generateServerClientUsingCookies< } const { runWithAmplifyServerContext, resourcesConfig } = - createServerRunnerForAPI({ config }); + createServerRunnerForAPI({ config: options.config }); // This function reference gets passed down to InternalGraphQLAPI.ts.graphql // where this._graphql is passed in as the `fn` argument // causing it to always get invoked inside `runWithAmplifyServerContext` const getAmplify = (fn: (amplify: any) => Promise) => runWithAmplifyServerContext({ - nextServerContext: { cookies }, + nextServerContext: { cookies: options.cookies }, operation: contextSpec => fn(getAmplifyServerContext(contextSpec).amplify), }); - return generateClientWithAmplifyInstance>({ + const { cookies: _cookies, config: _config, ...params } = options; + + return generateClientWithAmplifyInstance< + T, + WithCustomEndpoint, + WithApiKey, + V6ClientSSRCookies + >({ amplify: getAmplify, config: resourcesConfig, - authMode, - authToken, - }); + ...params, + } as any); // TS can't narrow the type here. } /** @@ -99,12 +126,29 @@ export function generateServerClientUsingCookies< */ export function generateServerClientUsingReqRes< T extends Record = never, ->({ config, authMode, authToken }: ReqClientParams): V6ClientSSRRequest { - const amplifyConfig = parseAmplifyConfig(config); +>(options: ReqClientParams): V6ClientSSRRequest; +export function generateServerClientUsingReqRes< + T extends Record = never, +>(options: ReqClientParams): V6ClientSSRRequest; +export function generateServerClientUsingReqRes< + T extends Record = never, +>(options: ReqClientParams): V6ClientSSRRequest; +export function generateServerClientUsingReqRes< + T extends Record = never, +>(options: ReqClientParams): V6ClientSSRRequest; +export function generateServerClientUsingReqRes< + T extends Record = never, + WithCustomEndpoint extends boolean = false, + WithApiKey extends boolean = false, +>( + options: ReqClientParams, +): V6ClientSSRRequest { + const amplifyConfig = parseAmplifyConfig(options.config); + + const { config: _config, ...params } = options; - return generateClient({ + return generateClient({ config: amplifyConfig, - authMode, - authToken, - }); + ...params, + } as any); // TS can't narrow the type here. } diff --git a/packages/api-graphql/__tests__/internals/server/generateClientWithAmplifyInstance.test.ts b/packages/api-graphql/__tests__/internals/server/generateClientWithAmplifyInstance.test.ts index 8af92fad586..704881899a2 100644 --- a/packages/api-graphql/__tests__/internals/server/generateClientWithAmplifyInstance.test.ts +++ b/packages/api-graphql/__tests__/internals/server/generateClientWithAmplifyInstance.test.ts @@ -36,10 +36,7 @@ describe('server generateClient', () => { test('subscriptions are disabled', () => { const getAmplify = async (fn: any) => await fn(Amplify); - const client = generateClientWithAmplifyInstance< - Schema, - V6ClientSSRCookies - >({ + const client = generateClientWithAmplifyInstance({ amplify: getAmplify, config: config, }); @@ -71,10 +68,7 @@ describe('server generateClient', () => { const getAmplify = async (fn: any) => await fn(Amplify); - const client = generateClientWithAmplifyInstance< - Schema, - V6ClientSSRCookies - >({ + const client = generateClientWithAmplifyInstance({ amplify: getAmplify, config: config, }); @@ -118,10 +112,7 @@ describe('server generateClient', () => { const getAmplify = async (fn: any) => await fn(Amplify); - const client = generateClientWithAmplifyInstance< - Schema, - V6ClientSSRCookies - >({ + const client = generateClientWithAmplifyInstance({ amplify: getAmplify, config: config, }); @@ -167,10 +158,7 @@ describe('server generateClient', () => { const getAmplify = async (fn: any) => await fn(Amplify); - const client = generateClientWithAmplifyInstance< - Schema, - V6ClientSSRCookies - >({ + const client = generateClientWithAmplifyInstance({ amplify: getAmplify, config: config, }); @@ -197,10 +185,7 @@ describe('server generateClient', () => { describe('with request', () => { test('subscriptions are disabled', () => { - const client = generateClientWithAmplifyInstance< - Schema, - V6ClientSSRRequest - >({ + const client = generateClientWithAmplifyInstance({ amplify: null, config: config, }); @@ -215,10 +200,7 @@ describe('server generateClient', () => { Amplify.configure(configFixture as any); const config = Amplify.getConfig(); - const client = generateClientWithAmplifyInstance< - Schema, - V6ClientSSRRequest - >({ + const client = generateClientWithAmplifyInstance({ amplify: null, config: config, }); diff --git a/packages/api-graphql/src/internals/server/generateClientWithAmplifyInstance.ts b/packages/api-graphql/src/internals/server/generateClientWithAmplifyInstance.ts index 219a07b570e..8529e9f0f83 100644 --- a/packages/api-graphql/src/internals/server/generateClientWithAmplifyInstance.ts +++ b/packages/api-graphql/src/internals/server/generateClientWithAmplifyInstance.ts @@ -32,18 +32,11 @@ import { cancel, graphql, isCancelError } from '..'; */ export function generateClientWithAmplifyInstance< T extends Record = never, - WithCustomEndpoint extends boolean = false, - WithApiKey extends boolean = false, ClientType extends - | V6ClientSSRRequest - | V6ClientSSRCookies< - T, - WithCustomEndpoint, - WithApiKey - > = V6ClientSSRCookies, + | V6ClientSSRRequest + | V6ClientSSRCookies = V6ClientSSRCookies, >( - params: ServerClientGenerationParams & - CommonPublicClientOptions, + params: ServerClientGenerationParams & CommonPublicClientOptions, ): ClientType { const client = { [__amplify]: params.amplify, @@ -59,6 +52,19 @@ export function generateClientWithAmplifyInstance< const apiGraphqlConfig = params.config?.API?.GraphQL; + if (client[__endpoint]) { + if (!client[__authMode]) { + throw new Error( + 'generateClient() requires an explicit `authMode` when `endpoint` is provided.', + ); + } + if (client[__authMode] === 'apiKey' && !client[__apiKey]) { + throw new Error( + "generateClient() requires an explicit `apiKey` when `endpoint` is provided and `authMode = 'apiKey'`.", + ); + } + } + if (!client[__endpoint] && isApiGraphQLConfig(apiGraphqlConfig)) { addSchemaToClientWithInstance(client, params, getInternals); } diff --git a/packages/api-graphql/src/server/generateClient.ts b/packages/api-graphql/src/server/generateClient.ts index 9ea767635e8..2340a591b2a 100644 --- a/packages/api-graphql/src/server/generateClient.ts +++ b/packages/api-graphql/src/server/generateClient.ts @@ -37,26 +37,17 @@ export function generateClient< T extends Record = never, WithCustomEndpoint extends boolean = false, WithApiKey extends boolean = false, ->({ - config, - authMode, - authToken, -}: GenerateServerClientParams< - WithCustomEndpoint, - WithApiKey ->): V6ClientSSRRequest { +>( + options: GenerateServerClientParams, +): V6ClientSSRRequest { // passing `null` instance because each (future model) method must retrieve a valid instance // from server context const client = generateClientWithAmplifyInstance< T, - any, - any, V6ClientSSRRequest >({ amplify: null, - config, - authMode, - authToken, + ...options, }); // TODO: improve this and the next type @@ -67,14 +58,14 @@ export function generateClient< const wrappedGraphql = ( contextSpec: AmplifyServer.ContextSpec, - options: GraphQLOptionsV6, + innerOptions: GraphQLOptionsV6, additionalHeaders?: CustomHeaders, ) => { const amplifyInstance = getAmplifyServerContext(contextSpec).amplify; return prevGraphql.call( { [__amplify]: amplifyInstance }, - options as any, + innerOptions as any, additionalHeaders as any, ); }; diff --git a/packages/api/__tests__/API.test.ts b/packages/api/__tests__/API.test.ts index add59f9f293..9985c2636b2 100644 --- a/packages/api/__tests__/API.test.ts +++ b/packages/api/__tests__/API.test.ts @@ -1,10 +1,16 @@ +import { enableFetchMocks } from 'jest-fetch-mock'; import { Amplify, ResourcesConfig } from 'aws-amplify'; import { GraphQLAPI } from '@aws-amplify/api-graphql'; import { generateClient, CONNECTION_STATE_CHANGE } from '@aws-amplify/api'; +import { generateServerClientUsingCookies, generateServerClientUsingReqRes } from '@aws-amplify/adapter-nextjs/api'; +import { generateClientWithAmplifyInstance } from '@aws-amplify/api/internals'; import { AmplifyClassV6 } from '@aws-amplify/core'; import { Observable } from 'rxjs'; import { decodeJWT } from '@aws-amplify/core'; +// Make global `Request` available. (Necessary for using `adapter-nextjs` clients.) +enableFetchMocks(); + type AuthMode = | 'apiKey' | 'oidc' @@ -132,8 +138,8 @@ function expectOp({ apiKeyOverride?: string | undefined; authTokenOverride?: string | undefined; }) { - const expecto = op === 'subscription' ? expectSubscription : expectPost; - expecto({ endpoint, authMode, apiKeyOverride, authTokenOverride }); // test pass ... umm ... + const doExpect = op === 'subscription' ? expectSubscription : expectPost; + doExpect({ endpoint, authMode, apiKeyOverride, authTokenOverride }); // test pass ... umm ... } function prepareMocks() { @@ -193,76 +199,7 @@ function prepareMocks() { _subspy.mockReturnValue(new Observable()); } -describe.skip('API generateClient', () => { - afterEach(() => { - jest.clearAllMocks(); - }); - - test('client-side client.graphql', async () => { - jest.spyOn(AmplifyClassV6.prototype, 'getConfig').mockImplementation(() => { - return { - API: { GraphQL: { endpoint: 'test', defaultAuthMode: 'none' } }, - }; - }); - const spy = jest - .spyOn(GraphQLAPI, 'graphql') - .mockResolvedValue('grapqhqlResponse' as any); - const client = generateClient(); - expect(await client.graphql({ query: 'query' })).toBe('grapqhqlResponse'); - expect(spy).toHaveBeenCalledWith( - { Auth: {}, libraryOptions: {}, resourcesConfig: {} }, - { query: 'query' }, - undefined, - { - action: '1', - category: 'api', - }, - ); - }); - - test('CONNECTION_STATE_CHANGE importable as a value, not a type', async () => { - expect(CONNECTION_STATE_CHANGE).toBe('ConnectionStateChange'); - }); - // test('server-side client.graphql', async () => { - // const config: ResourcesConfig = { - // API: { - // GraphQL: { - // apiKey: 'adsf', - // customEndpoint: undefined, - // customEndpointRegion: undefined, - // defaultAuthMode: 'apiKey', - // endpoint: 'https://0.0.0.0/graphql', - // region: 'us-east-1', - // }, - // }, - // }; - - // const query = `query Q { - // getWidget { - // __typename id owner createdAt updatedAt someField - // } - // }`; - - // const spy = jest - // .spyOn(InternalGraphQLAPIClass.prototype, 'graphql') - // .mockResolvedValue('grapqhqlResponse' as any); - - // await runWithAmplifyServerContext(config, {}, ctx => { - // const client = generateClientSSR(ctx); - // return client.graphql({ query }) as any; - // }); - - // expect(spy).toHaveBeenCalledWith( - // expect.objectContaining({ - // resourcesConfig: config, - // }), - // { query }, - // undefined - // ); - // }); -}); - -describe.only('generateClient', () => { +describe('generateClient (web)', () => { beforeEach(() => { prepareMocks() }); @@ -276,7 +213,7 @@ describe.only('generateClient', () => { describe(`[${opType}] without a custom endpoint`, () => { test("does not require `authMode` or `apiKey` override", () => { - expect(() => generateClient()).not.toThrow(); + expect(() => { generateClient() }).not.toThrow(); }); test("does not require `authMode` or `apiKey` override in client.graphql()", async () => { @@ -367,11 +304,13 @@ describe.only('generateClient', () => { }) test("requires `apiKey` with `authMode: 'apiKey'` override in client", async () => { - expect(() => generateClient({ - endpoint: CUSTOM_ENDPOINT, - // @ts-ignore - authMode: 'apiKey', - })).toThrow(); + expect(() => { + generateClient({ + endpoint: CUSTOM_ENDPOINT, + // @ts-expect-error + authMode: 'apiKey', + }) + }).toThrow(); }); test("allows `authMode` override in client", async () => { @@ -479,5 +418,718 @@ describe.only('generateClient', () => { }); }) }; +}); + +describe('generateClient (cookie client)', () => { + + /** + * NOTICE + * + * Cookie client is largely a pass-thru to `generateClientWithAmplifyInstance`. + * + * These tests intend to cover narrowing rules on the public surface. Behavior is + * tested in the `SSR common` describe block. + */ + beforeEach(() => { + prepareMocks(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + const cookies = () => ({ + get() { return undefined }, + getAll() { return [] }, + has() { return false }, + }) as any; + + describe('typings', () => { + /** + * Static / Type tests only. + * + * (No executed intended or expected.) + */ + + describe('without a custom endpoint', () => { + test("do not require `authMode` or `apiKey` override", () => { + // expect no type error + () => generateServerClientUsingCookies({ + config: Amplify.getConfig(), + cookies + }); + }); + + test("do not require `authMode` or `apiKey` override in client.graphql()", () => { + async () => { + const client = generateServerClientUsingCookies({ + config: Amplify.getConfig(), + cookies + }); + await client.graphql({ query: `query A { queryA { a b c } }` }); + } + }); + + test("allows `authMode` override in client", () => { + async () => { + const client = generateServerClientUsingCookies({ + config: Amplify.getConfig(), + cookies, + authMode: 'userPool', + }); + + await client.graphql({ + query: `query A { queryA { a b c } }`, + }); + } + }); + + test("allow `authMode` override in `client.graphql()`", () => { + async () => { + const client = generateServerClientUsingCookies({ + config: Amplify.getConfig(), + cookies, + }); + + await client.graphql({ + query: `query A { queryA { a b c } }`, + authMode: 'userPool', + }); + } + }); + + test("allows `apiKey` override in `client.graphql()`", () => { + async () => { + const client = generateServerClientUsingCookies({ + config: Amplify.getConfig(), + cookies, + }); + + await client.graphql({ + query: `query A { queryA { a b c } }`, + apiKey: CUSTOM_API_KEY, + }); + } + }); + + test("allows `authMode` + `apiKey` override in `client.graphql()`", () => { + async () => { + const client = generateServerClientUsingCookies({ + config: Amplify.getConfig(), + cookies, + authMode: 'userPool' + }); + + await client.graphql({ + query: `query A { queryA { a b c } }`, + authMode: 'apiKey', + apiKey: CUSTOM_API_KEY, + }); + } + }); + }) + + describe('with a custom endpoint', () => { + test("requires `authMode` override", () => { + // @ts-expect-error + () => generateServerClientUsingCookies({ + config: Amplify.getConfig(), + cookies, + endpoint: CUSTOM_ENDPOINT + }); + }) + + test("requires `apiKey` with `authMode: 'apiKey'` override in client", () => { + // @ts-expect-error + () => generateServerClientUsingCookies({ + config: Amplify.getConfig(), + cookies, + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + }); + }); + + test("allows `authMode` override in client", () => { + async () => { + const client = generateServerClientUsingCookies({ + config: Amplify.getConfig(), + cookies, + endpoint: CUSTOM_ENDPOINT, + authMode: 'userPool', + }); + + await client.graphql({ + query: `query A { queryA { a b c } }`, + }); + } + }); + + test("allows `authMode: 'none'` override in client.graphql()", () => { + async () => { + const client = generateServerClientUsingCookies({ + config: Amplify.getConfig(), + cookies, + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + await client.graphql({ + query: `query A { queryA { a b c } }`, + }); + } + }); + + test("allows `authMode: 'apiKey'` + `apiKey` override in client", () => { + async () => { + const client = generateServerClientUsingCookies({ + config: Amplify.getConfig(), + cookies, + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + apiKey: CUSTOM_API_KEY + }); + + await client.graphql({ + query: `query A { queryA { a b c } }`, + }); + } + }); + + test("allows `authMode` override in client.graphql()", () => { + async () => { + const client = generateServerClientUsingCookies({ + config: Amplify.getConfig(), + cookies, + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + await client.graphql({ + query: `query A { queryA { a b c } }`, + authMode: 'userPool' + }); + } + }); + + test("requires `apiKey` with `authMode: 'apiKey'` override in client.graphql()", () => { + async () => { + const client = generateServerClientUsingCookies({ + config: Amplify.getConfig(), + cookies, + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + // @ts-expect-error + await client.graphql({ + query: `query A { queryA { a b c } }`, + authMode: 'apiKey' + }); + } + }); + + test("allows `authMode: 'apiKey'` + `apiKey` override in client.graphql()", () => { + async () => { + const client = generateServerClientUsingCookies({ + config: Amplify.getConfig(), + cookies, + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + await client.graphql({ + query: `query A { queryA { a b c } }`, + authMode: 'apiKey', + apiKey: CUSTOM_API_KEY + }); + } + }); + }) + + }); }); + +describe('generateClient (req/res client)', () => { + + /** + * NOTICE + * + * ReqRes client is largely a pass-thru to `server/generateClient`, which is a pass-thru + * to `generateClientWithAmplifyInstance` (with add Amplify instance). + * + * These tests intend to cover narrowing rules on the public surface. Behavior is + * tested in the `SSR common` describe block. + */ + + beforeEach(() => { + prepareMocks(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + const cookies = () => ({ + get() { return undefined }, + getAll() { return [] }, + has() { return false }, + }) as any; + + const contextSpec = {} as any; + + describe('typings', () => { + /** + * Static / Type tests only. + * + * (No executed intended or expected.) + */ + + describe('without a custom endpoint', () => { + test("do not require `authMode` or `apiKey` override", () => { + // expect no type error + () => generateServerClientUsingReqRes({ + config: Amplify.getConfig(), + }); + }); + + test("do not require `authMode` or `apiKey` override in client.graphql()", () => { + async () => { + const client = generateServerClientUsingReqRes({ + config: Amplify.getConfig(), + }); + await client.graphql(contextSpec, { query: `query A { queryA { a b c } }` }); + } + }); + + test("allows `authMode` override in client", () => { + async () => { + const client = generateServerClientUsingReqRes({ + config: Amplify.getConfig(), + authMode: 'userPool', + }); + + await client.graphql(contextSpec, { + query: `query A { queryA { a b c } }`, + }); + } + }); + + test("allow `authMode` override in `client.graphql()`", () => { + async () => { + const client = generateServerClientUsingReqRes({ + config: Amplify.getConfig(), + }); + + await client.graphql(contextSpec, { + query: `query A { queryA { a b c } }`, + authMode: 'userPool', + }); + } + }); + + test("allows `apiKey` override in `client.graphql()`", () => { + async () => { + const client = generateServerClientUsingReqRes({ + config: Amplify.getConfig(), + }); + + await client.graphql(contextSpec, { + query: `query A { queryA { a b c } }`, + apiKey: CUSTOM_API_KEY, + }); + } + }); + + test("allows `authMode` + `apiKey` override in `client.graphql()`", () => { + async () => { + const client = generateServerClientUsingReqRes({ + config: Amplify.getConfig(), + authMode: 'userPool' + }); + + await client.graphql(contextSpec, { + query: `query A { queryA { a b c } }`, + authMode: 'apiKey', + apiKey: CUSTOM_API_KEY, + }); + } + }); + }) + + describe('with a custom endpoint', () => { + test("requires `authMode` override", () => { + // @ts-expect-error + () => generateServerClientUsingReqRes({ + config: Amplify.getConfig(), + endpoint: CUSTOM_ENDPOINT + }); + }) + + test("requires `apiKey` with `authMode: 'apiKey'` override in client", () => { + // @ts-expect-error + () => generateServerClientUsingReqRes({ + config: Amplify.getConfig(), + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + }); + }); + + test("allows `authMode` override in client", () => { + async () => { + const client = generateServerClientUsingReqRes({ + config: Amplify.getConfig(), + endpoint: CUSTOM_ENDPOINT, + authMode: 'userPool', + }); + + await client.graphql(contextSpec, { + query: `query A { queryA { a b c } }`, + }); + } + }); + + test("allows `authMode: 'none'` override in client.graphql()", () => { + async () => { + const client = generateServerClientUsingReqRes({ + config: Amplify.getConfig(), + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + await client.graphql(contextSpec, { + query: `query A { queryA { a b c } }`, + }); + } + }); + + test("allows `authMode: 'apiKey'` + `apiKey` override in client", () => { + async () => { + const client = generateServerClientUsingReqRes({ + config: Amplify.getConfig(), + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + apiKey: CUSTOM_API_KEY + }); + + await client.graphql(contextSpec, { + query: `query A { queryA { a b c } }`, + }); + } + }); + + test("allows `authMode` override in client.graphql()", () => { + async () => { + const client = generateServerClientUsingReqRes({ + config: Amplify.getConfig(), + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + await client.graphql(contextSpec, { + query: `query A { queryA { a b c } }`, + authMode: 'userPool' + }); + } + }); + + test("requires `apiKey` with `authMode: 'apiKey'` override in client.graphql()", () => { + async () => { + const client = generateServerClientUsingReqRes({ + config: Amplify.getConfig(), + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + // @ts-expect-error + await client.graphql(contextSpec, { + query: `query A { queryA { a b c } }`, + authMode: 'apiKey' + }); + } + }); + + test("allows `authMode: 'apiKey'` + `apiKey` override in client.graphql()", () => { + async () => { + const client = generateServerClientUsingReqRes({ + config: Amplify.getConfig(), + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + await client.graphql(contextSpec, { + query: `query A { queryA { a b c } }`, + authMode: 'apiKey', + apiKey: CUSTOM_API_KEY + }); + } + }); + }) + + }); +}); + +describe.skip('SSR common', () => { + /** + * NOTICE + * + * This tests the runtime validation behavior common to both SSR clients. + * + * 1. Cookie client uses `generateClientWithAmplifyInstance` directly. + * 2. ReqRest client uses `server/generateClient`. + * 3. `server/generateClient` is a pass-thru to `generateClientWithAmplifyInstance` that + * injects an `Amplify` instance. + * + * The runtime validations we need to check funnel through `generateClientWithAmplifyInstance`. + */ + + beforeEach(() => { + prepareMocks(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + for (const op of ['query', 'subscription'] as const) { + const opType = op === 'subscription' ? 'sub' : 'qry'; + + describe(`[${opType}] without a custom endpoint`, () => { + test("does not require `authMode` or `apiKey` override", () => { + expect(() => generateClientWithAmplifyInstance({ + amplify: async () => Amplify, + config: Amplify.getConfig(), + })).not.toThrow(); + }); + + test("does not require `authMode` or `apiKey` override in client.graphql()", async () => { + const client = generateClientWithAmplifyInstance({ + amplify: async () => Amplify, + config: Amplify.getConfig(), + }); + + await client.graphql({ query: `${op} A { queryA { a b c } }` }); + + expectOp({ + op, + endpoint: DEFAULT_ENDPOINT, + authMode: DEFAULT_AUTH_MODE, + }); + }); + + test("allows `authMode` override in client", async () => { + const client = generateClientWithAmplifyInstance({ + amplify: async () => Amplify, + config: Amplify.getConfig(), + authMode: 'userPool', + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + }); + + expectOp({ + op, + endpoint: DEFAULT_ENDPOINT, + authMode: 'userPool', + }); + }); + + test("allows `authMode` override in `client.graphql()`", async () => { + const client = generateClientWithAmplifyInstance({ + amplify: async () => Amplify, + config: Amplify.getConfig(), + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + authMode: 'userPool', + }); + + expectOp({ + op, + endpoint: DEFAULT_ENDPOINT, + authMode: 'userPool', + }); + }); + + test("allows `apiKey` override in `client.graphql()`", async () => { + const client = generateClientWithAmplifyInstance({ + amplify: async () => Amplify, + config: Amplify.getConfig(), + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + apiKey: CUSTOM_API_KEY, + }); + + expectOp({ + op, + endpoint: DEFAULT_ENDPOINT, + authMode: 'apiKey', + apiKeyOverride: CUSTOM_API_KEY + }); + }); + + test("allows `authMode` + `apiKey` override in `client.graphql()`", async () => { + const client = generateClientWithAmplifyInstance({ + amplify: async () => Amplify, + config: Amplify.getConfig(), + authMode: 'userPool' + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + authMode: 'apiKey', + apiKey: CUSTOM_API_KEY, + }); + + expectOp({ + op, + endpoint: DEFAULT_ENDPOINT, + authMode: 'apiKey', + apiKeyOverride: CUSTOM_API_KEY + }); + }); + }); + + describe.skip(`[${opType}] with a custom endpoint`, () => { + test("requires `authMode` override", () => { + // @ts-expect-error + expect(() => generateClientWithAmplifyInstance({ + amplify: async () => Amplify, + config: Amplify.getConfig(), + endpoint: CUSTOM_ENDPOINT + })).toThrow() + }) + + test("requires `apiKey` with `authMode: 'apiKey'` override in client", async () => { + // @ts-expect-error + expect(() => generateClientWithAmplifyInstance({ + amplify: async () => Amplify, + config: Amplify.getConfig(), + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + })).toThrow(); + }); + + test("allows `authMode` override in client", async () => { + const client = generateClientWithAmplifyInstance({ + amplify: async () => Amplify, + config: Amplify.getConfig(), + endpoint: CUSTOM_ENDPOINT, + authMode: 'userPool', + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + }); + + expectOp({ + op, + endpoint: CUSTOM_ENDPOINT, + authMode: 'userPool', + }); + }); + + test("allows `authMode: 'none'` override in client.graphql()", async () => { + const client = generateClientWithAmplifyInstance({ + amplify: async () => Amplify, + config: Amplify.getConfig(), + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + }); + + expectOp({ + op, + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + }); + + test("allows `authMode: 'apiKey'` + `apiKey` override in client", async () => { + const client = generateClientWithAmplifyInstance({ + amplify: async () => Amplify, + config: Amplify.getConfig(), + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + apiKey: CUSTOM_API_KEY + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + }); + + expectOp({ + op, + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + apiKeyOverride: CUSTOM_API_KEY + }); + }); + + test("allows `authMode` override in client.graphql()", async () => { + const client = generateClientWithAmplifyInstance({ + amplify: async () => Amplify, + config: Amplify.getConfig(), + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + authMode: 'userPool' + }); + + expectOp({ + op, + endpoint: CUSTOM_ENDPOINT, + authMode: 'userPool', + }); + }); + + test("requires `apiKey` with `authMode: 'apiKey'` override in client.graphql()", async () => { + // no TS expect error here. types for `generateClientWithAmplifyInstance` have been simplified + // because they are not customer-facing. + const client = generateClientWithAmplifyInstance({ + amplify: async () => Amplify, + config: Amplify.getConfig(), + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + expect(() => client.graphql({ + query: `${op} A { queryA { a b c } }`, + authMode: 'apiKey' + })).toThrow() + }); + + test("allows `authMode: 'apiKey'` + `apiKey` override in client.graphql()", async () => { + const client = generateClientWithAmplifyInstance({ + amplify: async () => Amplify, + config: Amplify.getConfig(), + endpoint: CUSTOM_ENDPOINT, + authMode: 'none', + }); + + await client.graphql({ + query: `${op} A { queryA { a b c } }`, + authMode: 'apiKey', + apiKey: CUSTOM_API_KEY + }); + + expectOp({ + op, + endpoint: CUSTOM_ENDPOINT, + authMode: 'apiKey', + apiKeyOverride: CUSTOM_API_KEY + }); + }); + }) + }; +}) diff --git a/packages/api/__tests__/SRR.test.ts b/packages/api/__tests__/SRR.test.ts new file mode 100644 index 00000000000..5b1b79d8dff --- /dev/null +++ b/packages/api/__tests__/SRR.test.ts @@ -0,0 +1,106 @@ +import { enableFetchMocks } from 'jest-fetch-mock'; +import { Amplify, ResourcesConfig } from 'aws-amplify'; + +// allows SSR function to be invoked without catastrophically failing out of the gate. +enableFetchMocks(); + +const generateClientWithAmplifyInstanceSpy = jest.fn(); +jest.mock('@aws-amplify/api/internals', () => ({ + generateClientWithAmplifyInstance: generateClientWithAmplifyInstanceSpy +})); + +const generateClientSpy = jest.fn(); +jest.mock('aws-amplify/api/server', () => ({ + generateClient: generateClientSpy +})); + +const { + generateServerClientUsingCookies, + generateServerClientUsingReqRes, +} = require('@aws-amplify/adapter-nextjs/api'); + +describe('SSR internals', () => { + beforeEach(() => { + Amplify.configure( + { + API: { + GraphQL: { + defaultAuthMode: 'apiKey', + apiKey: 'a-key', + endpoint: 'https://an-endpoint.local/graphql', + region: 'north-pole-7', + }, + }, + Auth: { + Cognito: { + userPoolId: 'north-pole-7:santas-little-helpers', + identityPoolId: 'north-pole-7:santas-average-sized-helpers', + userPoolClientId: 'the-mrs-claus-oversight-committee', + }, + }, + } + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + const cookies = () => ({ + get() { return undefined }, + getAll() { return [] }, + has() { return false }, + }) as any; + + test('generateServerClientUsingCookies passes through to generateClientWithAmplifyInstance', () => { + generateClientWithAmplifyInstanceSpy.mockReturnValue('generateClientWithAmplifyInstance client'); + + const options = { + config: Amplify.getConfig(), + cookies: cookies, // must be a function to internal sanity checks + authMode: "authMode value", + authToken: "authToken value", + apiKey: "apiKey value", + endpoint: "endpoint value", + headers: "headers value" + } as any; + + const { + config: _config, // config is replaced with resources config + cookies: _cookies, // cookies are not sent + ...params + } = options; + + const client = generateServerClientUsingCookies(options); + + expect(generateClientWithAmplifyInstanceSpy).toHaveBeenCalledWith( + expect.objectContaining(params) + ); + expect(client).toEqual('generateClientWithAmplifyInstance client'); + }); + + test('generateServerClientUsingReqRes passes through to generateClientSpy', () => { + generateClientSpy.mockReturnValue('generateClientSpy client'); + + const options = { + config: Amplify.getConfig(), + authMode: "authMode value", + authToken: "authToken value", + apiKey: "apiKey value", + endpoint: "endpoint value", + headers: "headers value" + } as any; + + const { + config: _config, // config is replaced with resources config + ...params + } = options; + + const client = generateServerClientUsingReqRes(options); + + expect(generateClientSpy).toHaveBeenCalledWith( + expect.objectContaining(params) + ); + expect(client).toEqual('generateClientSpy client'); + }); +}) \ No newline at end of file diff --git a/packages/api/package.json b/packages/api/package.json index 5fe2429ee6f..173406972b0 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -68,7 +68,8 @@ }, "homepage": "https://aws-amplify.github.io/", "devDependencies": { - "typescript": "5.0.2" + "typescript": "5.0.2", + "jest-fetch-mock": "3.0.3" }, "files": [ "dist/cjs", diff --git a/packages/api/src/API.ts b/packages/api/src/API.ts index bcf338a98bd..e8a13aadd44 100644 --- a/packages/api/src/API.ts +++ b/packages/api/src/API.ts @@ -4,6 +4,10 @@ import { CommonPublicClientOptions, V6Client } from '@aws-amplify/api-graphql'; import { generateClient as internalGenerateClient } from '@aws-amplify/api-graphql/internals'; import { Amplify } from '@aws-amplify/core'; +// NOTE: The type narrowing on CommonPublicClientOptions seems to hinge on +// defining these signatures separately. Not sure why offhand. This is worth +// some investigation later. + /** * Generates an API client that can work with models or raw GraphQL * From 841ea14307f8c7ec99de1e503832a7d8b2033729 Mon Sep 17 00:00:00 2001 From: Jon Wire Date: Tue, 17 Dec 2024 11:31:59 -0600 Subject: [PATCH 16/16] patched up SSR tests; small SSR types fix --- .../src/api/generateServerClient.ts | 2 - packages/api/__tests__/API.test.ts | 37 +++++++++---------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/packages/adapter-nextjs/src/api/generateServerClient.ts b/packages/adapter-nextjs/src/api/generateServerClient.ts index f99ac60e90b..fe44db35cda 100644 --- a/packages/adapter-nextjs/src/api/generateServerClient.ts +++ b/packages/adapter-nextjs/src/api/generateServerClient.ts @@ -98,8 +98,6 @@ export function generateServerClientUsingCookies< return generateClientWithAmplifyInstance< T, - WithCustomEndpoint, - WithApiKey, V6ClientSSRCookies >({ amplify: getAmplify, diff --git a/packages/api/__tests__/API.test.ts b/packages/api/__tests__/API.test.ts index 9985c2636b2..4ae7e18168c 100644 --- a/packages/api/__tests__/API.test.ts +++ b/packages/api/__tests__/API.test.ts @@ -1,10 +1,9 @@ import { enableFetchMocks } from 'jest-fetch-mock'; -import { Amplify, ResourcesConfig } from 'aws-amplify'; +import { Amplify } from '@aws-amplify/core'; import { GraphQLAPI } from '@aws-amplify/api-graphql'; import { generateClient, CONNECTION_STATE_CHANGE } from '@aws-amplify/api'; import { generateServerClientUsingCookies, generateServerClientUsingReqRes } from '@aws-amplify/adapter-nextjs/api'; import { generateClientWithAmplifyInstance } from '@aws-amplify/api/internals'; -import { AmplifyClassV6 } from '@aws-amplify/core'; import { Observable } from 'rxjs'; import { decodeJWT } from '@aws-amplify/core'; @@ -869,7 +868,7 @@ describe('generateClient (req/res client)', () => { }); }); -describe.skip('SSR common', () => { +describe('SSR common', () => { /** * NOTICE * @@ -897,14 +896,14 @@ describe.skip('SSR common', () => { describe(`[${opType}] without a custom endpoint`, () => { test("does not require `authMode` or `apiKey` override", () => { expect(() => generateClientWithAmplifyInstance({ - amplify: async () => Amplify, + amplify: Amplify as any, config: Amplify.getConfig(), })).not.toThrow(); }); test("does not require `authMode` or `apiKey` override in client.graphql()", async () => { const client = generateClientWithAmplifyInstance({ - amplify: async () => Amplify, + amplify: Amplify as any, config: Amplify.getConfig(), }); @@ -919,7 +918,7 @@ describe.skip('SSR common', () => { test("allows `authMode` override in client", async () => { const client = generateClientWithAmplifyInstance({ - amplify: async () => Amplify, + amplify: Amplify as any, config: Amplify.getConfig(), authMode: 'userPool', }); @@ -937,7 +936,7 @@ describe.skip('SSR common', () => { test("allows `authMode` override in `client.graphql()`", async () => { const client = generateClientWithAmplifyInstance({ - amplify: async () => Amplify, + amplify: Amplify as any, config: Amplify.getConfig(), }); @@ -955,7 +954,7 @@ describe.skip('SSR common', () => { test("allows `apiKey` override in `client.graphql()`", async () => { const client = generateClientWithAmplifyInstance({ - amplify: async () => Amplify, + amplify: Amplify as any, config: Amplify.getConfig(), }); @@ -974,7 +973,7 @@ describe.skip('SSR common', () => { test("allows `authMode` + `apiKey` override in `client.graphql()`", async () => { const client = generateClientWithAmplifyInstance({ - amplify: async () => Amplify, + amplify: Amplify as any, config: Amplify.getConfig(), authMode: 'userPool' }); @@ -994,11 +993,11 @@ describe.skip('SSR common', () => { }); }); - describe.skip(`[${opType}] with a custom endpoint`, () => { + describe(`[${opType}] with a custom endpoint`, () => { test("requires `authMode` override", () => { // @ts-expect-error expect(() => generateClientWithAmplifyInstance({ - amplify: async () => Amplify, + amplify: Amplify as any, config: Amplify.getConfig(), endpoint: CUSTOM_ENDPOINT })).toThrow() @@ -1007,7 +1006,7 @@ describe.skip('SSR common', () => { test("requires `apiKey` with `authMode: 'apiKey'` override in client", async () => { // @ts-expect-error expect(() => generateClientWithAmplifyInstance({ - amplify: async () => Amplify, + amplify: Amplify as any, config: Amplify.getConfig(), endpoint: CUSTOM_ENDPOINT, authMode: 'apiKey', @@ -1016,7 +1015,7 @@ describe.skip('SSR common', () => { test("allows `authMode` override in client", async () => { const client = generateClientWithAmplifyInstance({ - amplify: async () => Amplify, + amplify: Amplify as any, config: Amplify.getConfig(), endpoint: CUSTOM_ENDPOINT, authMode: 'userPool', @@ -1035,7 +1034,7 @@ describe.skip('SSR common', () => { test("allows `authMode: 'none'` override in client.graphql()", async () => { const client = generateClientWithAmplifyInstance({ - amplify: async () => Amplify, + amplify: Amplify as any, config: Amplify.getConfig(), endpoint: CUSTOM_ENDPOINT, authMode: 'none', @@ -1054,7 +1053,7 @@ describe.skip('SSR common', () => { test("allows `authMode: 'apiKey'` + `apiKey` override in client", async () => { const client = generateClientWithAmplifyInstance({ - amplify: async () => Amplify, + amplify: Amplify as any, config: Amplify.getConfig(), endpoint: CUSTOM_ENDPOINT, authMode: 'apiKey', @@ -1075,8 +1074,8 @@ describe.skip('SSR common', () => { test("allows `authMode` override in client.graphql()", async () => { const client = generateClientWithAmplifyInstance({ - amplify: async () => Amplify, - config: Amplify.getConfig(), + amplify: Amplify as any, + config: {}, endpoint: CUSTOM_ENDPOINT, authMode: 'none', }); @@ -1097,7 +1096,7 @@ describe.skip('SSR common', () => { // no TS expect error here. types for `generateClientWithAmplifyInstance` have been simplified // because they are not customer-facing. const client = generateClientWithAmplifyInstance({ - amplify: async () => Amplify, + amplify: Amplify as any, config: Amplify.getConfig(), endpoint: CUSTOM_ENDPOINT, authMode: 'none', @@ -1111,7 +1110,7 @@ describe.skip('SSR common', () => { test("allows `authMode: 'apiKey'` + `apiKey` override in client.graphql()", async () => { const client = generateClientWithAmplifyInstance({ - amplify: async () => Amplify, + amplify: Amplify as any, config: Amplify.getConfig(), endpoint: CUSTOM_ENDPOINT, authMode: 'none',