Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ testem.log
.keys.json
keys.ts
src/environments/keys.ts
debug.log

# System Files
.DS_Store
Expand Down
14 changes: 8 additions & 6 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ import { ProjectTypeListComponent } from './modules/customer-management/componen
// tslint:disable-next-line: max-line-length
import { CreateProjectTypeComponent } from './modules/customer-management/components/projects-type/components/create-project-type/create-project-type.component';
import { CustomerEffects } from './modules/customer-management/store/customer-management.effects';
import { UserEffects } from './modules/users/store/user.effects';
import { UserEffects as UsersEffects } from './modules/users/store/user.effects';
import { UserEffects } from './modules/user/store/user.effects';
import { EntryEffects } from './modules/time-clock/store/entry.effects';
import { InjectTokenInterceptor } from './modules/shared/interceptors/inject.token.interceptor';
import { SubstractDatePipe } from './modules/shared/pipes/substract-date/substract-date.pipe';
Expand All @@ -74,7 +75,7 @@ import { LoadingBarComponent } from './modules/shared/components/loading-bar/loa
import { UsersComponent } from './modules/users/pages/users.component';
import { UsersListComponent } from './modules/users/components/users-list/users-list.component';
import { UiSwitchModule } from 'ngx-ui-switch';
import {NgxMaterialTimepickerModule} from 'ngx-material-timepicker';
import { NgxMaterialTimepickerModule } from 'ngx-material-timepicker';
// tslint:disable-next-line: max-line-length
import { TechnologyReportTableComponent } from './modules/technology-report/components/technology-report-table/technology-report-table.component';
import { TechnologyReportComponent } from './modules/technology-report/pages/technology-report.component';
Expand Down Expand Up @@ -151,8 +152,8 @@ const maskConfig: Partial<IConfig> = {
}),
!environment.production
? StoreDevtoolsModule.instrument({
maxAge: 15, // Retains last 15 states
})
maxAge: 15, // Retains last 15 states
})
: [],
EffectsModule.forRoot([
ProjectEffects,
Expand All @@ -161,9 +162,10 @@ const maskConfig: Partial<IConfig> = {
TechnologyEffects,
ProjectTypeEffects,
EntryEffects,
UsersEffects,
UserEffects,
]),
ToastrModule.forRoot()
ToastrModule.forRoot(),
],
providers: [
{
Expand All @@ -176,4 +178,4 @@ const maskConfig: Partial<IConfig> = {
],
bootstrap: [AppComponent],
})
export class AppModule { }
export class AppModule {}
4 changes: 4 additions & 0 deletions src/app/modules/login/services/azure.ad.b2c.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,8 @@ export class AzureAdB2CService {
getUserGroup(): string {
return this.msal.getAccount().idToken?.extension_role;
}

getUserId(): string{
return this.msal.getAccount().accountIdentifier;
}
}
9 changes: 9 additions & 0 deletions src/app/modules/user/models/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface User {
name: string;
email: string;
roles?: string[];
groups?: string[];
id: string;
tenant_id?: string;
deleted?: string;
}
60 changes: 60 additions & 0 deletions src/app/modules/user/services/user-info.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { TestBed } from '@angular/core/testing';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { of } from 'rxjs';
import { getUserGroups } from '../store/user.selectors';
import { UserInfoService } from './user-info.service';

describe('UserInfoService', () => {
let service: UserInfoService;
let store: MockStore;
let mockGetUserGroupsSelector: any;
const initialState = {
name: 'Unknown Name',
email: '[email protected]',
roles: [],
groups: ['fake-admin', 'fake-tester'],
id: 'dummy_id_load',
tenant_id: 'dummy_tenant_id_load',
deleted: '',
};

beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideMockStore({ initialState })],
});
service = TestBed.inject(UserInfoService);
store = TestBed.inject(MockStore);
mockGetUserGroupsSelector = store.overrideSelector(getUserGroups, initialState.groups);
});

