Skip to content

Commit

Permalink
AAE-20808 using new GraphQL library (#10454)
Browse files Browse the repository at this point in the history
* AAE-20808 using new GraphQL library

* AAE-20808 Code refactoring

* AAE-20808 Improving unit tests

* AAE-20808 unit test improvement

* AAE-20808 Fixed process services storybook build
  • Loading branch information
ehsan-2019 authored Dec 6, 2024
1 parent 06f1699 commit 44321b0
Show file tree
Hide file tree
Showing 8 changed files with 422 additions and 129 deletions.
2 changes: 1 addition & 1 deletion lib/process-services-cloud/.storybook/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
},

"exclude": ["../**/*.spec.ts" ],
"include": ["../src/**/*", "*.js"]
"include": ["../src/**/*", "*.js", "../../core/feature-flags"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,19 @@
*/

import { TestBed } from '@angular/core/testing';
import { ProcessServiceCloudTestingModule } from '../testing/process-service-cloud.testing.module';
import { NotificationCloudService } from './notification-cloud.service';
import { provideMockFeatureFlags } from '@alfresco/adf-core/feature-flags';
import { WebSocketService } from './web-socket.service';
import { Apollo } from 'apollo-angular';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { AuthenticationService } from '@alfresco/adf-core';
import { Subject } from 'rxjs';

describe('NotificationCloudService', () => {
let service: NotificationCloudService;
let apollo: Apollo;
let apolloCreateSpy: jasmine.Spy;
let apolloSubscribeSpy: jasmine.Spy;

const useMock: any = {
subscribe: () => {}
};
let wsService: WebSocketService;
const apolloMock = jasmine.createSpyObj('Apollo', ['use', 'createNamed']);
const onLogoutSubject: Subject<void> = new Subject<void>();

const queryMock = `
subscription {
Expand All @@ -43,39 +43,39 @@ describe('NotificationCloudService', () => {

beforeEach(() => {
TestBed.configureTestingModule({
imports: [ProcessServiceCloudTestingModule]
imports: [HttpClientTestingModule],
providers: [
WebSocketService,
provideMockFeatureFlags({ ['studio-ws-graphql-subprotocol']: false }),
{
provide: Apollo,
useValue: apolloMock
},
{
provide: AuthenticationService,
useValue: {
getToken: () => 'testToken',
onLogout: onLogoutSubject.asObservable()
}
}
]
});
service = TestBed.inject(NotificationCloudService);
apollo = TestBed.inject(Apollo);

service.appsListening = [];
apolloCreateSpy = spyOn(apollo, 'createNamed');
apolloSubscribeSpy = spyOn(apollo, 'use').and.returnValue(useMock);
wsService = TestBed.inject(WebSocketService);
});

it('should not create more than one websocket per app if it was already created', () => {
service.makeGQLQuery('myAppName', queryMock);
expect(service.appsListening.length).toBe(1);
expect(service.appsListening[0]).toBe('myAppName');

service.makeGQLQuery('myAppName', queryMock);
expect(service.appsListening.length).toBe(1);
expect(service.appsListening[0]).toBe('myAppName');

expect(apolloCreateSpy).toHaveBeenCalledTimes(1);
expect(apolloSubscribeSpy).toHaveBeenCalledTimes(2);
});
it('should call getSubscription with the correct parameters', () => {
const getSubscriptionSpy = spyOn(wsService, 'getSubscription').and.callThrough();

it('should create new websocket if it is subscribing to new app', () => {
service.makeGQLQuery('myAppName', queryMock);
expect(service.appsListening.length).toBe(1);
expect(service.appsListening[0]).toBe('myAppName');

service.makeGQLQuery('myOtherAppName', queryMock);
expect(service.appsListening.length).toBe(2);
expect(service.appsListening[1]).toBe('myOtherAppName');

expect(apolloCreateSpy).toHaveBeenCalledTimes(2);
expect(apolloSubscribeSpy).toHaveBeenCalledTimes(2);
expect(getSubscriptionSpy).toHaveBeenCalledWith({
apolloClientName: 'myAppName',
wsUrl: 'myAppName/notifications',
httpUrl: 'myAppName/notifications/graphql',
subscriptionOptions: {
query: jasmine.any(Object)
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,101 +15,23 @@
* limitations under the License.
*/

import { Apollo } from 'apollo-angular';
import { HttpLink } from 'apollo-angular/http';
import { split, gql, InMemoryCache, ApolloLink, InMemoryCacheConfig } from '@apollo/client/core';
import { WebSocketLink } from '@apollo/client/link/ws';
import { onError } from '@apollo/client/link/error';
import { getMainDefinition } from '@apollo/client/utilities';
import { gql } from '@apollo/client/core';
import { Injectable } from '@angular/core';
import { AuthenticationService } from '@alfresco/adf-core';
import { BaseCloudService } from './base-cloud.service';
import { AdfHttpClient } from '@alfresco/adf-core/api';

import { WebSocketService } from './web-socket.service';
@Injectable({
providedIn: 'root'
})
export class NotificationCloudService extends BaseCloudService {
appsListening = [];

constructor(public apollo: Apollo, private http: HttpLink, private authService: AuthenticationService, protected adfHttpClient: AdfHttpClient) {
super(adfHttpClient);
}

private get webSocketHost() {
return this.contextRoot.split('://')[1];
}

private get protocol() {
return this.contextRoot.split('://')[0] === 'https' ? 'wss' : 'ws';
}

initNotificationsForApp(appName: string) {
if (!this.appsListening.includes(appName)) {
this.appsListening.push(appName);
const httpLink = this.http.create({
uri: `${this.getBasePath(appName)}/notifications/graphql`
});

const webSocketLink = new WebSocketLink({
uri: `${this.protocol}://${this.webSocketHost}/${appName}/notifications/ws/graphql`,
options: {
reconnect: true,
lazy: true,
connectionParams: {
kaInterval: 2000,
// eslint-disable-next-line @typescript-eslint/naming-convention
'X-Authorization': 'Bearer ' + this.authService.getToken()
}
}
});

const link = split(
({ query }) => {
const definition = getMainDefinition(query);
return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';
},
webSocketLink,
httpLink
);

const errorLink = onError(({ graphQLErrors, operation, forward }) => {
if (graphQLErrors) {
for (const err of graphQLErrors) {
switch (err.extensions.code) {
case 'UNAUTHENTICATED': {
const oldHeaders = operation.getContext().headers;
operation.setContext({
headers: {
...oldHeaders,
// eslint-disable-next-line @typescript-eslint/naming-convention
'X-Authorization': 'Bearer ' + this.authService.getToken()
}
});
forward(operation);
break;
}
default:
break;
}
}
}
});

this.apollo.createNamed(appName, {
link: ApolloLink.from([errorLink, link]),
cache: new InMemoryCache({ merge: true } as InMemoryCacheConfig),
defaultOptions: {
watchQuery: {
errorPolicy: 'all'
}
}
});
}
}
export class NotificationCloudService {
constructor(private readonly webSocketService: WebSocketService) {}

makeGQLQuery(appName: string, gqlQuery: string) {
this.initNotificationsForApp(appName);
return this.apollo.use(appName).subscribe({ query: gql(gqlQuery) });
return this.webSocketService.getSubscription({
apolloClientName: appName,
wsUrl: `${appName}/notifications`,
httpUrl: `${appName}/notifications/graphql`,
subscriptionOptions: {
query: gql(gqlQuery)
}
});
}
}
1 change: 1 addition & 0 deletions lib/process-services-cloud/src/lib/services/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ export * from './form-fields.interfaces';
export * from './base-cloud.service';
export * from './task-list-cloud.service.interface';
export * from './variable-mapper.sevice';
export * from './web-socket.service';
136 changes: 136 additions & 0 deletions lib/process-services-cloud/src/lib/services/web-socket.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*!
* @license
* Copyright © 2005-2024 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { TestBed } from '@angular/core/testing';
import { Apollo, gql } from 'apollo-angular';
import { lastValueFrom, of, Subject } from 'rxjs';
import { WebSocketService } from './web-socket.service';
import { SubscriptionOptions } from '@apollo/client/core';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { AuthenticationService, AppConfigService } from '@alfresco/adf-core';
import { FeaturesServiceToken } from '@alfresco/adf-core/feature-flags';

