From b2b3a56276166bc113d1aef5a5e7361b99c596aa Mon Sep 17 00:00:00 2001 From: "Vila,Jordi (IT EDP)" Date: Tue, 9 Jun 2026 13:41:52 +0200 Subject: [PATCH] Fix: avoid staying in the failed login page --- src/app/app.component.spec.ts | 71 ++++++++++++++++++++++++++++++++++- src/app/app.component.ts | 14 +++++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index b2b9893..aae626b 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,6 +1,6 @@ import { AppShellToastsComponent, AppShellToastService } from '@opendevstack/ngx-appshell' import { NatsMessage, NatsService } from './services/nats.service' -import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed, fakeAsync, flushMicrotasks, tick } from '@angular/core/testing'; import { AppComponent } from './app.component'; import { of, Subject } from 'rxjs'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @@ -49,7 +49,8 @@ describe('AppComponent', () => { ['retrieveCatalogDescriptors', 'setCatalogDescriptors', 'getCatalogDescriptors', 'getSlugUrl', 'getCatalog', 'setSelectedCatalogSlug', 'getSelectedCatalogSlug', 'getSelectedCatalogDescriptor'], { selectedCatalogSlug$: selectedCatalogSlugSubject.asObservable() } ); - routerSpy = jasmine.createSpyObj('Router', ['navigate', 'getCurrentNavigation'], { events: routerEventsSubject.asObservable(), url: '/' }); + routerSpy = jasmine.createSpyObj('Router', ['navigate', 'navigateByUrl', 'getCurrentNavigation'], { events: routerEventsSubject.asObservable(), url: '/' }); + routerSpy.navigateByUrl.and.resolveTo(true); mockAppConfigService = jasmine.createSpyObj('AppConfigService', ['getConfig']); mockProjectService = jasmine.createSpyObj('ProjectService', ['getCurrentProject', 'setCurrentProject', 'getUserProjects', 'getProjectCluster'], { project$: projectSubject.asObservable() }); mockMatDialog = jasmine.createSpyObj('MatDialog', ['open']); @@ -790,4 +791,70 @@ describe('AppComponent', () => { (component as any).closeDisclaimer(); expect(component.displayTopDisclaimer).toBeFalse(); }); + + it('should redirect to home when user is logged in on login-failed route', () => { + const loginFailedLeaf: any = { routeConfig: { path: 'login-failed' }, firstChild: null }; + const root: any = { routeConfig: { path: 'root' }, firstChild: loginFailedLeaf }; + + Object.defineProperty(routerSpy, 'routerState', { + get: () => ({ snapshot: { root } }), + configurable: true + }); + + routerSpy.navigateByUrl.calls.reset(); + + azureLoggedUser$.next({ fullName: 'User', username: 'user@example.com', projects: [] } as AppUser); + + expect(routerSpy.navigateByUrl).toHaveBeenCalledWith('/', { replaceUrl: true }); + }); + + it('should prevent duplicate redirects while login-failed redirect is in progress', () => { + const loginFailedLeaf: any = { routeConfig: { path: 'login-failed' }, firstChild: null }; + const root: any = { routeConfig: { path: 'root' }, firstChild: loginFailedLeaf }; + + Object.defineProperty(routerSpy, 'routerState', { + get: () => ({ snapshot: { root } }), + configurable: true + }); + + let resolveNavigation: ((value: boolean) => void) | undefined; + const pendingNavigation = new Promise((resolve) => { + resolveNavigation = resolve; + }); + routerSpy.navigateByUrl.and.returnValue(pendingNavigation as any); + routerSpy.navigateByUrl.calls.reset(); + + const user = { fullName: 'User', username: 'user@example.com', projects: [] } as AppUser; + azureLoggedUser$.next(user); + azureLoggedUser$.next(user); + + expect(routerSpy.navigateByUrl).toHaveBeenCalledTimes(1); + expect((component as any)._loginFailedRedirectInProgress).toBeTrue(); + + resolveNavigation?.(true); + }); + + it('should reset login-failed redirect lock after navigation settles', fakeAsync(() => { + const loginFailedLeaf: any = { routeConfig: { path: 'login-failed' }, firstChild: null }; + const root: any = { routeConfig: { path: 'root' }, firstChild: loginFailedLeaf }; + + Object.defineProperty(routerSpy, 'routerState', { + get: () => ({ snapshot: { root } }), + configurable: true + }); + + let resolveNavigation: ((value: boolean) => void) | undefined; + const pendingNavigation = new Promise((resolve) => { + resolveNavigation = resolve; + }); + routerSpy.navigateByUrl.and.returnValue(pendingNavigation as any); + + azureLoggedUser$.next({ fullName: 'User', username: 'user@example.com', projects: [] } as AppUser); + expect((component as any)._loginFailedRedirectInProgress).toBeTrue(); + + resolveNavigation?.(true); + flushMicrotasks(); + + expect((component as any)._loginFailedRedirectInProgress).toBeFalse(); + })); }); \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c25d722..e3bee97 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -65,6 +65,7 @@ export class AppComponent implements OnInit, OnDestroy { platformSelectorData = {} as PlatformSelectorWidgetDialogData; private _lastNotFoundState = false; + private _loginFailedRedirectInProgress = false; private readonly _destroying$ = new Subject(); @@ -136,6 +137,13 @@ export class AppComponent implements OnInit, OnDestroy { this.azureService.loggedUser$.subscribe((user: AppUser | null) => { this.loggedUser = user; if (user) { + if (this.isLoginFailedRouteActive() && !this._loginFailedRedirectInProgress) { + this._loginFailedRedirectInProgress = true; + this.router.navigateByUrl('/', { replaceUrl: true }).finally(() => { + this._loginFailedRedirectInProgress = false; + }); + return; + } const currentProjectForUi = this.projectService.getCurrentProject(); if(currentProjectForUi) { // Apply optimistic UI, start with it and later apply validations after fetching projects to avoid empty parts @@ -306,6 +314,12 @@ export class AppComponent implements OnInit, OnDestroy { return routePath === 'page-not-found' || routePath === '**'; } + private isLoginFailedRouteActive(): boolean { + const routeSnapshot = this.getDeepestRouteSnapshot(); + const routePath = routeSnapshot?.routeConfig?.path; + return routePath === 'login-failed'; + } + private getDeepestRouteSnapshot(): ActivatedRouteSnapshot | null { const snapshotRoot = this.router.routerState?.snapshot?.root; if (!snapshotRoot) {