mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
AAE-39851 Inject DOCUMENT and BffUrlBuilder into BffAuthService; update tests for better isolation
This commit is contained in:
@@ -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`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user