diff --git a/packages/api-graphql/__tests__/events.test.ts b/packages/api-graphql/__tests__/events.test.ts index 767e765b34a..f2fdb5b9075 100644 --- a/packages/api-graphql/__tests__/events.test.ts +++ b/packages/api-graphql/__tests__/events.test.ts @@ -1,78 +1,204 @@ -import { Subscription } from 'rxjs'; import { Amplify } from '@aws-amplify/core'; -import { MESSAGE_TYPES } from '../src/Providers/constants'; -import * as constants from '../src/Providers/constants'; - -import { - delay, - FakeWebSocketInterface, - replaceConstant, -} from '../__tests__/helpers'; -import { ConnectionState as CS } from '../src/types/PubSub'; - -import { AWSAppSyncEventProvider } from '../src/Providers/AWSAppSyncEventsProvider'; +import { AppSyncEventProvider } from '../src/Providers/AWSAppSyncEventsProvider'; import { events } from '../src/'; +import { appsyncRequest } from '../src/internals/events/appsyncRequest'; import { GraphQLAuthMode } from '@aws-amplify/core/internals/utils'; -/** - * TODO: - * 1. gen2 config - * 2. manual config - * 3. all auth modes - * 4. ensure auth works as expected for all modes/locations - */ - -test('no configure()', async () => { - await expect(events.connect('/')).rejects.toThrow( - 'Amplify configuration is missing. Have you called Amplify.configure()?', - ); +const abortController = new AbortController(); + +var mockSubscribeObservable: any; + +jest.mock('../src/Providers/AWSAppSyncEventsProvider', () => { + mockSubscribeObservable = jest.fn(() => ({ + subscribe: jest.fn(), + })); + + return { + AppSyncEventProvider: { + connect: jest.fn(), + subscribe: jest.fn(mockSubscribeObservable), + publish: jest.fn(), + close: jest.fn(), + }, + }; +}); + +jest.mock('../src/internals/events/appsyncRequest', () => { + return { + appsyncRequest: jest.fn().mockResolvedValue({}), + }; }); -describe('Events Client', () => { - beforeEach(() => { - Amplify.configure({ - custom: { - events: { - url: 'https://not-a-real.ddpg-api.us-west-2.amazonaws.com/event', - aws_region: 'us-west-2', - default_authorization_type: 'API_KEY', - api_key: 'da2-abcxyz321', +describe('Events', () => { + afterAll(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + }); + + describe('config', () => { + test('no configure()', async () => { + await expect(events.connect('/')).rejects.toThrow( + 'Amplify configuration is missing. Have you called Amplify.configure()?', + ); + }); + + test('manual (resource config)', async () => { + Amplify.configure({ + API: { + Events: { + endpoint: + 'https://not-a-real.ddpg-api.us-west-2.amazonaws.com/event', + region: 'us-west-2', + defaultAuthMode: 'apiKey', + apiKey: 'da2-abcxyz321', + }, + }, + }); + + await expect(events.connect('/')).resolves.not.toThrow(); + }); + + test('outputs (amplify-backend config)', async () => { + Amplify.configure({ + custom: { + events: { + url: 'https://not-a-real.ddpg-api.us-west-2.amazonaws.com/event', + aws_region: 'us-west-2', + default_authorization_type: 'API_KEY', + api_key: 'da2-abcxyz321', + }, }, - }, - version: '1.2', + version: '1.2', + }); + + await expect(events.connect('/')).resolves.not.toThrow(); }); }); - const authModes: GraphQLAuthMode[] = [ - 'apiKey', - 'userPool', - 'oidc', - 'iam', - 'lambda', - 'none', - ]; - - describe('channel', () => { - test('happy connect', async () => { - const channel = await events.connect('/'); - - expect(channel.subscribe).toBeInstanceOf(Function); - expect(channel.close).toBeInstanceOf(Function); + describe('client', () => { + beforeEach(() => { + Amplify.configure({ + API: { + Events: { + endpoint: + 'https://not-a-real.ddpg-api.us-west-2.amazonaws.com/event', + region: 'us-west-2', + defaultAuthMode: 'apiKey', + apiKey: 'da2-abcxyz321', + }, + }, + }); }); - describe('auth modes', () => { - let provider: typeof AWSAppSyncEventProvider; - let providerSpy: any; + const authModes: GraphQLAuthMode[] = [ + 'apiKey', + 'userPool', + 'oidc', + 'iam', + 'lambda', + 'none', + ]; + + describe('channel', () => { + test('happy connect', async () => { + const channel = await events.connect('/'); + + expect(channel.subscribe).toBeInstanceOf(Function); + expect(channel.close).toBeInstanceOf(Function); + }); + + describe('auth modes', () => { + let mockProvider: typeof AppSyncEventProvider; + + beforeEach(() => { + mockProvider = AppSyncEventProvider; + }); + + for (const authMode of authModes) { + test(`auth override: ${authMode}`, async () => { + await events.connect('/', { authMode }); + + expect(mockProvider.connect).toHaveBeenCalledWith( + expect.objectContaining({ authenticationType: authMode }), + ); + }); + } + }); + }); + + describe('subscribe', () => { + test('happy subscribe', async () => { + const channel = await events.connect('/'); + + channel.subscribe({ + next: data => void data, + error: error => void error, + }); + }); + + describe('auth modes', () => { + let mockProvider: typeof AppSyncEventProvider; + + beforeEach(() => { + mockProvider = AppSyncEventProvider; + }); + + for (const authMode of authModes) { + test(`auth override: ${authMode}`, async () => { + const channel = await events.connect('/'); + + channel.subscribe( + { + next: data => void data, + error: error => void error, + }, + { authMode }, + ); + + expect(mockSubscribeObservable).toHaveBeenCalledWith( + expect.objectContaining({ authenticationType: authMode }), + ); + }); + } + }); + }); + + describe('post', () => { + let mockReq: typeof appsyncRequest; beforeEach(() => { - provider = new AWSAppSyncEventProvider(); - providerSpy = jest.spyOn(provider, 'connect'); + mockReq = appsyncRequest; + }); + + test('happy post', async () => { + await events.post('/', { test: 'data' }); + + expect(mockReq).toHaveBeenCalledWith( + Amplify, + expect.objectContaining({ + query: '/', + variables: ['{"test":"data"}'], + }), + {}, + abortController, + ); }); for (const authMode of authModes) { test(`auth override: ${authMode}`, async () => { - await events.connect('/', { authMode }); + await events.post('/', { test: 'data' }, { authMode }); + + expect(mockReq).toHaveBeenCalledWith( + Amplify, + expect.objectContaining({ + query: '/', + variables: ['{"test":"data"}'], + authenticationType: authMode, + }), + {}, + abortController, + ); }); } }); diff --git a/packages/api-graphql/src/Providers/AWSAppSyncEventsProvider/index.ts b/packages/api-graphql/src/Providers/AWSAppSyncEventsProvider/index.ts index 612d5d2faf1..af66fc6c564 100644 --- a/packages/api-graphql/src/Providers/AWSAppSyncEventsProvider/index.ts +++ b/packages/api-graphql/src/Providers/AWSAppSyncEventsProvider/index.ts @@ -12,6 +12,7 @@ import { CustomHeaders } from '@aws-amplify/data-schema/runtime'; import { MESSAGE_TYPES } from '../constants'; import { AWSWebSocketProvider } from '../AWSWebSocketProvider'; import { awsRealTimeHeaderBasedAuth } from '../AWSWebSocketProvider/authHeaders'; + // resolved/actual AuthMode values. identityPool gets resolves to IAM upstream in InternalGraphQLAPI._graphqlSubscribe type ResolvedGraphQLAuthModes = Exclude; @@ -42,7 +43,7 @@ interface DataResponse { const PROVIDER_NAME = 'AWSAppSyncEventsProvider'; -export class AWSAppSyncEventProvider extends AWSWebSocketProvider { +class AWSAppSyncEventProvider extends AWSWebSocketProvider { constructor() { super(PROVIDER_NAME); } @@ -182,3 +183,5 @@ export class AWSAppSyncEventProvider extends AWSWebSocketProvider { }; } } + +export const AppSyncEventProvider = new AWSAppSyncEventProvider(); diff --git a/packages/api-graphql/src/internals/events/appsyncRequest.ts b/packages/api-graphql/src/internals/events/appsyncRequest.ts index eadcd7db585..5b53d81204d 100644 --- a/packages/api-graphql/src/internals/events/appsyncRequest.ts +++ b/packages/api-graphql/src/internals/events/appsyncRequest.ts @@ -7,11 +7,7 @@ import { GraphQLAuthMode, getAmplifyUserAgent, } from '@aws-amplify/core/internals/utils'; -import { - // cancel as cancelREST, - post, - // updateRequestToBeCancellable, -} from '@aws-amplify/api-rest/internals'; +import { post } from '@aws-amplify/api-rest/internals'; import { CustomHeaders, RequestOptions, diff --git a/packages/api-graphql/src/internals/events/index.ts b/packages/api-graphql/src/internals/events/index.ts index e49a629d762..02f003c4d7c 100644 --- a/packages/api-graphql/src/internals/events/index.ts +++ b/packages/api-graphql/src/internals/events/index.ts @@ -5,10 +5,10 @@ import { Subscription } from 'rxjs'; import { Amplify } from '@aws-amplify/core'; import { DocumentType } from '@aws-amplify/core/internals/utils'; -import { AWSAppSyncEventProvider } from '../../Providers/AWSAppSyncEventsProvider'; +import { AppSyncEventProvider as eventProvider } from '../../Providers/AWSAppSyncEventsProvider'; import { appsyncRequest } from './appsyncRequest'; -import { configure, normalizeAuth } from './utils'; +import { configure, normalizeAuth, serializeEvents } from './utils'; import type { EventsChannel, EventsOptions, @@ -17,12 +17,9 @@ import type { SubscriptionObserver, } from './types'; -const eventProvider = new AWSAppSyncEventProvider(); - /** + * Establish a WebSocket connection to an Events channel * - * @param channelName - * @param options */ async function connect( channelName: string, @@ -79,42 +76,20 @@ async function connect( }; return { - // WS publish is not enabled in the service yet, will be a follow up feature - // publish: pub, + /** + * Subscribe to incoming events + */ subscribe: sub, + /** + * Close channel and any active subscriptions + */ close, + // publish: pub, }; } /** - * Event API expects and array of JSON strings - * - * @param events - JSON-serializable value or an array of values - * @returns array of JSON strings - */ -const serializeEvents = (events: DocumentType | DocumentType[]): string[] => { - if (Array.isArray(events)) { - return events.map((ev, idx) => { - const eventJson = JSON.stringify(ev); - if (eventJson === undefined) { - throw new Error( - `Event must be a valid JSON value. Received ${ev} at index ${idx}`, - ); - } - - return eventJson; - }); - } - - const eventJson = JSON.stringify(events); - if (eventJson === undefined) { - throw new Error(`Event must be a valid JSON value. Received ${events}`); - } - - return [eventJson]; -}; - -/** + * Publish events to a channel via REST request * * @param channelName - publish destination * @param event - JSON-serializable value or an array of values