From 4b20b0cb8116a234c22fb48a79200eb0783b57a1 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 23 Aug 2022 17:50:46 +0200 Subject: [PATCH 1/7] force initservices to wait until authentication is no longer blocking --- src/app/core/auth/auth-request.service.ts | 37 ++++++++++++++--------- src/app/core/auth/auth.reducer.ts | 6 ++++ src/modules/app/browser-init.service.ts | 7 +++++ src/modules/app/server-init.service.ts | 12 ++++++-- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index 00a94822d3..bcaa5972ac 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -1,5 +1,5 @@ import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap, take } from 'rxjs/operators'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestService } from '../data/request.service'; import { isNotEmpty } from '../../shared/empty.util'; @@ -26,8 +26,8 @@ export abstract class AuthRequestService { ) { } - protected fetchRequest(request: RestRequest): Observable> { - return this.rdbService.buildFromRequestUUID(request.uuid).pipe( + protected fetchRequest(requestId: string): Observable> { + return this.rdbService.buildFromRequestUUID(requestId).pipe( getFirstCompletedRemoteData(), ); } @@ -37,27 +37,36 @@ export abstract class AuthRequestService { } public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable> { - return this.halService.getEndpoint(this.linkName).pipe( + const requestId = this.requestService.generateRequestId(); + + this.halService.getEndpoint(this.linkName).pipe( filter((href: string) => isNotEmpty(href)), map((endpointURL) => this.getEndpointByMethod(endpointURL, method)), distinctUntilChanged(), - map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL, body, options)), - tap((request: PostRequest) => this.requestService.send(request)), - mergeMap((request: PostRequest) => this.fetchRequest(request)), - distinctUntilChanged()); + map((endpointURL: string) => new PostRequest(requestId, endpointURL, body, options)), + take(1) + ).subscribe((request: PostRequest) => { + this.requestService.send(request); + }); + + return this.fetchRequest(requestId); } public getRequest(method: string, options?: HttpOptions): Observable> { - return this.halService.getEndpoint(this.linkName).pipe( + const requestId = this.requestService.generateRequestId(); + + this.halService.getEndpoint(this.linkName).pipe( filter((href: string) => isNotEmpty(href)), map((endpointURL) => this.getEndpointByMethod(endpointURL, method)), distinctUntilChanged(), - map((endpointURL: string) => new GetRequest(this.requestService.generateRequestId(), endpointURL, undefined, options)), - tap((request: GetRequest) => this.requestService.send(request)), - mergeMap((request: GetRequest) => this.fetchRequest(request)), - distinctUntilChanged()); - } + map((endpointURL: string) => new GetRequest(requestId, endpointURL, undefined, options)), + take(1) + ).subscribe((request: GetRequest) => { + this.requestService.send(request); + }); + return this.fetchRequest(requestId); + } /** * Factory function to create the request object to send. This needs to be a POST client side and * a GET server side. Due to CSRF validation, the server isn't allowed to send a POST, so we allow diff --git a/src/app/core/auth/auth.reducer.ts b/src/app/core/auth/auth.reducer.ts index 6f47a3c20c..acdb8ef812 100644 --- a/src/app/core/auth/auth.reducer.ts +++ b/src/app/core/auth/auth.reducer.ts @@ -17,6 +17,7 @@ import { import { AuthTokenInfo } from './models/auth-token-info.model'; import { AuthMethod } from './models/auth.method'; import { AuthMethodType } from './models/auth.method-type'; +import { StoreActionTypes } from '../../store.actions'; /** * The auth state. @@ -251,6 +252,11 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut idle: false, }); + case StoreActionTypes.REHYDRATE: + return Object.assign({}, state, { + blocking: true, + }); + default: return state; } diff --git a/src/modules/app/browser-init.service.ts b/src/modules/app/browser-init.service.ts index 3980b8bc28..2d49870d58 100644 --- a/src/modules/app/browser-init.service.ts +++ b/src/modules/app/browser-init.service.ts @@ -91,6 +91,13 @@ export class BrowserInitService extends InitService { this.initKlaro(); + // wait for auth to be ready + await this.store.pipe( + select(isAuthenticationBlocking), + distinctUntilChanged(), + find((b: boolean) => b === false) + ).toPromise(); + return true; }; } diff --git a/src/modules/app/server-init.service.ts b/src/modules/app/server-init.service.ts index 803dc7a75a..fb3539ecfa 100644 --- a/src/modules/app/server-init.service.ts +++ b/src/modules/app/server-init.service.ts @@ -6,7 +6,7 @@ * http://www.dspace.org/license/ */ import { InitService } from '../../app/init.service'; -import { Store } from '@ngrx/store'; +import { Store, select } from '@ngrx/store'; import { AppState } from '../../app/app.reducer'; import { TransferState } from '@angular/platform-browser'; import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service'; @@ -20,7 +20,8 @@ import { MetadataService } from '../../app/core/metadata/metadata.service'; import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service'; import { CSSVariableService } from '../../app/shared/sass-helper/sass-helper.service'; import { ThemeService } from '../../app/shared/theme-support/theme.service'; -import { take } from 'rxjs/operators'; +import { take, distinctUntilChanged, find } from 'rxjs/operators'; +import { isAuthenticationBlocking } from '../../app/core/auth/selectors'; /** * Performs server-side initialization. @@ -66,6 +67,13 @@ export class ServerInitService extends InitService { this.initRouteListeners(); this.themeService.listenForThemeChanges(false); + // wait for auth to be ready + await this.store.pipe( + select(isAuthenticationBlocking), + distinctUntilChanged(), + find((b: boolean) => b === false) + ).toPromise(); + return true; }; } From e464c0f8c7d44050fa22e32ec349894dffac9ea6 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Thu, 25 Aug 2022 14:01:23 +0200 Subject: [PATCH 2/7] 92900: Move duplicate code to new InitService method --- src/app/init.service.spec.ts | 24 ++- src/app/init.service.ts | 15 ++ src/modules/app/browser-init.service.spec.ts | 155 ------------------- src/modules/app/browser-init.service.ts | 22 +-- src/modules/app/server-init.service.ts | 7 +- 5 files changed, 44 insertions(+), 179 deletions(-) delete mode 100644 src/modules/app/browser-init.service.spec.ts diff --git a/src/app/init.service.spec.ts b/src/app/init.service.spec.ts index 2c9edfd4e0..592abdfd42 100644 --- a/src/app/init.service.spec.ts +++ b/src/app/init.service.spec.ts @@ -5,7 +5,7 @@ import { inject, TestBed, waitForAsync } from '@angular/core/testing'; import { MetadataService } from './core/metadata/metadata.service'; import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service'; import { CommonModule } from '@angular/common'; -import { StoreModule } from '@ngrx/store'; +import { Store, StoreModule } from '@ngrx/store'; import { authReducer } from './core/auth/auth.reducer'; import { storeModuleConfig } from './app.reducer'; import { AngularticsProviderMock } from './shared/mocks/angulartics-provider.service.mock'; @@ -31,6 +31,7 @@ import { getMockThemeService } from './shared/mocks/theme-service.mock'; import objectContaining = jasmine.objectContaining; import createSpyObj = jasmine.createSpyObj; import SpyObj = jasmine.SpyObj; +import { getTestScheduler } from 'jasmine-marbles'; let spy: SpyObj; @@ -124,6 +125,15 @@ describe('InitService', () => { let metadataServiceSpy; let breadcrumbsServiceSpy; + const BLOCKING = { + t: { core: { auth: { blocking: true } } }, + f: { core: { auth: { blocking: false } } }, + }; + const BOOLEAN = { + t: true, + f: false, + }; + beforeEach(waitForAsync(() => { correlationIdServiceSpy = jasmine.createSpyObj('correlationIdServiceSpy', [ 'initCorrelationId', @@ -182,6 +192,18 @@ describe('InitService', () => { expect(breadcrumbsServiceSpy.listenForRouteChanges).toHaveBeenCalledTimes(1); })); }); + + describe('authenticationReady', () => { + it('should emit & complete the first time auth is unblocked', () => { + getTestScheduler().run(({ cold, expectObservable }) => { + TestBed.overrideProvider(Store, { useValue: cold('t--t--f--t--f--', BLOCKING) }); + const service = TestBed.inject(InitService); + + // @ts-ignore + expectObservable(service.authenticationReady$()).toBe('------(f|)', BOOLEAN); + }); + }); + }); }); }); diff --git a/src/app/init.service.ts b/src/app/init.service.ts index 30630e1d84..3af8b6ceb6 100644 --- a/src/app/init.service.ts +++ b/src/app/init.service.ts @@ -20,6 +20,9 @@ import { Angulartics2DSpace } from './statistics/angulartics/dspace-provider'; import { MetadataService } from './core/metadata/metadata.service'; import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service'; import { ThemeService } from './shared/theme-support/theme.service'; +import { isAuthenticationBlocking } from './core/auth/selectors'; +import { distinctUntilChanged, find } from 'rxjs/operators'; +import { Observable } from 'rxjs'; /** * Performs the initialization of the app. @@ -186,4 +189,16 @@ export abstract class InitService { this.breadcrumbsService.listenForRouteChanges(); this.themeService.listenForRouteChanges(); } + + /** + * Emits once authentication is ready (no longer blocking) + * @protected + */ + protected authenticationReady$(): Observable { + return this.store.pipe( + select(isAuthenticationBlocking), + distinctUntilChanged(), + find((b: boolean) => b === false) + ); + } } diff --git a/src/modules/app/browser-init.service.spec.ts b/src/modules/app/browser-init.service.spec.ts deleted file mode 100644 index 05da7d9d36..0000000000 --- a/src/modules/app/browser-init.service.spec.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { InitService } from '../../app/init.service'; -import { APP_CONFIG } from 'src/config/app-config.interface'; -import { inject, TestBed, waitForAsync } from '@angular/core/testing'; -import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service'; -import { MetadataService } from '../../app/core/metadata/metadata.service'; -import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service'; -import { CommonModule } from '@angular/common'; -import { Store, StoreModule } from '@ngrx/store'; -import { authReducer } from '../../app/core/auth/auth.reducer'; -import { storeModuleConfig } from '../../app/app.reducer'; -import { AngularticsProviderMock } from '../../app/shared/mocks/angulartics-provider.service.mock'; -import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider'; -import { AuthService } from '../../app/core/auth/auth.service'; -import { AuthServiceMock } from '../../app/shared/mocks/auth.service.mock'; -import { ActivatedRoute, Router } from '@angular/router'; -import { RouterMock } from '../../app/shared/mocks/router.mock'; -import { MockActivatedRoute } from '../../app/shared/mocks/active-router.mock'; -import { MenuService } from '../../app/shared/menu/menu.service'; -import { LocaleService } from '../../app/core/locale/locale.service'; -import { environment } from '../../environments/environment'; -import { provideMockStore } from '@ngrx/store/testing'; -import { AppComponent } from '../../app/app.component'; -import { RouteService } from '../../app/core/services/route.service'; -import { getMockLocaleService } from '../../app/app.component.spec'; -import { MenuServiceStub } from '../../app/shared/testing/menu-service.stub'; -import { CorrelationIdService } from '../../app/correlation-id/correlation-id.service'; -import { KlaroService } from '../../app/shared/cookies/klaro.service'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { TranslateLoaderMock } from '../../app/shared/mocks/translate-loader.mock'; -import { getTestScheduler } from 'jasmine-marbles'; -import { ThemeService } from '../../app/shared/theme-support/theme.service'; -import { getMockThemeService } from '../../app/shared/mocks/theme-service.mock'; -import { BrowserInitService } from './browser-init.service'; -import { TransferState } from '@angular/platform-browser'; - -const initialState = { - core: { - auth: { - loading: false, - blocking: true, - } - } -}; - -describe('BrowserInitService', () => { - describe('browser-specific initialization steps', () => { - let correlationIdServiceSpy; - let dspaceTransferStateSpy; - let transferStateSpy; - let metadataServiceSpy; - let breadcrumbsServiceSpy; - let klaroServiceSpy; - let googleAnalyticsSpy; - - beforeEach(waitForAsync(() => { - correlationIdServiceSpy = jasmine.createSpyObj('correlationIdServiceSpy', [ - 'initCorrelationId', - ]); - dspaceTransferStateSpy = jasmine.createSpyObj('dspaceTransferStateSpy', [ - 'transfer', - ]); - transferStateSpy = jasmine.createSpyObj('dspaceTransferStateSpy', [ - 'get', 'hasKey' - ]); - breadcrumbsServiceSpy = jasmine.createSpyObj('breadcrumbsServiceSpy', [ - 'listenForRouteChanges', - ]); - metadataServiceSpy = jasmine.createSpyObj('metadataService', [ - 'listenForRouteChange', - ]); - klaroServiceSpy = jasmine.createSpyObj('klaroServiceSpy', [ - 'initialize', - ]); - googleAnalyticsSpy = jasmine.createSpyObj('googleAnalyticsService', [ - 'addTrackingIdToPage', - ]); - - - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - imports: [ - CommonModule, - StoreModule.forRoot(authReducer, storeModuleConfig), - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }), - ], - providers: [ - { provide: InitService, useClass: BrowserInitService }, - { provide: CorrelationIdService, useValue: correlationIdServiceSpy }, - { provide: APP_CONFIG, useValue: environment }, - { provide: LocaleService, useValue: getMockLocaleService() }, - { provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() }, - { provide: MetadataService, useValue: metadataServiceSpy }, - { provide: BreadcrumbsService, useValue: breadcrumbsServiceSpy }, - { provide: AuthService, useValue: new AuthServiceMock() }, - { provide: Router, useValue: new RouterMock() }, - { provide: ActivatedRoute, useValue: new MockActivatedRoute() }, - { provide: MenuService, useValue: new MenuServiceStub() }, - { provide: KlaroService, useValue: klaroServiceSpy }, - { provide: GoogleAnalyticsService, useValue: googleAnalyticsSpy }, - { provide: ThemeService, useValue: getMockThemeService() }, - provideMockStore({ initialState }), - AppComponent, - RouteService, - { provide: TransferState, useValue: undefined }, - ] - }); - })); - - describe('initGoogleÀnalytics', () => { - it('should call googleAnalyticsService.addTrackingIdToPage()', inject([InitService], (service) => { - // @ts-ignore - service.initGoogleAnalytics(); - expect(googleAnalyticsSpy.addTrackingIdToPage).toHaveBeenCalledTimes(1); - })); - }); - - describe('initKlaro', () => { - const BLOCKING = { - t: { core: { auth: { blocking: true } } }, - f: { core: { auth: { blocking: false } } }, - }; - - it('should not initialize Klaro while auth is blocking', () => { - getTestScheduler().run(({ cold, flush}) => { - TestBed.overrideProvider(Store, { useValue: cold('t--t--t--', BLOCKING) }); - const service = TestBed.inject(InitService); - - // @ts-ignore - service.initKlaro(); - flush(); - expect(klaroServiceSpy.initialize).not.toHaveBeenCalled(); - }); - }); - - - it('should only initialize Klaro the first time auth is unblocked', () => { - getTestScheduler().run(({ cold, flush}) => { - TestBed.overrideProvider(Store, { useValue: cold('t--t--f--t--f--', BLOCKING) }); - const service = TestBed.inject(InitService); - - // @ts-ignore - service.initKlaro(); - flush(); - expect(klaroServiceSpy.initialize).toHaveBeenCalledTimes(1); - }); - }); - }); - }); -}); - diff --git a/src/modules/app/browser-init.service.ts b/src/modules/app/browser-init.service.ts index 2d49870d58..05c591b0c6 100644 --- a/src/modules/app/browser-init.service.ts +++ b/src/modules/app/browser-init.service.ts @@ -6,7 +6,7 @@ * http://www.dspace.org/license/ */ import { InitService } from '../../app/init.service'; -import { select, Store } from '@ngrx/store'; +import { Store } from '@ngrx/store'; import { AppState } from '../../app/app.reducer'; import { TransferState } from '@angular/platform-browser'; import { APP_CONFIG, APP_CONFIG_STATE, AppConfig } from '../../config/app-config.interface'; @@ -21,15 +21,13 @@ import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-prov import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service'; import { MetadataService } from '../../app/core/metadata/metadata.service'; import { BreadcrumbsService } from '../../app/breadcrumbs/breadcrumbs.service'; -import { CSSVariableService } from '../../app/shared/sass-helper/sass-helper.service'; import { KlaroService } from '../../app/shared/cookies/klaro.service'; import { AuthService } from '../../app/core/auth/auth.service'; import { ThemeService } from '../../app/shared/theme-support/theme.service'; import { StoreAction, StoreActionTypes } from '../../app/store.actions'; import { coreSelector } from '../../app/core/core.selectors'; -import { distinctUntilChanged, filter, find, map, take } from 'rxjs/operators'; +import { find, map } from 'rxjs/operators'; import { isNotEmpty } from '../../app/shared/empty.util'; -import { isAuthenticationBlocking } from '../../app/core/auth/selectors'; /** * Performs client-side initialization. @@ -91,12 +89,7 @@ export class BrowserInitService extends InitService { this.initKlaro(); - // wait for auth to be ready - await this.store.pipe( - select(isAuthenticationBlocking), - distinctUntilChanged(), - find((b: boolean) => b === false) - ).toPromise(); + await this.authenticationReady$().toPromise(); return true; }; @@ -124,16 +117,11 @@ export class BrowserInitService extends InitService { } /** - * Initialize Klaro + * Initialize Klaro (once authentication is resolved) * @protected */ protected initKlaro() { - this.store.pipe( - select(isAuthenticationBlocking), - distinctUntilChanged(), - filter((isBlocking: boolean) => isBlocking === false), - take(1) - ).subscribe(() => { + this.authenticationReady$().subscribe(() => { this.klaroService.initialize(); }); } diff --git a/src/modules/app/server-init.service.ts b/src/modules/app/server-init.service.ts index fb3539ecfa..2e35083bf4 100644 --- a/src/modules/app/server-init.service.ts +++ b/src/modules/app/server-init.service.ts @@ -67,12 +67,7 @@ export class ServerInitService extends InitService { this.initRouteListeners(); this.themeService.listenForThemeChanges(false); - // wait for auth to be ready - await this.store.pipe( - select(isAuthenticationBlocking), - distinctUntilChanged(), - find((b: boolean) => b === false) - ).toPromise(); + await this.authenticationReady$().toPromise(); return true; }; From db169251dfadc552f9e3c516316326fd22a383b9 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Thu, 25 Aug 2022 13:59:40 +0200 Subject: [PATCH 3/7] 92900: Update docs & specs for AuthRequestService --- .../core/auth/auth-request.service.spec.ts | 161 ++++++++++++++---- src/app/core/auth/auth-request.service.ts | 16 ++ 2 files changed, 146 insertions(+), 31 deletions(-) diff --git a/src/app/core/auth/auth-request.service.spec.ts b/src/app/core/auth/auth-request.service.spec.ts index 707daf9e30..2afeb26b02 100644 --- a/src/app/core/auth/auth-request.service.spec.ts +++ b/src/app/core/auth/auth-request.service.spec.ts @@ -7,57 +7,156 @@ import { TestScheduler } from 'rxjs/testing'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { ShortLivedToken } from './models/short-lived-token.model'; import { RemoteData } from '../data/remote-data'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import objectContaining = jasmine.objectContaining; +import { AuthStatus } from './models/auth-status.model'; +import { RestRequestMethod } from '../data/rest-request-method'; describe(`AuthRequestService`, () => { let halService: HALEndpointService; let endpointURL: string; + let requestID: string; let shortLivedToken: ShortLivedToken; let shortLivedTokenRD: RemoteData; let requestService: RequestService; let rdbService: RemoteDataBuildService; - let service: AuthRequestService; + let service; let testScheduler; - class TestAuthRequestService extends AuthRequestService { - constructor( - hes: HALEndpointService, - rs: RequestService, - rdbs: RemoteDataBuildService - ) { - super(hes, rs, rdbs); - } + const status = new AuthStatus(); - protected createShortLivedTokenRequest(href: string): PostRequest { - return new PostRequest(this.requestService.generateRequestId(), href); - } + class TestAuthRequestService extends AuthRequestService { + constructor( + hes: HALEndpointService, + rs: RequestService, + rdbs: RemoteDataBuildService + ) { + super(hes, rs, rdbs); } - const init = (cold: typeof TestScheduler.prototype.createColdObservable) => { - endpointURL = 'https://rest.api/auth'; - shortLivedToken = Object.assign(new ShortLivedToken(), { - value: 'some-token' - }); - shortLivedTokenRD = createSuccessfulRemoteDataObject(shortLivedToken); + protected createShortLivedTokenRequest(href: string): PostRequest { + return new PostRequest(this.requestService.generateRequestId(), href); + } + } - halService = jasmine.createSpyObj('halService', { - 'getEndpoint': cold('a', { a: endpointURL }) - }); - requestService = jasmine.createSpyObj('requestService', { - 'send': null - }); - rdbService = jasmine.createSpyObj('rdbService', { - 'buildFromRequestUUID': cold('a', { a: shortLivedTokenRD }) - }); + const init = (cold: typeof TestScheduler.prototype.createColdObservable) => { + endpointURL = 'https://rest.api/auth'; + requestID = 'requestID'; + shortLivedToken = Object.assign(new ShortLivedToken(), { + value: 'some-token' + }); + shortLivedTokenRD = createSuccessfulRemoteDataObject(shortLivedToken); - service = new TestAuthRequestService(halService, requestService, rdbService); - }; + halService = jasmine.createSpyObj('halService', { + 'getEndpoint': cold('a', { a: endpointURL }) + }); + requestService = jasmine.createSpyObj('requestService', { + 'generateRequestId': requestID, + 'send': null, + }); + rdbService = jasmine.createSpyObj('rdbService', { + 'buildFromRequestUUID': cold('a', { a: shortLivedTokenRD }) + }); + + service = new TestAuthRequestService(halService, requestService, rdbService); + + spyOn(service as any, 'fetchRequest').and.returnValue(cold('a', { a: createSuccessfulRemoteDataObject(status) })); + }; + + beforeEach(() => { + testScheduler = new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + }); + + describe('REST request methods', () => { + let options: HttpOptions; beforeEach(() => { - testScheduler = new TestScheduler((actual, expected) => { - expect(actual).toEqual(expected); + options = Object.create({}); + }); + + describe('GET', () => { + it('should send a GET request to the right endpoint and return the auth status', () => { + testScheduler.run(({ cold, expectObservable, flush }) => { + init(cold); + + expectObservable(service.getRequest('method', options)).toBe('a', { + a: objectContaining({ payload: status }), + }); + flush(); + + expect(requestService.send).toHaveBeenCalledWith(objectContaining({ + uuid: requestID, + href: endpointURL + '/method', + method: RestRequestMethod.GET, + body: undefined, + options, + })); + expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID); + }); + }); + + it('should send the request even if caller doesn\'t subscribe to the response', () => { + testScheduler.run(({ cold, flush }) => { + init(cold); + + service.getRequest('method', options); + flush(); + + expect(requestService.send).toHaveBeenCalledWith(objectContaining({ + uuid: requestID, + href: endpointURL + '/method', + method: RestRequestMethod.GET, + body: undefined, + options, + })); + expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID); + }); }); }); + describe('POST', () => { + it('should send a POST request to the right endpoint and return the auth status', () => { + testScheduler.run(({ cold, expectObservable, flush }) => { + init(cold); + + expectObservable(service.postToEndpoint('method', { content: 'something' }, options)).toBe('a', { + a: objectContaining({ payload: status }), + }); + flush(); + + expect(requestService.send).toHaveBeenCalledWith(objectContaining({ + uuid: requestID, + href: endpointURL + '/method', + method: RestRequestMethod.POST, + body: { content: 'something' }, + options, + })); + expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID); + }); + }); + + it('should send the request even if caller doesn\'t subscribe to the response', () => { + testScheduler.run(({ cold, flush }) => { + init(cold); + + service.postToEndpoint('method', { content: 'something' }, options); + flush(); + + expect(requestService.send).toHaveBeenCalledWith(objectContaining({ + uuid: requestID, + href: endpointURL + '/method', + method: RestRequestMethod.POST, + body: { content: 'something' }, + options, + })); + expect((service as any).fetchRequest).toHaveBeenCalledWith(requestID); + }); + }); + }); + }); + describe(`getShortlivedToken`, () => { it(`should call createShortLivedTokenRequest with the url for the endpoint`, () => { testScheduler.run(({ cold, expectObservable, flush }) => { diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index bcaa5972ac..4087989a6b 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -26,6 +26,11 @@ export abstract class AuthRequestService { ) { } + /** + * Fetch the response to a request from the cache, once it's completed. + * @param requestId the UUID of the request for which to retrieve the response + * @protected + */ protected fetchRequest(requestId: string): Observable> { return this.rdbService.buildFromRequestUUID(requestId).pipe( getFirstCompletedRemoteData(), @@ -36,6 +41,12 @@ export abstract class AuthRequestService { return isNotEmpty(method) ? `${endpoint}/${method}` : `${endpoint}`; } + /** + * Send a POST request to an authentication endpoint + * @param method the method to send to (e.g. 'status') + * @param body the data to send (optional) + * @param options the HTTP options for the request + */ public postToEndpoint(method: string, body?: any, options?: HttpOptions): Observable> { const requestId = this.requestService.generateRequestId(); @@ -52,6 +63,11 @@ export abstract class AuthRequestService { return this.fetchRequest(requestId); } + /** + * Send a GET request to an authentication endpoint + * @param method the method to send to (e.g. 'status') + * @param options the HTTP options for the request + */ public getRequest(method: string, options?: HttpOptions): Observable> { const requestId = this.requestService.generateRequestId(); From a77b1da804d0eb23d80cfdec4bda1242af63ffc3 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Thu, 25 Aug 2022 15:40:14 +0200 Subject: [PATCH 4/7] 92900: Patch start:dev CLI arguments through to ng serve --- scripts/serve.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/serve.ts b/scripts/serve.ts index bf5506b8bd..09e517c3d4 100644 --- a/scripts/serve.ts +++ b/scripts/serve.ts @@ -7,8 +7,9 @@ const appConfig: AppConfig = buildAppConfig(); /** * Calls `ng serve` with the following arguments configured for the UI in the app config: host, port, nameSpace, ssl + * Any CLI arguments given to this script are patched through to `ng serve` as well. */ child.spawn( - `ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl}`, + `ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl} ${process.argv.slice(2).join(' ')}`, { stdio: 'inherit', shell: true } ); From c5d3776df73e1a27141d124a044cb6514d8159b3 Mon Sep 17 00:00:00 2001 From: Yura Bondarenko Date: Fri, 5 Aug 2022 12:23:03 +0200 Subject: [PATCH 5/7] 93492: Fix SSR dodging pinned admin sidebar On the server, @slideSidebarPadding always resolved to 'expanded' because slideSidebarOver did not emit true This resulted in the SSR HTML leaving space for the expanded sidebar. Also got rid of the sliding animation as it would always play, even when replacing SSR HTML where the sidebar was already visible. --- .../admin-sidebar.component.html | 2 +- .../admin-sidebar/admin-sidebar.component.ts | 25 +++++-------------- src/app/root/root.component.ts | 5 ++-- src/app/shared/animations/slide.ts | 7 ------ 4 files changed, 10 insertions(+), 29 deletions(-) diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.html b/src/app/admin/admin-sidebar/admin-sidebar.component.html index 84402c64e9..ef220b834b 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar.component.html +++ b/src/app/admin/admin-sidebar/admin-sidebar.component.html @@ -1,4 +1,4 @@ -