describe('WebSocketService', () => {
let service: WebSocketService;
const onLogoutSubject: Subject<void> = new Subject<void>();

const apolloMock = jasmine.createSpyObj('Apollo', ['use', 'createNamed']);
const isOnSpy = jasmine.createSpy('isOn$');

beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [
{
provide: Apollo,
useValue: apolloMock
},
{
provide: AppConfigService,
useValue: {
get: () => 'wss://testHost'
}
},
{
provide: AuthenticationService,
useValue: {
getToken: () => 'testToken',
onLogout: onLogoutSubject.asObservable()
}
},
{
provide: FeaturesServiceToken,
useValue: {
isOn$: isOnSpy
}
}
]
});
service = TestBed.inject(WebSocketService);
TestBed.inject(FeaturesServiceToken);
apolloMock.use.and.returnValues(undefined, { subscribe: () => of({}) });
isOnSpy.and.returnValues(of(true));
});

afterEach(() => {
apolloMock.use.calls.reset();
apolloMock.createNamed.calls.reset();
});

it('should not create a new Apollo client if it is already in use', async () => {
const apolloClientName = 'testClient';
const subscriptionOptions: SubscriptionOptions = { query: gql(`subscription {testQuery}`) };
const wsOptions = { apolloClientName, wsUrl: 'testUrl', subscriptionOptions };

apolloMock.use.and.returnValues(true, { subscribe: () => of({}) });

await lastValueFrom(service.getSubscription(wsOptions));

expect(apolloMock.use).toHaveBeenCalledTimes(2);
expect(apolloMock.use).toHaveBeenCalledWith(apolloClientName);
expect(apolloMock.createNamed).not.toHaveBeenCalled();
});