it('should be created', () => {
expect(service).toBeTruthy();
});

it('should call groups selector', () => {
const expectedGroups = ['fake-admin', 'fake-tester'];

service.groups().subscribe((value) => {
expect(value).toEqual(expectedGroups);
});
});

const params = [
{ groupName: 'fake-admin', expectedValue: true, groups: ['fake-admin', 'fake-tester'] },
{ groupName: 'fake-owner', expectedValue: false, groups: ['fake-admin', 'fake-tester'] },
];

params.map((param) => {
it(`given group ${param.groupName} and groups [${param.groups.toString()}], isMemberOf() should return ${
param.expectedValue
}`, () => {
const groups$ = of(param.groups);

spyOn(service, 'groups').and.returnValue(groups$);

service.isMemberOf(param.groupName).subscribe((value) => {
expect(value).toEqual(param.expectedValue);
});
});
});
});
33 changes: 33 additions & 0 deletions src/app/modules/user/services/user-info.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { select, Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { getUserGroups } from '../store/user.selectors';
import { GROUPS } from '../../../../environments/environment';

@Injectable({
providedIn: 'root',
})
export class UserInfoService {
constructor(private store: Store) {}

groups(): Observable<string[]> {
return this.store.pipe(select(getUserGroups));
}

isMemberOf(groupName: string): Observable<boolean> {
return this.groups().pipe(
map((groups: string[]) => {
return groups.includes(groupName);
})
);
}

isAdmin(): Observable<boolean> {
return this.isMemberOf(GROUPS.ADMIN);
}

isTester(): Observable<boolean> {
return this.isMemberOf(GROUPS.TESTER);
}
}
20 changes: 20 additions & 0 deletions src/app/modules/user/services/user.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
import { User } from '../models/user';

@Injectable({
providedIn: 'root',
})
export class UserService {

constructor(private http: HttpClient) {
}

baseUrl = `${environment.timeTrackerApiUrl}/users`;

loadUser(userId: string): Observable<User> {
return this.http.get<User>(`${this.baseUrl}/${userId}`);
}
}
28 changes: 28 additions & 0 deletions src/app/modules/user/store/user.actions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { LoadUserFail, LoadUserSuccess, UserActionTypes } from './user.actions';

import { User } from '../models/user';

describe('Actions for User', () => {
it('LoadUserSuccess type is UserActionTypes.LOAD_USER_SUCCESS', () => {
const user: User = {
name: 'Unknown Name',
email: '[email protected]',
roles: [],
groups: [],
id: 'dummy_id_load',
tenant_id: 'dummy_tenant_id_load',
deleted: ''
};

const loadUserSuccess = new LoadUserSuccess(user);

expect(loadUserSuccess.type).toEqual(UserActionTypes.LOAD_USER_SUCCESS);
});

it('LoadUserFail type is UserActionTypes.LOAD_USER_FAIL', () => {
const loadUserFail = new LoadUserFail('error');

expect(loadUserFail.type).toEqual(UserActionTypes.LOAD_USER_FAIL);
});

});
27 changes: 27 additions & 0 deletions src/app/modules/user/store/user.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Action } from '@ngrx/store';
import { User } from '../models/user';

export enum UserActionTypes {
LOAD_USER = '[User] LOAD_USER',
LOAD_USER_SUCCESS = '[User] LOAD_USER_SUCCESS',
LOAD_USER_FAIL = '[User] LOAD_USER_FAIL',
}

export class LoadUser implements Action {
public readonly type = UserActionTypes.LOAD_USER;
constructor(readonly userId: string) {}
}

export class LoadUserSuccess implements Action {
public readonly type = UserActionTypes.LOAD_USER_SUCCESS;

constructor(readonly payload: User) {}
}

export class LoadUserFail implements Action {
public readonly type = UserActionTypes.LOAD_USER_FAIL;

constructor(public error: string) {}
}

