AAE-39851 Inject DOCUMENT and BffUrlBuilder into BffAuthService; update tests for better isolation

This commit is contained in:
alep85
2025-12-05 17:09:04 +01:00
parent 60796c68b5
commit bf7abe5444
4 changed files with 305 additions and 185 deletions
@@ -15,15 +15,19 @@
* limitations under the License.
*/
import { TestBed } from '@angular/core/testing';
import { HttpHeaders, provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { BffAuthService, BffUserResponse } from './bff-auth.service';
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { firstValueFrom } from 'rxjs';
import { provideHttpClient, HttpHeaders } from '@angular/common/http';
import { BffAuthService, BffUserResponse } from './bff-auth.service';
import { BffUrlBuilder } from './bff-url-builder.service';
import { DOCUMENT } from '@angular/common';
describe('BffAuthService', () => {
let service: BffAuthService;
let httpMock: HttpTestingController;
let urlBuilder: jasmine.SpyObj<BffUrlBuilder>;
let mockDocument: any;
const mockAuthenticatedUser: BffUserResponse = {
isAuthenticated: true,
@@ -57,11 +61,40 @@ describe('BffAuthService', () => {
}
};
beforeEach(() => {
TestBed.configureTestingModule({
providers: [BffAuthService, provideHttpClient(), provideHttpClientTesting()]
});
// eslint-disable-next-line jsdoc/require-jsdoc
function flushConstructorRequests() {
httpMock.match(urlBuilder.getUserUrl()).forEach((req) => req.flush(mockAuthenticatedUser));
}
beforeEach(() => {
urlBuilder = jasmine.createSpyObj<BffUrlBuilder>('BffUrlBuilder', ['getUserUrl', 'getLoginUrl', 'getLogoutUrl']);
urlBuilder.getUserUrl.and.returnValue('http://hawkins-lab:1983/fakePath/bff/user');
urlBuilder.getLoginUrl.and.callFake((returnUrl?: string) => {
if (!returnUrl || returnUrl === '/') {
return 'http://hawkins-lab:1983/fakePath/bff/login';
}
return `http://hawkins-lab:1983/fakePath/bff/login?returnUrl=${encodeURIComponent(returnUrl)}`;
});
urlBuilder.getLogoutUrl.and.returnValue('http://hawkins-lab:1983/fakePath/bff/logout');
mockDocument = {
location: {
href: '',
reload: () => {}
}
};
TestBed.configureTestingModule({
providers: [
BffAuthService,
{ provide: BffUrlBuilder, useValue: urlBuilder },
{ provide: DOCUMENT, useValue: mockDocument },
provideHttpClient(),
provideHttpClientTesting()
]
});
spyOn(mockDocument.location, 'reload');
service = TestBed.inject(BffAuthService);
httpMock = TestBed.inject(HttpTestingController);
});
@@ -70,55 +103,32 @@ describe('BffAuthService', () => {
});
describe('getUser', () => {
it('should return authenticated user when user is logged in', async () => {
service = TestBed.inject(BffAuthService);
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
expect(bffUserRequestMatchers.length).toBe(2);
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
it('should call urlBuilder.getUserUrl and return authenticated user', async () => {
flushConstructorRequests();
const resultPromise = firstValueFrom(service.getUser());
const req = httpMock.expectOne((reqObj) => reqObj.url.includes('/bff/user'));
const req = httpMock.expectOne(urlBuilder.getUserUrl.calls.mostRecent().returnValue);
expect(req.request.method).toBe('GET');
req.flush(mockAuthenticatedUser);
const result = await resultPromise;
expect(urlBuilder.getUserUrl).toHaveBeenCalled();
expect(result).toEqual(mockAuthenticatedUser);
expect(result.isAuthenticated).toBe(true);
expect(result.user.email).toBe('test@example.com');
});
it('should return unauthenticated user when user is not logged in', async () => {
service = TestBed.inject(BffAuthService);
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
expect(bffUserRequestMatchers.length).toBe(2);
bffUserRequestMatchers.forEach((req) => req.flush(mockUnauthenticatedUser));
httpMock.match(urlBuilder.getUserUrl()).forEach((req) => req.flush(mockUnauthenticatedUser));
const resultPromise = firstValueFrom(service.getUser());
const req = httpMock.expectOne((reqObj) => reqObj.url.includes('/bff/user'));
const req = httpMock.expectOne(urlBuilder.getUserUrl.calls.mostRecent().returnValue);
expect(req.request.method).toBe('GET');
req.flush(mockUnauthenticatedUser);
const result = await resultPromise;
expect(result).toEqual(mockUnauthenticatedUser);
expect(result.isAuthenticated).toBe(false);
});
it('should handle error when getUser fails', async () => {
service = TestBed.inject(BffAuthService);
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
expect(bffUserRequestMatchers.length).toBe(2);
bffUserRequestMatchers.forEach((req) => req.flush('Unauthorized', { status: 401, statusText: 'Unauthorized' }));
flushConstructorRequests();
try {
const result = firstValueFrom(service.getUser());
const req = httpMock.expectOne((req) => req.url.includes('/bff/user'));
const req = httpMock.expectOne(urlBuilder.getUserUrl.calls.mostRecent().returnValue);
req.flush('Unauthorized', { status: 401, statusText: 'Unauthorized' });
await result;
fail('Should have thrown an error');
@@ -127,163 +137,94 @@ describe('BffAuthService', () => {
}
});
it('should use correct URL format with protocol and host', async () => {
service = TestBed.inject(BffAuthService);
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
expect(bffUserRequestMatchers.length).toBe(2);
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
it('should use custom URL from urlBuilder', async () => {
flushConstructorRequests();
service.getUser().subscribe();
const req = httpMock.expectOne((req) => req.url.includes('/bff/user'));
expect(req.request.url).toMatch(/^https?:\/\/.+\/bff\/user$/);
req.flush(mockAuthenticatedUser);
const req = httpMock.expectOne(urlBuilder.getUserUrl.calls.mostRecent().returnValue);
expect(req.request.url).toBe('http://hawkins-lab:1983/fakePath/bff/user');
});
});
describe('login', () => {
it('should redirect to /bff/login when no returnUrl is provided', () => {
service = TestBed.inject(BffAuthService);
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
expect(bffUserRequestMatchers.length).toBe(2);
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
it('should redirect to login url when no returnUrl is provided', () => {
flushConstructorRequests();
service.login();
expect(urlBuilder.getLoginUrl).toHaveBeenCalledWith(undefined);
expect(mockDocument.location.href).toBe(urlBuilder.getLoginUrl.calls.mostRecent().returnValue);
});
it('should redirect to /bff/login when returnUrl is root', () => {
service = TestBed.inject(BffAuthService);
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
expect(bffUserRequestMatchers.length).toBe(2);
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
it('should redirect to login url when returnUrl is root', () => {
flushConstructorRequests();
service.login('/');
expect(urlBuilder.getLoginUrl).toHaveBeenCalledWith('/');
expect(mockDocument.location.href).toBe(urlBuilder.getLoginUrl.calls.mostRecent().returnValue);
});
it('should redirect to /bff/login with returnUrl when currentUrl is provided', () => {
service = TestBed.inject(BffAuthService);
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
expect(bffUserRequestMatchers.length).toBe(2);
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
it('should redirect to login url with returnUrl when currentUrl is provided', () => {
flushConstructorRequests();
service.login('/dashboard');
expect(urlBuilder.getLoginUrl).toHaveBeenCalledWith('/dashboard');
expect(mockDocument.location.href).toBe(urlBuilder.getLoginUrl.calls.mostRecent().returnValue);
});
it('should properly encode returnUrl parameter', () => {
service = TestBed.inject(BffAuthService);
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
expect(bffUserRequestMatchers.length).toBe(2);
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
flushConstructorRequests();
service.login('/path?query=value&other=data');
expect(urlBuilder.getLoginUrl).toHaveBeenCalledWith('/path?query=value&other=data');
expect(mockDocument.location.href).toBe(urlBuilder.getLoginUrl.calls.mostRecent().returnValue);
});
});
describe('logout', () => {
it('should call /bff/logout and redirect to default location on success', async () => {
service = TestBed.inject(BffAuthService);
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
expect(bffUserRequestMatchers.length).toBe(2);
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
it('should call urlBuilder.getLogoutUrl and redirect to default location on success', () => {
flushConstructorRequests();
service.logout();
const req = httpMock.expectOne((req) => req.url.includes('/bff/logout'));
const req = httpMock.expectOne(urlBuilder.getLogoutUrl.calls.mostRecent().returnValue);
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual({});
req.flush({});
expect(mockDocument.location.href).toBe('/');
});
it('should redirect to custom redirectTo location when provided', async () => {
service = TestBed.inject(BffAuthService);
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
expect(bffUserRequestMatchers.length).toBe(2);
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
it('should redirect to custom redirectTo location when provided', () => {
flushConstructorRequests();
service.logout();
const req = httpMock.expectOne((req) => req.url.includes('/bff/logout'));
const req = httpMock.expectOne(urlBuilder.getLogoutUrl.calls.mostRecent().returnValue);
req.flush({ redirectTo: '/custom-logout' });
expect(mockDocument.location.href).toBe('/custom-logout');
});
it('should reload page on logout error', async () => {
service = TestBed.inject(BffAuthService);
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
expect(bffUserRequestMatchers.length).toBe(2);
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
it('should reload page on logout error', () => {
flushConstructorRequests();
service.logout();
const req = httpMock.expectOne((req) => req.url.includes('/bff/logout'));
const req = httpMock.expectOne(urlBuilder.getLogoutUrl.calls.mostRecent().returnValue);
req.flush('Server Error', { status: 500, statusText: 'Server Error' });
expect(mockDocument.location.reload).toHaveBeenCalled();
});
});
describe('constructor', () => {
it('should set isAuthenticated to true when user is authenticated', (done) => {
service = TestBed.inject(BffAuthService);
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
expect(bffUserRequestMatchers.length).toBe(2);
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
service.onLogin.subscribe((isLoggedIn) => {
if (isLoggedIn) {
expect(service.isAuthenticated).toBe(true);
done();
}
it('should set isAuthenticated to true when user is authenticated', async () => {
flushConstructorRequests();
const isLoggedIn = await firstValueFrom(service.onLogin);
expect(isLoggedIn).toBe(true);
expect(service.isAuthenticated).toBe(true);
});
it('should not emit onLogin when user is not authenticated', fakeAsync(() => {
httpMock.match(urlBuilder.getUserUrl()).forEach((req) => req.flush(mockUnauthenticatedUser));
let emitted = false;
service.onLogin.subscribe((val) => {
if (val) emitted = true;
});
});
it('should not emit onLogin when user is not authenticated', (done) => {
service = TestBed.inject(BffAuthService);
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
expect(bffUserRequestMatchers.length).toBe(2);
bffUserRequestMatchers.forEach((req) => req.flush(mockUnauthenticatedUser));
let loginEmitted = false;
service.onLogin.subscribe((isLoggedIn) => {
if (isLoggedIn) {
loginEmitted = true;
}
});
setTimeout(() => {
expect(loginEmitted).toBe(false);
expect(service.isAuthenticated).toBe(false);
done();
}, 100);
});
it('should populate userInfo from getUser response', (done) => {
service = TestBed.inject(BffAuthService);
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
expect(bffUserRequestMatchers.length).toBe(2);
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
setTimeout(() => {
expect(service.userInfo).toEqual(mockAuthenticatedUser);
expect(service.userInfo.user.email).toBe('test@example.com');
done();
}, 100);
});
tick(100);
expect(emitted).toBe(false);
expect(service.isAuthenticated).toBe(false);
}));
it('should populate userInfo from getUser response', fakeAsync(() => {
flushConstructorRequests();
tick(100);
expect(service.userInfo).toEqual(mockAuthenticatedUser);
expect(service.userInfo.user.email).toBe('test@example.com');
}));
});
describe('interface methods', () => {
beforeEach(() => {
service = TestBed.inject(BffAuthService);
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/fakePath/bff/user'));
expect(bffUserRequestMatchers.length).toBe(2);
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
});
@@ -16,10 +16,12 @@
*/
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Inject, Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { EventEmitter } from 'eventemitter3';
import { BehaviorSubject, filter, Observable } from 'rxjs';
import { AuthenticationServiceInterface } from '../../interfaces/authentication-service.interface';
import { EventEmitter } from 'eventemitter3';
import { BffUrlBuilder } from './bff-url-builder.service';
export interface BffUserInfo {
sub: string;
@@ -57,7 +59,11 @@ export class BffAuthService implements AuthenticationServiceInterface {
once: EventEmitterInstance['once'];
emit: EventEmitterInstance['emit'];
constructor(private http: HttpClient) {
constructor(
private http: HttpClient,
private urlBuilder: BffUrlBuilder,
@Inject(DOCUMENT) private document: Document
) {
this.getUser()
.pipe(filter((user) => user.isAuthenticated))
.subscribe(() => {
@@ -65,7 +71,6 @@ export class BffAuthService implements AuthenticationServiceInterface {
this.onLogin.next(this.isAuthenticated);
});
this.getUser().subscribe((userResponse) => {
console.log('[BffAuthService] userResponse: ', userResponse);
this.userInfo = userResponse;
});
}
@@ -102,36 +107,20 @@ export class BffAuthService implements AuthenticationServiceInterface {
}
getUser(): Observable<BffUserResponse> {
const protocol = window.location.protocol;
const host = window.location.host;
console.log('[BffAuthService] getUser from ', `${protocol}//${host}/bff/user`);
return this.http.get<BffUserResponse>(`${protocol}//${host}/bff/user`);
return this.http.get<BffUserResponse>(this.urlBuilder.getUserUrl());
}
login(currentUrl?: string): Promise<void> | void {
const protocol = window.location.protocol;
const host = window.location.host;
let url: string;
if (!currentUrl || currentUrl === '/') {
url = `${protocol}//${host}/bff/login`;
} else {
url = `${protocol}//${host}/bff/login?returnUrl=${encodeURIComponent(currentUrl ?? '')}`;
}
console.log('url: ', url);
window.location.href = url;
this.document.location.href = this.urlBuilder.getLoginUrl(currentUrl);
}
logout(): Promise<void> | void {
const protocol = window.location.protocol;
const host = window.location.host;
this.http.post<{ redirectTo?: string }>(`${protocol}//${host}/bff/logout`, {}).subscribe({
this.http.post<{ redirectTo?: string }>(this.urlBuilder.getLogoutUrl(), {}).subscribe({
next: (res) => {
console.log('[BffAuthService] logout: ', res);
const target = res.redirectTo || '/';
window.location.href = target;
this.document.location.href = target;
},
error: () => window.location.reload()
error: () => this.document.location.reload()
});
}
}
@@ -0,0 +1,100 @@
/*!
* @license
* Copyright © 2005-2025 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 { DOCUMENT } from '@angular/common';
import { TestBed } from '@angular/core/testing';
import { BffUrlBuilder } from './bff-url-builder.service';
describe('BffUrlBuilder', () => {
let service: BffUrlBuilder;
let mockDocument: Document;
// eslint-disable-next-line jsdoc/require-jsdoc
function setFakeDocumentLocation(pathname: string, protocol = 'https:', host = 'hawkins-lab:1983') {
mockDocument.location = { protocol, host, pathname } as any;
}
beforeEach(() => {
mockDocument = {
location: { protocol: 'https:', host: 'hawkins-lab:1983', pathname: '/' }
} as any;
TestBed.configureTestingModule({
providers: [BffUrlBuilder, { provide: DOCUMENT, useValue: mockDocument }]
});
service = TestBed.inject(BffUrlBuilder);
});
describe('getUserUrl', () => {
it('should build user url with valid path segment', () => {
setFakeDocumentLocation('/app1');
expect(service.getUserUrl()).toBe('https://hawkins-lab:1983/app1/bff/user');
});
it('should build user url without path segment if invalid', () => {
setFakeDocumentLocation('/');
expect(service.getUserUrl()).toBe('https://hawkins-lab:1983/bff/user');
});
it('should build user url without path segment if segment is invalid', () => {
setFakeDocumentLocation('/123bad');
expect(service.getUserUrl()).toBe('https://hawkins-lab:1983/bff/user');
});
});
describe('getLoginUrl', () => {
it('should build login url with valid path segment and no returnUrl', () => {
setFakeDocumentLocation('/app1');
expect(service.getLoginUrl()).toBe('https://hawkins-lab:1983/app1/bff/login');
});
it('should build login url without path segment if invalid and no returnUrl', () => {
setFakeDocumentLocation('/');
expect(service.getLoginUrl()).toBe('https://hawkins-lab:1983/bff/login');
});
it('should build login url with valid path segment and returnUrl', () => {
setFakeDocumentLocation('/app1');
expect(service.getLoginUrl('/dashboard')).toBe('https://hawkins-lab:1983/app1/bff/login?returnUrl=%2Fdashboard');
});
it('should build login url without path segment if invalid and with returnUrl', () => {
setFakeDocumentLocation('/');
expect(service.getLoginUrl('/dashboard')).toBe('https://hawkins-lab:1983/bff/login?returnUrl=%2Fdashboard');
});
});
describe('getLogoutUrl', () => {
it('should build logout url with valid path segment', () => {
setFakeDocumentLocation('/app1');
expect(service.getLogoutUrl()).toBe('https://hawkins-lab:1983/app1/bff/logout');
});
it('should build logout url without path segment if invalid', () => {
setFakeDocumentLocation('/');
expect(service.getLogoutUrl()).toBe('https://hawkins-lab:1983/bff/logout');
});
});
describe('edge cases for path segment', () => {
it('should ignore path segment with special characters', () => {
setFakeDocumentLocation('/app$1');
expect(service.getUserUrl()).toBe('https://hawkins-lab:1983/bff/user');
});
it('should ignore path segment with spaces', () => {
setFakeDocumentLocation('/app 1');
expect(service.getUserUrl()).toBe('https://hawkins-lab:1983/bff/user');
});
it('should ignore empty path segment', () => {
setFakeDocumentLocation('');
expect(service.getUserUrl()).toBe('https://hawkins-lab:1983/bff/user');
});
});
});
@@ -0,0 +1,90 @@
/*!
* @license
* Copyright © 2005-2025 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 { Inject, Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
@Injectable({ providedIn: 'root' })
export class BffUrlBuilder {
constructor(@Inject(DOCUMENT) private document: Document) {}
private getValidPathSegment(pathname: string): string {
if (!pathname || pathname === '/') return '';
const segment = pathname.split('/')[1] || '';
if (/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(segment)) {
return segment;
}
return '';
}
private getProtocolHost(): { protocol: string; host: string; pathname: string } {
const { protocol, host, pathname } = this.document.location;
return { protocol, host, pathname };
}
/**
* Constructs and returns the URL for the BFF user endpoint.
*
* The URL is built using the protocol, host, and pathname obtained from `getProtocolHost()`.
* If a valid path segment is present (determined by `getValidPathSegment(pathname)`), it is included in the URL path.
* Otherwise, the URL is constructed without the path segment.
*
* @returns The fully constructed BFF user endpoint URL.
*/
getUserUrl(): string {
const { protocol, host, pathname } = this.getProtocolHost();
const pathSegment = this.getValidPathSegment(pathname);
return pathSegment ? `${protocol}//${host}/${pathSegment}/bff/user` : `${protocol}//${host}/bff/user`;
}
/**
* Constructs the login URL for the BFF (Backend For Frontend) authentication service.
*
* If `currentUrl` is not provided or is `'/'`, returns the login URL without a `returnUrl` query parameter.
* Otherwise, appends the encoded `currentUrl` as the `returnUrl` query parameter to the login URL.
*
* The URL is built using the protocol, host, and an optional path segment derived from the current location.
*
* @param currentUrl - The URL to redirect to after successful login. If omitted or `'/'`, no redirect is specified.
* @returns The constructed login URL as a string.
*/
getLoginUrl(currentUrl?: string): string {
const { protocol, host, pathname } = this.getProtocolHost();
const pathSegment = this.getValidPathSegment(pathname);
if (!currentUrl || currentUrl === '/') {
return pathSegment ? `${protocol}//${host}/${pathSegment}/bff/login` : `${protocol}//${host}/bff/login`;
}
const returnUrl = encodeURIComponent(currentUrl ?? '');
return pathSegment
? `${protocol}//${host}/${pathSegment}/bff/login?returnUrl=${returnUrl}`
: `${protocol}//${host}/bff/login?returnUrl=${returnUrl}`;
}
/**
* Constructs and returns the logout URL for the BFF (Backend For Frontend) service.
* The URL is built using the protocol, host, and an optional valid path segment
* obtained from the current location. If a valid path segment exists, it is included
* in the URL path before `/bff/logout`; otherwise, the URL defaults to `/bff/logout`.
*
* @returns The fully qualified logout URL for the BFF service.
*/
getLogoutUrl(): string {
const { protocol, host, pathname } = this.getProtocolHost();
const pathSegment = this.getValidPathSegment(pathname);
return pathSegment ? `${protocol}//${host}/${pathSegment}/bff/logout` : `${protocol}//${host}/bff/logout`;
}
}