it('should subscribe to Apollo client if not already in use', async () => {
const apolloClientName = 'testClient';
const expectedApolloClientName = 'testClient';
const subscriptionOptions: SubscriptionOptions = { query: gql(`subscription {testQuery}`) };
const wsOptions = { apolloClientName, wsUrl: 'testUrl', subscriptionOptions };

await lastValueFrom(service.getSubscription(wsOptions));

expect(apolloMock.use).toHaveBeenCalledWith(expectedApolloClientName);
expect(apolloMock.use).toHaveBeenCalledTimes(2);
expect(apolloMock.createNamed).toHaveBeenCalledTimes(1);
expect(apolloMock.createNamed).toHaveBeenCalledWith(expectedApolloClientName, jasmine.any(Object));
});

it('should create named client with the right authentication token when FF is on', async () => {
let headers = {};
const expectedHeaders = { Authorization: 'Bearer testToken' };
const apolloClientName = 'testClient';
const subscriptionOptions: SubscriptionOptions = { query: gql(`subscription {testQuery}`) };
const wsOptions = { apolloClientName, wsUrl: 'testUrl', subscriptionOptions };
apolloMock.createNamed.and.callFake((_, options) => {
headers = options.headers;
});

await lastValueFrom(service.getSubscription(wsOptions));

expect(apolloMock.use).toHaveBeenCalledTimes(2);
expect(apolloMock.createNamed).toHaveBeenCalled();
expect(headers).toEqual(expectedHeaders);
});

it('should create named client with the right authentication token when FF is off', async () => {
isOnSpy.and.returnValues(of(false));
let headers = {};
const expectedHeaders = { 'X-Authorization': 'Bearer testToken' };
const apolloClientName = 'testClient';
const subscriptionOptions: SubscriptionOptions = { query: gql(`subscription {testQuery}`) };
const wsOptions = { apolloClientName, wsUrl: 'testUrl', subscriptionOptions };
apolloMock.createNamed.and.callFake((_, options) => {
headers = options.headers;
});

await lastValueFrom(service.getSubscription(wsOptions));

expect(apolloMock.use).toHaveBeenCalledTimes(2);
expect(apolloMock.createNamed).toHaveBeenCalled();
expect(headers).toEqual(expectedHeaders);
});
});
Loading

0 comments on commit 44321b0

Please sign in to comment.