Skip to content

Commit 1316540

Browse files
[CST-15074] Fixes cyclic dependency issue
1 parent c1b951b commit 1316540

15 files changed

Lines changed: 302 additions & 53 deletions
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import {
3+
Store,
4+
StoreModule,
5+
} from '@ngrx/store';
6+
import {
7+
MockStore,
8+
provideMockStore,
9+
} from '@ngrx/store/testing';
10+
11+
import { storeModuleConfig } from '../../app.reducer';
12+
import { AuthMethodTypeComponent } from '../../shared/log-in/methods/auth-methods.type';
13+
import { authReducer } from './auth.reducer';
14+
import { AuthMethodsService } from './auth-methods.service';
15+
import { AuthMethod } from './models/auth.method';
16+
import { AuthMethodType } from './models/auth.method-type';
17+
18+
describe('AuthMethodsService', () => {
19+
let service: AuthMethodsService;
20+
let store: MockStore;
21+
let mockAuthMethods: Map<AuthMethodType, AuthMethodTypeComponent>;
22+
let mockAuthMethodsArray: AuthMethod[] = [
23+
{ id: 'password', authMethodType: AuthMethodType.Password, position: 2 } as AuthMethod,
24+
{ id: 'shibboleth', authMethodType: AuthMethodType.Shibboleth, position: 1 } as AuthMethod,
25+
{ id: 'oidc', authMethodType: AuthMethodType.Oidc, position: 3 } as AuthMethod,
26+
{ id: 'ip', authMethodType: AuthMethodType.Ip, position: 4 } as AuthMethod,
27+
];
28+
29+
const initialState = {
30+
core: {
31+
auth: {
32+
authMethods: mockAuthMethodsArray,
33+
},
34+
},
35+
};
36+
37+
beforeEach(() => {
38+
TestBed.configureTestingModule({
39+
imports: [
40+
StoreModule.forRoot(authReducer, storeModuleConfig),
41+
],
42+
providers: [
43+
AuthMethodsService,
44+
provideMockStore({ initialState }),
45+
],
46+
});
47+
48+
service = TestBed.inject(AuthMethodsService);
49+
store = TestBed.inject(Store) as MockStore;
50+
51+
// Setup mock auth methods map
52+
mockAuthMethods = new Map<AuthMethodType, AuthMethodTypeComponent>();
53+
mockAuthMethods.set(AuthMethodType.Password, {} as AuthMethodTypeComponent);
54+
mockAuthMethods.set(AuthMethodType.Shibboleth, {} as AuthMethodTypeComponent);
55+
mockAuthMethods.set(AuthMethodType.Oidc, {} as AuthMethodTypeComponent);
56+
mockAuthMethods.set(AuthMethodType.Ip, {} as AuthMethodTypeComponent);
57+
58+
});
59+
60+
it('should be created', () => {
61+
expect(service).toBeTruthy();
62+
});
63+
64+
describe('getAuthMethods', () => {
65+
it('should return auth methods sorted by position', () => {
66+
67+
// Expected result after sorting and filtering IP auth
68+
const expected = [
69+
{ id: 'shibboleth', authMethodType: AuthMethodType.Shibboleth, position: 1 },
70+
{ id: 'password', authMethodType: AuthMethodType.Password, position: 2 },
71+
{ id: 'oidc', authMethodType: AuthMethodType.Oidc, position: 3 },
72+
];
73+
74+
service.getAuthMethods(mockAuthMethods).subscribe(result => {
75+
expect(result.length).toBe(3);
76+
expect(result).toEqual(expected);
77+
});
78+
});
79+
80+
it('should exclude specified auth method type', () => {
81+
82+
// Expected result after excluding Password auth and filtering IP auth
83+
const expected = [
84+
{ id: 'shibboleth', authMethodType: AuthMethodType.Shibboleth, position: 1 },
85+
{ id: 'oidc', authMethodType: AuthMethodType.Oidc, position: 3 },
86+
];
87+
88+
89+
service.getAuthMethods(mockAuthMethods, AuthMethodType.Password).subscribe(result => {
90+
expect(result.length).toBe(2);
91+
expect(result).toEqual(expected);
92+
});
93+
});
94+
95+
it('should always filter out IP authentication method', () => {
96+
97+
// Add IP auth to the mock methods map
98+
mockAuthMethods.set(AuthMethodType.Ip, {} as AuthMethodTypeComponent);
99+
100+
101+
service.getAuthMethods(mockAuthMethods).subscribe(result => {
102+
expect(result.length).toBe(3);
103+
expect(result.find(method => method.authMethodType === AuthMethodType.Ip)).toBeUndefined();
104+
});
105+
});
106+
107+
it('should handle empty auth methods array', () => {
108+
const authMethods = new Map<AuthMethodType, AuthMethodTypeComponent>();
109+
110+
111+
service.getAuthMethods(authMethods).subscribe(result => {
112+
expect(result.length).toBe(0);
113+
expect(result).toEqual([]);
114+
});
115+
});
116+
117+
it('should handle duplicate auth method types and keep only unique ones', () => {
118+
// Arrange
119+
const duplicateMethodsArray = [
120+
...mockAuthMethodsArray,
121+
{ id: 'password2', authMethodType: AuthMethodType.Password, position: 5 } as AuthMethod,
122+
];
123+
124+
125+
service.getAuthMethods(mockAuthMethods).subscribe(result => {
126+
expect(result.length).toBe(3);
127+
// Check that we only have one Password auth method
128+
const passwordMethods = result.filter(method => method.authMethodType === AuthMethodType.Password);
129+
expect(passwordMethods.length).toBe(1);
130+
});
131+
});
132+
});
133+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Injectable } from '@angular/core';
2+
import {
3+
select,
4+
Store,
5+
} from '@ngrx/store';
6+
import uniqBy from 'lodash/uniqBy';
7+
import { Observable } from 'rxjs';
8+
import { map } from 'rxjs/operators';
9+
10+
import { AppState } from '../../app.reducer';
11+
import { AuthMethodTypeComponent } from '../../shared/log-in/methods/auth-methods.type';
12+
import { rendersAuthMethodType } from '../../shared/log-in/methods/log-in.methods-decorator.utils';
13+
import { AuthMethod } from './models/auth.method';
14+
import { AuthMethodType } from './models/auth.method-type';
15+
import { getAuthenticationMethods } from './selectors';
16+
17+
@Injectable({
18+
providedIn: 'root',
19+
})
20+
/**
21+
* Service responsible for managing and filtering authentication methods.
22+
* Provides methods to retrieve and process authentication methods from the application store.
23+
*/
24+
export class AuthMethodsService {
25+
constructor(protected store: Store<AppState>) {
26+
}
27+
28+
/**
29+
* Retrieves and processes authentication methods from the store.
30+
*
31+
* @param authMethods A map of authentication method types to their corresponding components
32+
* @param excludedAuthMethod Optional authentication method type to exclude from the results
33+
* @returns An Observable of filtered and sorted authentication methods
34+
*/
35+
public getAuthMethods(
36+
authMethods: Map<AuthMethodType, AuthMethodTypeComponent>,
37+
excludedAuthMethod?: AuthMethodType,
38+
): Observable<AuthMethod[]> {
39+
return this.store.pipe(
40+
select(getAuthenticationMethods),
41+
map((methods: AuthMethod[]) => methods
42+
// ignore the given auth method if it should be excluded
43+
.filter((authMethod: AuthMethod) => excludedAuthMethod == null || authMethod.authMethodType !== excludedAuthMethod)
44+
.filter((authMethod: AuthMethod) => rendersAuthMethodType(authMethods, authMethod.authMethodType) !== undefined)
45+
.sort((method1: AuthMethod, method2: AuthMethod) => method1.position - method2.position),
46+
),
47+
// ignore the ip authentication method when it's returned by the backend
48+
map((methods: AuthMethod[]) => uniqBy(methods.filter(a => a.authMethodType !== AuthMethodType.Ip), 'authMethodType')),
49+
);
50+
}
51+
}

src/app/core/auth/auth.service.ts

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
} from '@ngrx/store';
1212
import { TranslateService } from '@ngx-translate/core';
1313
import { CookieAttributes } from 'js-cookie';
14-
import uniqBy from 'lodash/uniqBy';
1514
import {
1615
Observable,
1716
of as observableOf,
@@ -39,7 +38,6 @@ import {
3938
isNotNull,
4039
isNotUndefined,
4140
} from '../../shared/empty.util';
42-
import { rendersAuthMethodType } from '../../shared/log-in/methods/log-in.methods-decorator';
4341
import { NotificationsService } from '../../shared/notifications/notifications.service';
4442
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
4543
import { followLink } from '../../shared/utils/follow-link-config.model';
@@ -76,15 +74,13 @@ import {
7674
} from './auth.actions';
7775
import { AuthRequestService } from './auth-request.service';
7876
import { AuthMethod } from './models/auth.method';
79-
import { AuthMethodType } from './models/auth.method-type';
8077
import { AuthStatus } from './models/auth-status.model';
8178
import {
8279
AuthTokenInfo,
8380
TOKENITEM,
8481
} from './models/auth-token-info.model';
8582
import {
8683
getAuthenticatedUserId,
87-
getAuthenticationMethods,
8884
getAuthenticationToken,
8985
getExternalAuthCookieStatus,
9086
getRedirectUrl,
@@ -283,7 +279,7 @@ export class AuthService {
283279
if (status.hasSucceeded) {
284280
return status.payload.specialGroups;
285281
} else {
286-
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(),[]));
282+
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []));
287283
}
288284
}),
289285
);
@@ -591,7 +587,7 @@ export class AuthService {
591587
* @param location - The location.
592588
* @returns The external server redirect URL.
593589
*/
594-
getExternalServerRedirectUrl(origin: string, redirectRoute: string, location: string): string {
590+
getExternalServerRedirectUrl(origin: string, redirectRoute: string, location: string): string {
595591
const correctRedirectUrl = new URLCombiner(origin, redirectRoute).toString();
596592

597593
let externalServerUrl = location;
@@ -693,19 +689,4 @@ export class AuthService {
693689
this.store.dispatch(new UnsetUserAsIdleAction());
694690
}
695691
}
696-
697-
public getAuthMethods(excludedAuthMethod?: AuthMethodType): Observable<AuthMethod[]> {
698-
return this.store.pipe(
699-
select(getAuthenticationMethods),
700-
map((methods: AuthMethod[]) => methods
701-
// ignore the given auth method if it should be excluded
702-
.filter((authMethod: AuthMethod) => excludedAuthMethod == null || authMethod.authMethodType !== excludedAuthMethod)
703-
.filter((authMethod: AuthMethod) => rendersAuthMethodType(authMethod.authMethodType) !== undefined)
704-
.sort((method1: AuthMethod, method2: AuthMethod) => method1.position - method2.position),
705-
),
706-
// ignore the ip authentication method when it's returned by the backend
707-
map((authMethods: AuthMethod[]) => uniqBy(authMethods.filter(a => a.authMethodType !== AuthMethodType.Ip), 'authMethodType')),
708-
);
709-
}
710-
711692
}

src/app/external-log-in/external-log-in/external-log-in.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ <h4>{{ 'external-login.confirmation.header' | translate }}</h4>
2222
<h4 class="mt-2">{{ 'external-login.component.or' | translate }}</h4>
2323
</div>
2424
<div class="col d-flex justify-content-center align-items-center">
25-
<button class="btn block btn-lg btn-primary" (click)="openLoginModal(loginModal)">
25+
<button data-test="open-modal" class="btn block btn-lg btn-primary" (click)="openLoginModal(loginModal)">
2626
{{ 'external-login.connect-to-existing-account.label' | translate }}
2727
</button>
2828
</div>

src/app/external-log-in/external-log-in/external-log-in.component.spec.ts

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,24 @@ import { FormBuilder } from '@angular/forms';
88
import { By } from '@angular/platform-browser';
99
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
1010
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
11+
import { StoreModule } from '@ngrx/store';
12+
import { provideMockStore } from '@ngrx/store/testing';
1113
import {
1214
TranslateModule,
1315
TranslateService,
1416
} from '@ngx-translate/core';
1517
import { of as observableOf } from 'rxjs';
1618

19+
import { storeModuleConfig } from '../../app.reducer';
20+
import { authReducer } from '../../core/auth/auth.reducer';
1721
import { AuthService } from '../../core/auth/auth.service';
22+
import { AuthMethodsService } from '../../core/auth/auth-methods.service';
23+
import { AuthMethod } from '../../core/auth/models/auth.method';
24+
import { AuthMethodType } from '../../core/auth/models/auth.method-type';
1825
import { AuthRegistrationType } from '../../core/auth/models/auth.registration-type';
1926
import { MetadataValue } from '../../core/shared/metadata.models';
2027
import { Registration } from '../../core/shared/registration.model';
28+
import { AuthMethodTypeComponent } from '../../shared/log-in/methods/auth-methods.type';
2129
import { AuthServiceMock } from '../../shared/mocks/auth.service.mock';
2230
import { BrowserOnlyPipe } from '../../shared/utils/browser-only.pipe';
2331
import { ConfirmEmailComponent } from '../email-confirmation/confirm-email/confirm-email.component';
@@ -28,6 +36,22 @@ describe('ExternalLogInComponent', () => {
2836
let component: ExternalLogInComponent;
2937
let fixture: ComponentFixture<ExternalLogInComponent>;
3038
let modalService: NgbModal = jasmine.createSpyObj('modalService', ['open']);
39+
let authServiceStub: jasmine.SpyObj<AuthService>;
40+
let authMethodsServiceStub: jasmine.SpyObj<AuthMethodsService>;
41+
let mockAuthMethodsArray: AuthMethod[] = [
42+
{ id: 'password', authMethodType: AuthMethodType.Password, position: 2 } as AuthMethod,
43+
{ id: 'shibboleth', authMethodType: AuthMethodType.Shibboleth, position: 1 } as AuthMethod,
44+
{ id: 'oidc', authMethodType: AuthMethodType.Oidc, position: 3 } as AuthMethod,
45+
{ id: 'ip', authMethodType: AuthMethodType.Ip, position: 4 } as AuthMethod,
46+
];
47+
48+
const initialState = {
49+
core: {
50+
auth: {
51+
authMethods: mockAuthMethodsArray,
52+
},
53+
},
54+
};
3155

3256
const registrationDataMock = {
3357
id: '3',
@@ -55,38 +79,58 @@ describe('ExternalLogInComponent', () => {
5579
onDefaultLangChange: new EventEmitter(),
5680
};
5781

58-
beforeEach(() =>
59-
TestBed.configureTestingModule({
60-
imports: [CommonModule, TranslateModule.forRoot({}), BrowserOnlyPipe, ExternalLogInComponent, OrcidConfirmationComponent, BrowserAnimationsModule],
82+
beforeEach(async () => {
83+
authServiceStub = jasmine.createSpyObj('AuthService', ['getAuthenticationMethods']);
84+
authMethodsServiceStub = jasmine.createSpyObj('AuthMethodsService', ['getAuthMethods']);
85+
86+
await TestBed.configureTestingModule({
87+
imports: [
88+
CommonModule,
89+
TranslateModule.forRoot({}),
90+
BrowserOnlyPipe,
91+
ExternalLogInComponent,
92+
OrcidConfirmationComponent,
93+
BrowserAnimationsModule,
94+
StoreModule.forRoot(authReducer, storeModuleConfig),
95+
],
6196
providers: [
6297
{ provide: TranslateService, useValue: translateServiceStub },
6398
{ provide: AuthService, useValue: new AuthServiceMock() },
6499
{ provide: NgbModal, useValue: modalService },
65100
FormBuilder,
101+
provideMockStore({ initialState }),
66102
],
67103
})
68104
.overrideComponent(ExternalLogInComponent, {
69105
remove: {
70106
imports: [ConfirmEmailComponent],
71107
},
72108
})
73-
.compileComponents(),
74-
);
75-
109+
.compileComponents();
110+
});
76111
beforeEach(() => {
77112
fixture = TestBed.createComponent(ExternalLogInComponent);
78113
component = fixture.componentInstance;
79114
component.registrationData = Object.assign(new Registration(), registrationDataMock);
80115
component.registrationType = registrationDataMock.registrationType;
116+
117+
let mockAuthMethods = new Map<AuthMethodType, AuthMethodTypeComponent>();
118+
mockAuthMethods.set(AuthMethodType.Password, {} as AuthMethodTypeComponent);
119+
mockAuthMethods.set(AuthMethodType.Shibboleth, {} as AuthMethodTypeComponent);
120+
mockAuthMethods.set(AuthMethodType.Oidc, {} as AuthMethodTypeComponent);
121+
mockAuthMethods.set(AuthMethodType.Ip, {} as AuthMethodTypeComponent);
122+
component.authMethods = mockAuthMethods;
81123
fixture.detectChanges();
82124
});
83125

84126
it('should create', () => {
127+
fixture.detectChanges();
85128
expect(component).toBeTruthy();
86129
});
87130

88131
beforeEach(() => {
89132
component.registrationData = Object.assign(new Registration(), registrationDataMock, { email: 'user@institution.edu' });
133+
90134
fixture.detectChanges();
91135
});
92136

@@ -103,8 +147,11 @@ describe('ExternalLogInComponent', () => {
103147
});
104148

105149
it('should display login modal when connect to existing account button is clicked', () => {
106-
const button = fixture.nativeElement.querySelector('button.btn-primary');
107-
button.click();
150+
const button = fixture.debugElement.query(By.css('[data-test="open-modal"]'));
151+
152+
expect(button).not.toBeNull('Connect to existing account button should be in the DOM');
153+
154+
button.nativeElement.click();
108155
expect(modalService.open).toHaveBeenCalled();
109156
});
110157

0 commit comments

Comments
 (0)