Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
8 changes: 8 additions & 0 deletions debug.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[1207/120956.471:ERROR:directory_reader_win.cc(43)] FindFirstFile: El sistema no puede encontrar la ruta especificada. (0x3)
[1209/102017.100:ERROR:directory_reader_win.cc(43)] FindFirstFile: El sistema no puede encontrar la ruta especificada. (0x3)
[1209/114251.202:ERROR:directory_reader_win.cc(43)] FindFirstFile: El sistema no puede encontrar la ruta especificada. (0x3)
[1211/010054.703:ERROR:directory_reader_win.cc(43)] FindFirstFile: El sistema no puede encontrar la ruta especificada. (0x3)
[1211/165324.600:ERROR:directory_reader_win.cc(43)] FindFirstFile: El sistema no puede encontrar la ruta especificada. (0x3)
[1214/163635.420:ERROR:directory_reader_win.cc(43)] FindFirstFile: El sistema no puede encontrar la ruta especificada. (0x3)
[1218/134332.967:ERROR:directory_reader_win.cc(43)] FindFirstFile: El sistema no puede encontrar la ruta especificada. (0x3)
[1218/155414.096:ERROR:directory_reader_win.cc(43)] FindFirstFile: El sistema no puede encontrar la ruta especificada. (0x3)
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;
}
}
1 change: 1 addition & 0 deletions src/app/modules/shared/components/user/user.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { AzureAdB2CService } from '../../../login/services/azure.ad.b2c.service';


@Component({
selector: 'app-user',
templateUrl: './user.component.html',
Expand Down
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);
}
}
21 changes: 21 additions & 0 deletions src/app/modules/user/services/user.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';

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

constructor(private http: HttpClient) {
}

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

loadUser(userId): Observable<any> {
return this.http.get(`${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 initState = {
name: '',
email: '',
roles: [],
groups: [],
}

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

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

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(initState, action);

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

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

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