export type UserActions = LoadUser | LoadUserSuccess | LoadUserFail;
62 changes: 62 additions & 0 deletions src/app/modules/user/store/user.effects.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Observable, of, throwError } from 'rxjs';
import { Action } from '@ngrx/store';
import { User } from '../models/user';
import { UserEffects } from './user.effects';
import { TestBed } from '@angular/core/testing';
import { UserService } from '../services/user.service';
import { provideMockActions } from '@ngrx/effects/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { LoadUser, UserActionTypes } from './user.actions';

describe('UserEffects', () => {
let actions$: Observable<Action>;
let effects: UserEffects;
let service: UserService;
const userInfo: User = {
name: 'Unknown Name',
email: '[email protected]',
roles: [],
groups: [],
id: 'dummy_tenant_id_load',
tenant_id: null,
deleted: null
};

beforeEach(() => {
TestBed.configureTestingModule({
providers: [UserEffects, provideMockActions(() => actions$)],
imports: [HttpClientTestingModule],
});

effects = TestBed.inject(UserEffects);
service = TestBed.inject(UserService);
});

it('should be created', async () => {
expect(effects).toBeTruthy();
});

it('action type is LOAD_USER_SUCCESS when service is executed successfully', async () => {
const userId = 'dummy_id_load';
const serviceSpy = spyOn(service, 'loadUser');

actions$ = of(new LoadUser(userId));
serviceSpy.and.returnValue(of(userInfo));

effects.loadUserInfo$.subscribe((action) => {
expect(action.type).toEqual(UserActionTypes.LOAD_USER_SUCCESS);
});
});

it('action type is LOAD_USER_FAIL when service fail in execution', async () => {
const userId = 'dummy_id_load';
const serviceSpy = spyOn(service, 'loadUser');

actions$ = of(new LoadUser(userId));
serviceSpy.and.returnValue(throwError({ error: { message: 'fail!' } }));

effects.loadUserInfo$.subscribe((action) => {
expect(action.type).toEqual(UserActionTypes.LOAD_USER_FAIL);
});
});
});
24 changes: 24 additions & 0 deletions src/app/modules/user/store/user.effects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { Actions, ofType, Effect } from '@ngrx/effects';
import { Action } from '@ngrx/store';
import { Observable, of } from 'rxjs';
import { catchError, map, mergeMap } from 'rxjs/operators';
import { UserService } from '../services/user.service';
import * as actions from './user.actions';

@Injectable()
export class UserEffects {
constructor(private actions$: Actions, private userService: UserService) {}

@Effect()
loadUserInfo$: Observable<Action> = this.actions$.pipe(
ofType(actions.UserActionTypes.LOAD_USER),
map((action: actions.LoadUser) => action.userId),
mergeMap((userId) =>
this.userService.loadUser(userId).pipe(
map((response) => new actions.LoadUserSuccess(response)),
catchError((error) => of(new actions.LoadUserFail(error)))
)
)
);
}
44 changes: 44 additions & 0 deletions src/app/modules/user/store/user.reducer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { userReducer } from './user.reducer';
import { LoadUser, LoadUserFail, LoadUserSuccess } from './user.actions';
import { User } from '../models/user';

describe('userReducer', () => {
const initialState = {
name: '',
email: '',
roles: [],
groups: [],
};

it('on LoadUser, state equal to initialState', () => {
const userId = 'dummy_id_load';
const action = new LoadUser(userId);
const state = userReducer(initialState, action);

expect(state).toEqual(initialState);
});

it('on LoadUserSuccess, userFound is saved in store', () => {
const userFound: User = {
name: 'Unknown Name',
email: '[email protected]',
roles: [],
groups: [],
id: 'dummy_id_load',
tenant_id: null,
deleted: null
};

const action = new LoadUserSuccess(userFound);
const state = userReducer(initialState, action);

expect(state).toEqual(userFound);
});

it('on LoadUserFail, state equal to initialState', () => {
const action = new LoadUserFail('error');
const state = userReducer(initialState, action);

expect(state).toEqual(initialState);
});
});
Loading