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.
|
* 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 { 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 { 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', () => {
|
describe('BffAuthService', () => {
|
||||||
let service: BffAuthService;
|
let service: BffAuthService;
|
||||||
let httpMock: HttpTestingController;
|
let httpMock: HttpTestingController;
|
||||||
|
let urlBuilder: jasmine.SpyObj<BffUrlBuilder>;
|
||||||
|
let mockDocument: any;
|
||||||
|
|
||||||
const mockAuthenticatedUser: BffUserResponse = {
|
const mockAuthenticatedUser: BffUserResponse = {
|
||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
@@ -57,11 +61,40 @@ describe('BffAuthService', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
||||||
TestBed.configureTestingModule({
|
function flushConstructorRequests() {
|
||||||
providers: [BffAuthService, provideHttpClient(), provideHttpClientTesting()]
|
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);
|
httpMock = TestBed.inject(HttpTestingController);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,55 +103,32 @@ describe('BffAuthService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getUser', () => {
|
describe('getUser', () => {
|
||||||
it('should return authenticated user when user is logged in', async () => {
|
it('should call urlBuilder.getUserUrl and return authenticated user', async () => {
|
||||||
service = TestBed.inject(BffAuthService);
|
flushConstructorRequests();
|
||||||
|
|
||||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
|
||||||
expect(bffUserRequestMatchers.length).toBe(2);
|
|
||||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
|
||||||
|
|
||||||
const resultPromise = firstValueFrom(service.getUser());
|
const resultPromise = firstValueFrom(service.getUser());
|
||||||
|
const req = httpMock.expectOne(urlBuilder.getUserUrl.calls.mostRecent().returnValue);
|
||||||
const req = httpMock.expectOne((reqObj) => reqObj.url.includes('/bff/user'));
|
|
||||||
expect(req.request.method).toBe('GET');
|
expect(req.request.method).toBe('GET');
|
||||||
req.flush(mockAuthenticatedUser);
|
req.flush(mockAuthenticatedUser);
|
||||||
|
|
||||||
const result = await resultPromise;
|
const result = await resultPromise;
|
||||||
|
expect(urlBuilder.getUserUrl).toHaveBeenCalled();
|
||||||
expect(result).toEqual(mockAuthenticatedUser);
|
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 () => {
|
it('should return unauthenticated user when user is not logged in', async () => {
|
||||||
service = TestBed.inject(BffAuthService);
|
httpMock.match(urlBuilder.getUserUrl()).forEach((req) => req.flush(mockUnauthenticatedUser));
|
||||||
|
|
||||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
|
||||||
expect(bffUserRequestMatchers.length).toBe(2);
|
|
||||||
bffUserRequestMatchers.forEach((req) => req.flush(mockUnauthenticatedUser));
|
|
||||||
|
|
||||||
const resultPromise = firstValueFrom(service.getUser());
|
const resultPromise = firstValueFrom(service.getUser());
|
||||||
|
const req = httpMock.expectOne(urlBuilder.getUserUrl.calls.mostRecent().returnValue);
|
||||||
const req = httpMock.expectOne((reqObj) => reqObj.url.includes('/bff/user'));
|
|
||||||
expect(req.request.method).toBe('GET');
|
expect(req.request.method).toBe('GET');
|
||||||
req.flush(mockUnauthenticatedUser);
|
req.flush(mockUnauthenticatedUser);
|
||||||
|
|
||||||
const result = await resultPromise;
|
const result = await resultPromise;
|
||||||
|
|
||||||
expect(result).toEqual(mockUnauthenticatedUser);
|
expect(result).toEqual(mockUnauthenticatedUser);
|
||||||
expect(result.isAuthenticated).toBe(false);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle error when getUser fails', async () => {
|
it('should handle error when getUser fails', async () => {
|
||||||
service = TestBed.inject(BffAuthService);
|
flushConstructorRequests();
|
||||||
|
|
||||||
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' }));
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = firstValueFrom(service.getUser());
|
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' });
|
req.flush('Unauthorized', { status: 401, statusText: 'Unauthorized' });
|
||||||
await result;
|
await result;
|
||||||
fail('Should have thrown an error');
|
fail('Should have thrown an error');
|
||||||
@@ -127,163 +137,94 @@ describe('BffAuthService', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use correct URL format with protocol and host', async () => {
|
it('should use custom URL from urlBuilder', async () => {
|
||||||
service = TestBed.inject(BffAuthService);
|
flushConstructorRequests();
|
||||||
|
|
||||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
|
||||||
expect(bffUserRequestMatchers.length).toBe(2);
|
|
||||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
|
||||||
|
|
||||||
service.getUser().subscribe();
|
service.getUser().subscribe();
|
||||||
|
const req = httpMock.expectOne(urlBuilder.getUserUrl.calls.mostRecent().returnValue);
|
||||||
const req = httpMock.expectOne((req) => req.url.includes('/bff/user'));
|
expect(req.request.url).toBe('http://hawkins-lab:1983/fakePath/bff/user');
|
||||||
expect(req.request.url).toMatch(/^https?:\/\/.+\/bff\/user$/);
|
|
||||||
req.flush(mockAuthenticatedUser);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('login', () => {
|
describe('login', () => {
|
||||||
it('should redirect to /bff/login when no returnUrl is provided', () => {
|
it('should redirect to login url when no returnUrl is provided', () => {
|
||||||
service = TestBed.inject(BffAuthService);
|
flushConstructorRequests();
|
||||||
|
|
||||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
|
||||||
expect(bffUserRequestMatchers.length).toBe(2);
|
|
||||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
|
||||||
|
|
||||||
service.login();
|
service.login();
|
||||||
|
expect(urlBuilder.getLoginUrl).toHaveBeenCalledWith(undefined);
|
||||||
|
expect(mockDocument.location.href).toBe(urlBuilder.getLoginUrl.calls.mostRecent().returnValue);
|
||||||
});
|
});
|
||||||
|
it('should redirect to login url when returnUrl is root', () => {
|
||||||
it('should redirect to /bff/login when returnUrl is root', () => {
|
flushConstructorRequests();
|
||||||
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.login('/');
|
service.login('/');
|
||||||
|
expect(urlBuilder.getLoginUrl).toHaveBeenCalledWith('/');
|
||||||
|
expect(mockDocument.location.href).toBe(urlBuilder.getLoginUrl.calls.mostRecent().returnValue);
|
||||||
});
|
});
|
||||||
|
it('should redirect to login url with returnUrl when currentUrl is provided', () => {
|
||||||
it('should redirect to /bff/login with returnUrl when currentUrl is provided', () => {
|
flushConstructorRequests();
|
||||||
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.login('/dashboard');
|
service.login('/dashboard');
|
||||||
|
expect(urlBuilder.getLoginUrl).toHaveBeenCalledWith('/dashboard');
|
||||||
|
expect(mockDocument.location.href).toBe(urlBuilder.getLoginUrl.calls.mostRecent().returnValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly encode returnUrl parameter', () => {
|
it('should properly encode returnUrl parameter', () => {
|
||||||
service = TestBed.inject(BffAuthService);
|
flushConstructorRequests();
|
||||||
|
|
||||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
|
||||||
expect(bffUserRequestMatchers.length).toBe(2);
|
|
||||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
|
||||||
|
|
||||||
service.login('/path?query=value&other=data');
|
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', () => {
|
describe('logout', () => {
|
||||||
it('should call /bff/logout and redirect to default location on success', async () => {
|
it('should call urlBuilder.getLogoutUrl and redirect to default location on success', () => {
|
||||||
service = TestBed.inject(BffAuthService);
|
flushConstructorRequests();
|
||||||
|
|
||||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
|
||||||
expect(bffUserRequestMatchers.length).toBe(2);
|
|
||||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
|
||||||
|
|
||||||
service.logout();
|
service.logout();
|
||||||
|
const req = httpMock.expectOne(urlBuilder.getLogoutUrl.calls.mostRecent().returnValue);
|
||||||
const req = httpMock.expectOne((req) => req.url.includes('/bff/logout'));
|
|
||||||
expect(req.request.method).toBe('POST');
|
expect(req.request.method).toBe('POST');
|
||||||
expect(req.request.body).toEqual({});
|
|
||||||
req.flush({});
|
req.flush({});
|
||||||
|
expect(mockDocument.location.href).toBe('/');
|
||||||
});
|
});
|
||||||
|
it('should redirect to custom redirectTo location when provided', () => {
|
||||||
it('should redirect to custom redirectTo location when provided', async () => {
|
flushConstructorRequests();
|
||||||
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.logout();
|
service.logout();
|
||||||
|
const req = httpMock.expectOne(urlBuilder.getLogoutUrl.calls.mostRecent().returnValue);
|
||||||
const req = httpMock.expectOne((req) => req.url.includes('/bff/logout'));
|
|
||||||
req.flush({ redirectTo: '/custom-logout' });
|
req.flush({ redirectTo: '/custom-logout' });
|
||||||
|
expect(mockDocument.location.href).toBe('/custom-logout');
|
||||||
});
|
});
|
||||||
|
it('should reload page on logout error', () => {
|
||||||
it('should reload page on logout error', async () => {
|
flushConstructorRequests();
|
||||||
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.logout();
|
service.logout();
|
||||||
|
const req = httpMock.expectOne(urlBuilder.getLogoutUrl.calls.mostRecent().returnValue);
|
||||||
const req = httpMock.expectOne((req) => req.url.includes('/bff/logout'));
|
|
||||||
req.flush('Server Error', { status: 500, statusText: 'Server Error' });
|
req.flush('Server Error', { status: 500, statusText: 'Server Error' });
|
||||||
|
expect(mockDocument.location.reload).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('constructor', () => {
|
describe('constructor', () => {
|
||||||
it('should set isAuthenticated to true when user is authenticated', (done) => {
|
it('should set isAuthenticated to true when user is authenticated', async () => {
|
||||||
service = TestBed.inject(BffAuthService);
|
flushConstructorRequests();
|
||||||
|
const isLoggedIn = await firstValueFrom(service.onLogin);
|
||||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
expect(isLoggedIn).toBe(true);
|
||||||
expect(bffUserRequestMatchers.length).toBe(2);
|
expect(service.isAuthenticated).toBe(true);
|
||||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
});
|
||||||
|
it('should not emit onLogin when user is not authenticated', fakeAsync(() => {
|
||||||
service.onLogin.subscribe((isLoggedIn) => {
|
httpMock.match(urlBuilder.getUserUrl()).forEach((req) => req.flush(mockUnauthenticatedUser));
|
||||||
if (isLoggedIn) {
|
let emitted = false;
|
||||||
expect(service.isAuthenticated).toBe(true);
|
service.onLogin.subscribe((val) => {
|
||||||
done();
|
if (val) emitted = true;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
tick(100);
|
||||||
|
expect(emitted).toBe(false);
|
||||||
it('should not emit onLogin when user is not authenticated', (done) => {
|
expect(service.isAuthenticated).toBe(false);
|
||||||
service = TestBed.inject(BffAuthService);
|
}));
|
||||||
|
it('should populate userInfo from getUser response', fakeAsync(() => {
|
||||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
flushConstructorRequests();
|
||||||
expect(bffUserRequestMatchers.length).toBe(2);
|
tick(100);
|
||||||
bffUserRequestMatchers.forEach((req) => req.flush(mockUnauthenticatedUser));
|
expect(service.userInfo).toEqual(mockAuthenticatedUser);
|
||||||
|
expect(service.userInfo.user.email).toBe('test@example.com');
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('interface methods', () => {
|
describe('interface methods', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service = TestBed.inject(BffAuthService);
|
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/fakePath/bff/user'));
|
||||||
|
|
||||||
const bffUserRequestMatchers = httpMock.match((req) => req.url.includes('/bff/user'));
|
|
||||||
expect(bffUserRequestMatchers.length).toBe(2);
|
expect(bffUserRequestMatchers.length).toBe(2);
|
||||||
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
bffUserRequestMatchers.forEach((req) => req.flush(mockAuthenticatedUser));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,10 +16,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
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 { BehaviorSubject, filter, Observable } from 'rxjs';
|
||||||
import { AuthenticationServiceInterface } from '../../interfaces/authentication-service.interface';
|
import { AuthenticationServiceInterface } from '../../interfaces/authentication-service.interface';
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { BffUrlBuilder } from './bff-url-builder.service';
|
||||||
|
|
||||||
export interface BffUserInfo {
|
export interface BffUserInfo {
|
||||||
sub: string;
|
sub: string;
|
||||||
@@ -57,7 +59,11 @@ export class BffAuthService implements AuthenticationServiceInterface {
|
|||||||
once: EventEmitterInstance['once'];
|
once: EventEmitterInstance['once'];
|
||||||
emit: EventEmitterInstance['emit'];
|
emit: EventEmitterInstance['emit'];
|
||||||
|
|
||||||
constructor(private http: HttpClient) {
|
constructor(
|
||||||
|
private http: HttpClient,
|
||||||
|
private urlBuilder: BffUrlBuilder,
|
||||||
|
@Inject(DOCUMENT) private document: Document
|
||||||
|
) {
|
||||||
this.getUser()
|
this.getUser()
|
||||||
.pipe(filter((user) => user.isAuthenticated))
|
.pipe(filter((user) => user.isAuthenticated))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
@@ -65,7 +71,6 @@ export class BffAuthService implements AuthenticationServiceInterface {
|
|||||||
this.onLogin.next(this.isAuthenticated);
|
this.onLogin.next(this.isAuthenticated);
|
||||||
});
|
});
|
||||||
this.getUser().subscribe((userResponse) => {
|
this.getUser().subscribe((userResponse) => {
|
||||||
console.log('[BffAuthService] userResponse: ', userResponse);
|
|
||||||
this.userInfo = userResponse;
|
this.userInfo = userResponse;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -102,36 +107,20 @@ export class BffAuthService implements AuthenticationServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getUser(): Observable<BffUserResponse> {
|
getUser(): Observable<BffUserResponse> {
|
||||||
const protocol = window.location.protocol;
|
return this.http.get<BffUserResponse>(this.urlBuilder.getUserUrl());
|
||||||
const host = window.location.host;
|
|
||||||
console.log('[BffAuthService] getUser from ', `${protocol}//${host}/bff/user`);
|
|
||||||
return this.http.get<BffUserResponse>(`${protocol}//${host}/bff/user`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
login(currentUrl?: string): Promise<void> | void {
|
login(currentUrl?: string): Promise<void> | void {
|
||||||
const protocol = window.location.protocol;
|
this.document.location.href = this.urlBuilder.getLoginUrl(currentUrl);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logout(): Promise<void> | void {
|
logout(): Promise<void> | void {
|
||||||
const protocol = window.location.protocol;
|
this.http.post<{ redirectTo?: string }>(this.urlBuilder.getLogoutUrl(), {}).subscribe({
|
||||||
const host = window.location.host;
|
|
||||||
this.http.post<{ redirectTo?: string }>(`${protocol}//${host}/bff/logout`, {}).subscribe({
|
|
||||||
next: (res) => {
|
next: (res) => {
|
||||||
console.log('[BffAuthService] logout: ', res);
|
|
||||||
const target = res.redirectTo || '/';
|
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