From ce100ae1b67654e59e0388fba906ff1b726af7b7 Mon Sep 17 00:00:00 2001 From: Diego Tinitana Date: Thu, 19 Mar 2020 19:30:22 -0500 Subject: [PATCH] Add_security_Azure_AD_B2C --- .github/workflows/CD-time-tracker-ui.yml | 20 +++++ .github/workflows/CI-time-tracker-ui.yml | 20 +++++ .gitignore | 2 +- README.md | 6 +- package-lock.json | 8 ++ package.json | 1 + src/app/app-routing.module.ts | 23 +++-- src/app/app.component.html | 6 +- src/app/app.module.ts | 6 +- src/app/guards/azure-guard.service.spec.ts | 48 +++++++++++ src/app/guards/azure-guard.service.ts | 20 +++++ .../create-activity.component.spec.ts | 29 +++++++ .../activities-management.component.spec.ts | 2 +- src/app/modules/home/home.component.html | 5 ++ src/app/modules/home/home.component.scss | 0 src/app/modules/home/home.component.spec.ts | 25 ++++++ src/app/modules/home/home.component.ts | 15 ++++ src/app/modules/login/login.component.html | 1 + src/app/modules/login/login.component.scss | 0 src/app/modules/login/login.component.spec.ts | 66 ++++++++++++++ src/app/modules/login/login.component.ts | 25 ++++++ .../services/azure.ad.b2c.service.spec.ts | 86 +++++++++++++++++++ .../login/services/azure.ad.b2c.service.ts | 41 +++++++++ .../components/clock/clock.component.spec.ts | 1 - .../components/clock/clock.component.ts | 6 +- .../components/sidebar/sidebar.component.ts | 10 +-- .../components/user/user.component.html | 19 ++-- .../components/user/user.component.spec.ts | 46 +++++++++- .../shared/components/user/user.component.ts | 12 ++- .../group-by-date/group-by-date.pipe.spec.ts | 66 ++++++++++++++ .../pages/time-entries.component.spec.ts | 6 +- src/environments/environment.prod.ts | 6 ++ src/environments/environment.ts | 5 ++ src/environments/keys.example.json | 6 ++ tsconfig.json | 3 +- 35 files changed, 592 insertions(+), 49 deletions(-) create mode 100644 src/app/guards/azure-guard.service.spec.ts create mode 100644 src/app/guards/azure-guard.service.ts create mode 100644 src/app/modules/home/home.component.html create mode 100644 src/app/modules/home/home.component.scss create mode 100644 src/app/modules/home/home.component.spec.ts create mode 100644 src/app/modules/home/home.component.ts create mode 100644 src/app/modules/login/login.component.html create mode 100644 src/app/modules/login/login.component.scss create mode 100644 src/app/modules/login/login.component.spec.ts create mode 100644 src/app/modules/login/login.component.ts create mode 100644 src/app/modules/login/services/azure.ad.b2c.service.spec.ts create mode 100644 src/app/modules/login/services/azure.ad.b2c.service.ts create mode 100644 src/environments/keys.example.json diff --git a/.github/workflows/CD-time-tracker-ui.yml b/.github/workflows/CD-time-tracker-ui.yml index 50424f661..bcac64246 100644 --- a/.github/workflows/CD-time-tracker-ui.yml +++ b/.github/workflows/CD-time-tracker-ui.yml @@ -24,6 +24,26 @@ jobs: with: node-version: '12.x' + - name: Inject Secrets + shell: python + env: + SCOPES: ${{ secrets.scopes }} + CLIENT_ID: ${{ secrets.client_id }} + AUTHORITY: ${{ secrets.authority }} + BASE_PATH: "src/environments/" + run: | + import os + import json + data = {} + base_path = os.environ.get('BASE_PATH', 'src/environments/') + with open(base_path + "keys.example.json", "r+") as jsonFileRead: + data = json.load(jsonFileRead) + data["scopes"] = str(os.environ['SCOPES']).split(",") + data["client_id"] = os.environ['CLIENT_ID'] + data["authority"] = os.environ['AUTHORITY'] + with open(base_path + ".keys.json", "w+") as jsonFileWrite: + json.dump(data, jsonFileWrite) + - name: 'run: npm install and build' run: | npm install diff --git a/.github/workflows/CI-time-tracker-ui.yml b/.github/workflows/CI-time-tracker-ui.yml index 1c4a7fbd6..4d6aad5de 100644 --- a/.github/workflows/CI-time-tracker-ui.yml +++ b/.github/workflows/CI-time-tracker-ui.yml @@ -39,5 +39,25 @@ jobs: - name: Install dependencies run: npm install + - name: Inject Secrets + shell: python + env: + SCOPES: ${{ secrets.scopes }} + CLIENT_ID: ${{ secrets.client_id }} + AUTHORITY: ${{ secrets.authority }} + BASE_PATH: "src/environments/" + run: | + import os + import json + data = {} + base_path = os.environ.get('BASE_PATH', 'src/environments/') + with open(base_path + "keys.example.json", "r+") as jsonFileRead: + data = json.load(jsonFileRead) + data["scopes"] = str(os.environ['SCOPES']).split(",") + data["client_id"] = os.environ['CLIENT_ID'] + data["authority"] = os.environ['AUTHORITY'] + with open(base_path + ".keys.json", "w+") as jsonFileWrite: + json.dump(data, jsonFileWrite) + - name: Run the test run: npm run ci-test --if-present diff --git a/.gitignore b/.gitignore index 86d943a9b..4606716c0 100644 --- a/.gitignore +++ b/.gitignore @@ -40,7 +40,7 @@ npm-debug.log yarn-error.log testem.log /typings - +.keys.json # System Files .DS_Store Thumbs.db diff --git a/README.md b/README.md index 0bc8a1a49..f655acc24 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,12 @@ Run `npm install` to install the required node_modules for this project. Run `ng serve` to run the app in dev mode. After executing this command, you can navigate to `http://localhost:4200/` to see the app working. The app will automatically reload if you change anything in the source files. -## Prepare your environment for vscode +## Prepare your environment +### Set environment variables +Create file .keys.json from keys.example.ts into environments folder with the content pinned in our slack channel + +### Prepare your environment for vscode Install the following extensions: - `EditorConfig for Visual Studio Code`. diff --git a/package-lock.json b/package-lock.json index 2ed368ccd..eaec87f61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7257,6 +7257,14 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "msal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/msal/-/msal-1.2.1.tgz", + "integrity": "sha512-Zo28eyRtT/Un+zcpMfPtTPD+eo/OqzsRER0k5dyk8Mje/K1oLlaEOAgZHlJs59Y2xyuVg8OrcKqSn/1MeNjZYw==", + "requires": { + "tslib": "^1.9.3" + } + }, "multicast-dns": { "version": "6.2.3", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", diff --git a/package.json b/package.json index a8c270fb5..deb40e2ce 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "bootstrap": "^4.4.1", "jquery": "^3.4.1", "minimist": "^1.2.5", + "msal": "^1.2.1", "rxjs": "~6.5.4", "tslib": "^1.10.0", "zone.js": "~0.10.2" diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 5be963b0a..0570ce90a 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,19 +1,28 @@ import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; + +import { AzureGuardService } from './guards/azure-guard.service'; import { ReportsComponent } from './modules/reports/pages/reports.component'; import { TimeClockComponent } from './modules/time-clock/pages/time-clock.component'; import { TimeEntriesComponent } from './modules/time-entries/pages/time-entries.component'; import { ProjectManagementComponent } from './modules/project-management/pages/project-management.component'; import { ActivitiesManagementComponent } from './modules/activities-management/pages/activities-management.component'; +import { HomeComponent } from './modules/home/home.component'; +import { LoginComponent } from './modules/login/login.component'; const routes: Routes = [ - {path: 'reports', component: ReportsComponent}, - {path: 'time-clock', component: TimeClockComponent}, - {path: 'time-entries', component: TimeEntriesComponent}, - {path: 'project-management', component: ProjectManagementComponent}, - {path: 'activities-management', component: ActivitiesManagementComponent}, - {path: '', pathMatch: 'full', redirectTo: 'time-clock'}, - {path: '**', pathMatch: 'full', redirectTo: 'time-clock'}, + + { path: '', component: HomeComponent, canActivate: [AzureGuardService], + children: [ + { path: 'reports', component: ReportsComponent }, + { path: 'time-clock', component: TimeClockComponent }, + { path: 'time-entries', component: TimeEntriesComponent }, + { path: 'project-management', component: ProjectManagementComponent }, + { path: 'activities-management', component: ActivitiesManagementComponent }, + {path: '', pathMatch: 'full', redirectTo: 'time-clock'}, + ] + }, + { path: 'login', component: LoginComponent }, ]; @NgModule({ diff --git a/src/app/app.component.html b/src/app/app.component.html index 98c2c932e..90c6b6463 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d2d216329..f310c20ff 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -25,6 +25,8 @@ import { ActivitiesManagementComponent } from './modules/activities-management/p import { ActivityListComponent } from './modules/activities-management/components/activity-list/activity-list.component'; import { CreateActivityComponent } from './modules/activities-management/components/create-activity/create-activity.component'; +import { HomeComponent } from './modules/home/home.component'; +import { LoginComponent } from './modules/login/login.component'; @NgModule({ declarations: [ @@ -47,7 +49,9 @@ import { CreateActivityComponent } from './modules/activities-management/compone GroupByDatePipe, ActivitiesManagementComponent, CreateActivityComponent, - ActivityListComponent + ActivityListComponent, + HomeComponent, + LoginComponent ], imports: [ CommonModule, diff --git a/src/app/guards/azure-guard.service.spec.ts b/src/app/guards/azure-guard.service.spec.ts new file mode 100644 index 000000000..187801c4e --- /dev/null +++ b/src/app/guards/azure-guard.service.spec.ts @@ -0,0 +1,48 @@ +import { TestBed, inject } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { Router } from '@angular/router'; + +import { AzureAdB2CService } from '../modules/login/services/azure.ad.b2c.service'; +import { AzureGuardService } from './azure-guard.service'; + + +describe('AzureGuardService', () => { + let service: AzureGuardService; + let azureAdB2CService: AzureAdB2CService; + const azureAdB2CServiceStub = { + isLogin() { + return true; + } + }; + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ RouterTestingModule ], + providers: [ + { providers: AzureAdB2CService, useValue: azureAdB2CServiceStub}, + ] + }); + service = TestBed.inject(AzureGuardService); + azureAdB2CService = TestBed.inject(AzureAdB2CService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('can activate the route when user is logged-in', () => { + spyOn(azureAdB2CService, 'isLogin').and.returnValue(true); + const canActivate = service.canActivate(); + expect(azureAdB2CService.isLogin).toHaveBeenCalled(); + expect(canActivate).toEqual(true); + }); + + it('can not active the route and is redirected to login if user is not logged-in', inject([Router], (router: Router) => { + spyOn(azureAdB2CService, 'isLogin').and.returnValue(false); + spyOn(router, 'navigate').and.stub(); + const canActivate = service.canActivate(); + expect(azureAdB2CService.isLogin).toHaveBeenCalled(); + expect(canActivate).toEqual(false); + expect(router.navigate).toHaveBeenCalledWith(['login']); + })); + +}); diff --git a/src/app/guards/azure-guard.service.ts b/src/app/guards/azure-guard.service.ts new file mode 100644 index 000000000..bcb4b96fc --- /dev/null +++ b/src/app/guards/azure-guard.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { Router, CanActivate } from '@angular/router'; +import { AzureAdB2CService } from '../modules/login/services/azure.ad.b2c.service'; + +@Injectable({ + providedIn: 'root' +}) +export class AzureGuardService implements CanActivate { + + constructor(private azureAdB2CService: AzureAdB2CService, private router: Router) { } + + canActivate() { + if (this.azureAdB2CService.isLogin()) { + return true; + } else { + this.router.navigate(['login']); + return false; + } +} +} diff --git a/src/app/modules/activities-management/components/create-activity/create-activity.component.spec.ts b/src/app/modules/activities-management/components/create-activity/create-activity.component.spec.ts index 15cace0a6..16fd83160 100644 --- a/src/app/modules/activities-management/components/create-activity/create-activity.component.spec.ts +++ b/src/app/modules/activities-management/components/create-activity/create-activity.component.spec.ts @@ -7,6 +7,11 @@ describe('CreateActivityComponent', () => { let component: CreateActivityComponent; + const data = { + name: '', + description: '' + }; + beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ CreateActivityComponent], @@ -19,4 +24,28 @@ describe('CreateActivityComponent', () => { expect(component).toBeTruthy(); }); + it('should reset form onSubmit', () => { + spyOn(component.activityForm, 'reset'); + + component.onSubmit(data); + + expect(component.activityForm.reset).toHaveBeenCalled(); + }); + + it('should get name using activityForm', () => { + spyOn(component.activityForm, 'get'); + // tslint:disable-next-line:no-unused-expression + component.name; + expect(component.activityForm.get).toHaveBeenCalledWith('name'); + }); + + it('should get description using activityForm', () => { + spyOn(component.activityForm, 'get'); + + // tslint:disable-next-line:no-unused-expression + component.description; + + expect(component.activityForm.get).toHaveBeenCalledWith('description'); + }); + }); diff --git a/src/app/modules/activities-management/pages/activities-management.component.spec.ts b/src/app/modules/activities-management/pages/activities-management.component.spec.ts index 741949afc..0f278d790 100644 --- a/src/app/modules/activities-management/pages/activities-management.component.spec.ts +++ b/src/app/modules/activities-management/pages/activities-management.component.spec.ts @@ -16,7 +16,7 @@ describe('ActivitiesManagementComponent', () => { providers: [ ActivityService, HttpClient, HttpHandler ] }); component = TestBed.createComponent(ActivitiesManagementComponent).componentInstance; - activityService = TestBed.get(ActivityService); + activityService = TestBed.inject(ActivityService); spyOn(activityService, 'getActivities').and.returnValue(of(activitiesFromApi)); })); diff --git a/src/app/modules/home/home.component.html b/src/app/modules/home/home.component.html new file mode 100644 index 000000000..924cf8b51 --- /dev/null +++ b/src/app/modules/home/home.component.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/app/modules/home/home.component.scss b/src/app/modules/home/home.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/modules/home/home.component.spec.ts b/src/app/modules/home/home.component.spec.ts new file mode 100644 index 000000000..86774ae21 --- /dev/null +++ b/src/app/modules/home/home.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomeComponent } from './home.component'; + +describe('HomeComponent', () => { + let component: HomeComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ HomeComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/modules/home/home.component.ts b/src/app/modules/home/home.component.ts new file mode 100644 index 000000000..73acf06f0 --- /dev/null +++ b/src/app/modules/home/home.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'] +}) +export class HomeComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/src/app/modules/login/login.component.html b/src/app/modules/login/login.component.html new file mode 100644 index 000000000..19e52d0f4 --- /dev/null +++ b/src/app/modules/login/login.component.html @@ -0,0 +1 @@ + diff --git a/src/app/modules/login/login.component.scss b/src/app/modules/login/login.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/modules/login/login.component.spec.ts b/src/app/modules/login/login.component.spec.ts new file mode 100644 index 000000000..725dae4ce --- /dev/null +++ b/src/app/modules/login/login.component.spec.ts @@ -0,0 +1,66 @@ +import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; +import { AzureAdB2CService } from '../../modules/login/services/azure.ad.b2c.service'; +import { of } from 'rxjs'; + +import { LoginComponent } from './login.component'; +import { Router } from '@angular/router'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + let azureAdB2CService: AzureAdB2CService; + + const azureAdB2CServiceStub = { + isLogin() { + return true; + }, + signIn() { + return of(); + } + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ RouterTestingModule ], + declarations: [ LoginComponent ], + providers: [ + { providers: AzureAdB2CService, useValue: azureAdB2CServiceStub} + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + azureAdB2CService = TestBed.inject(AzureAdB2CService); + + }); + + it('Service injected via inject(...) and TestBed.get(...) should be the same instance', + inject([AzureAdB2CService], (injectService: AzureAdB2CService) => { + expect(injectService).toEqual(azureAdB2CService); + }) + ); + + it('should create login component', () => { + expect(component).toBeTruthy(); + }); + + it('should sign up or login with google if is not logged-in into the app', inject([Router], (router: Router) => { + spyOn(azureAdB2CService, 'isLogin').and.returnValue(false); + spyOn(azureAdB2CService, 'signIn').and.returnValue(of()); + component.login(); + expect(azureAdB2CService.signIn).toHaveBeenCalled(); + })); + + it('should not sign-up or login with google if is already logged-in into the app', inject([Router], (router: Router) => { + spyOn(azureAdB2CService, 'isLogin').and.returnValue(true); + spyOn(router, 'navigate').and.stub(); + component.login(); + expect(azureAdB2CService.isLogin).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith(['']); + })); +}); diff --git a/src/app/modules/login/login.component.ts b/src/app/modules/login/login.component.ts new file mode 100644 index 000000000..f3eb6e859 --- /dev/null +++ b/src/app/modules/login/login.component.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { AzureAdB2CService } from './services/azure.ad.b2c.service'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-login', + templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'] +}) + +export class LoginComponent { + + constructor(private azureAdB2CService: AzureAdB2CService, private router: Router) {} + + login(): void { + if (this.azureAdB2CService.isLogin()) { + this.router.navigate(['']); + } else { + this.azureAdB2CService.signIn().subscribe(() => { + this.router.navigate(['']); + }); + } + } + +} diff --git a/src/app/modules/login/services/azure.ad.b2c.service.spec.ts b/src/app/modules/login/services/azure.ad.b2c.service.spec.ts new file mode 100644 index 000000000..8b19b6fa5 --- /dev/null +++ b/src/app/modules/login/services/azure.ad.b2c.service.spec.ts @@ -0,0 +1,86 @@ +import { TestBed, inject } from '@angular/core/testing'; +import { AzureAdB2CService } from './azure.ad.b2c.service'; +import { UserAgentApplication, Account } from 'msal'; + +describe('AzureAdB2CService', () => { + let service: AzureAdB2CService; + const msalStub = { + loginPopup() { + return {}; + } + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [] + }); + service = TestBed.inject(AzureAdB2CService); + }); + + it('should be created', inject([AzureAdB2CService], + ( apiService: AzureAdB2CService) => { + expect(apiService).toBeTruthy(); + })); + + it('on signIn should call msal loginPopup', () => { + spyOn(UserAgentApplication.prototype, 'loginPopup').and.returnValue(( + new Promise((resolve) => { + resolve(); + }) + )); + service.signIn(); + expect(UserAgentApplication.prototype.loginPopup).toHaveBeenCalled(); + }); + + it('on logout should call msal logout', () => { + spyOn(UserAgentApplication.prototype, 'logout').and.returnValue(); + service.logout(); + expect(UserAgentApplication.prototype.logout).toHaveBeenCalled(); + }); + + it('should get Account name from UserAgentApplication', () => { + const account: Account = { + accountIdentifier: 'abc', + homeAccountIdentifier: 'abc', + userName: 'abc', + name: 'abc', + idToken: {}, + idTokenClaims: {}, + sid: 'abc', + environment: 'abc' + }; + spyOn(UserAgentApplication.prototype, 'getAccount').and.returnValues(account); + + const name = service.getName(); + + expect(UserAgentApplication.prototype.getAccount).toHaveBeenCalled(); + expect(name).toEqual(account.name); + }); + + it('isLogin returns true if UserAgentApplication has a defined Account', () => { + const account: Account = { + accountIdentifier: 'abc', + homeAccountIdentifier: 'abc', + userName: 'abc', + name: 'abc', + idToken: {}, + idTokenClaims: {}, + sid: 'abc', + environment: 'abc' + }; + spyOn(UserAgentApplication.prototype, 'getAccount').and.returnValue(account); + + const isLogin = service.isLogin(); + + expect(UserAgentApplication.prototype.getAccount).toHaveBeenCalled(); + expect(isLogin).toEqual(true); + }); + + it('isLogin returns false if UserAgentApplication has a null value for Account', () => { + spyOn(UserAgentApplication.prototype, 'getAccount').and.returnValue(null); + const isLogin = service.isLogin(); + expect(UserAgentApplication.prototype.getAccount).toHaveBeenCalled(); + expect(isLogin).toEqual(false); + }); + +}); diff --git a/src/app/modules/login/services/azure.ad.b2c.service.ts b/src/app/modules/login/services/azure.ad.b2c.service.ts new file mode 100644 index 000000000..da0c4a9d7 --- /dev/null +++ b/src/app/modules/login/services/azure.ad.b2c.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import { Observable, from } from 'rxjs'; +import { CLIENT_ID, AUTHORITY, SCOPES } from '../../../../environments/environment'; +import { UserAgentApplication } from 'msal'; + +@Injectable({ + providedIn: 'root' +}) + +export class AzureAdB2CService { + + msalConfig = { + auth: { + clientId: CLIENT_ID, + authority: AUTHORITY, + validateAuthority: false + } + }; + + tokenRequest = { + scopes: SCOPES + }; + + msal = new UserAgentApplication(this.msalConfig); + + signIn(): Observable { + return from(this.msal.loginPopup(this.tokenRequest)); + } + + logout() { + this.msal.logout(); + } + + getName(): string { + return this.msal.getAccount().name; + } + + isLogin() { + return this.msal.getAccount() ? true : false; + } +} diff --git a/src/app/modules/shared/components/clock/clock.component.spec.ts b/src/app/modules/shared/components/clock/clock.component.spec.ts index 73172557f..47455782f 100644 --- a/src/app/modules/shared/components/clock/clock.component.spec.ts +++ b/src/app/modules/shared/components/clock/clock.component.spec.ts @@ -43,5 +43,4 @@ describe('ClockComponent', () => { component.showClock(); expect(component.showClock).toHaveBeenCalled(); }); - }); diff --git a/src/app/modules/shared/components/clock/clock.component.ts b/src/app/modules/shared/components/clock/clock.component.ts index e8598ca05..85142de81 100644 --- a/src/app/modules/shared/components/clock/clock.component.ts +++ b/src/app/modules/shared/components/clock/clock.component.ts @@ -6,7 +6,7 @@ import { interval } from 'rxjs'; templateUrl: './clock.component.html', styleUrls: ['./clock.component.scss'] }) -export class ClockComponent implements OnInit { +export class ClockComponent { currentDate: Date = new Date(); hour: number; @@ -31,8 +31,4 @@ export class ClockComponent implements OnInit { this.seconds = this.currentDate.getSeconds(); }); } - - ngOnInit(): void { - } - } diff --git a/src/app/modules/shared/components/sidebar/sidebar.component.ts b/src/app/modules/shared/components/sidebar/sidebar.component.ts index 2d2883c56..58de5a33c 100644 --- a/src/app/modules/shared/components/sidebar/sidebar.component.ts +++ b/src/app/modules/shared/components/sidebar/sidebar.component.ts @@ -17,11 +17,11 @@ export class SidebarComponent implements OnInit { getItemsSidebar() { this.itemsSidebar = [ - {route: './time-clock', icon: 'far fa-clock', text: 'Time Clock' }, - {route: './time-entries', icon: 'far fa-list-alt', text: 'Time Entries' }, - {route: './reports', icon: 'fas fa-chart-pie', text: 'Reports' }, - {route: './project-management', icon: 'far fa-folder-open', text: 'Projects' }, - {route: './activities-management', icon: 'far fa-file-alt', text: 'Activities' } + {route: '/time-clock', icon: 'far fa-clock', text: 'Time Clock' }, + {route: '/time-entries', icon: 'far fa-list-alt', text: 'Time Entries' }, + {route: '/reports', icon: 'fas fa-chart-pie', text: 'Reports' }, + {route: '/project-management', icon: 'far fa-folder-open', text: 'Projects' }, + {route: '/activities-management', icon: 'far fa-file-alt', text: 'Activities' } ]; } } diff --git a/src/app/modules/shared/components/user/user.component.html b/src/app/modules/shared/components/user/user.component.html index 76fed457e..a28505efd 100644 --- a/src/app/modules/shared/components/user/user.component.html +++ b/src/app/modules/shared/components/user/user.component.html @@ -1,19 +1,10 @@
- - diff --git a/src/app/modules/shared/components/user/user.component.spec.ts b/src/app/modules/shared/components/user/user.component.spec.ts index bf5e41717..6be7913c2 100644 --- a/src/app/modules/shared/components/user/user.component.spec.ts +++ b/src/app/modules/shared/components/user/user.component.spec.ts @@ -1,14 +1,28 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - +import { of } from 'rxjs'; import { UserComponent } from './user.component'; +import { AzureAdB2CService } from '../../../login/services/azure.ad.b2c.service'; describe('UserComponent', () => { let component: UserComponent; let fixture: ComponentFixture; + let azureAdB2CService: AzureAdB2CService; + + const azureAdB2CServiceStub = { + isLogin() { + return true; + }, + signIn() { + return of(); + } + }; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ UserComponent ] + declarations: [ UserComponent ], + providers: [ + { providers: AzureAdB2CService, useValue: azureAdB2CServiceStub} + ] }) .compileComponents(); })); @@ -17,9 +31,35 @@ describe('UserComponent', () => { fixture = TestBed.createComponent(UserComponent); component = fixture.componentInstance; fixture.detectChanges(); + azureAdB2CService = TestBed.inject(AzureAdB2CService); }); - it('should be created', () => { + it('component should be created', () => { expect(component).toBeTruthy(); }); + + it('onInit checks if isLogin and gets the name', () => { + spyOn(azureAdB2CService, 'isLogin').and.returnValue(true); + spyOn(azureAdB2CService, 'getName').and.returnValue('Name'); + component.ngOnInit(); + expect(azureAdB2CService.isLogin).toHaveBeenCalled(); + expect(azureAdB2CService.getName).toHaveBeenCalled(); + }); + + it('onInit does not get the name if isLogin false', () => { + spyOn(azureAdB2CService, 'isLogin').and.returnValue(false); + spyOn(azureAdB2CService, 'getName').and.returnValue('Name'); + component.ngOnInit(); + expect(azureAdB2CService.isLogin).toHaveBeenCalled(); + expect(azureAdB2CService.getName).toHaveBeenCalledTimes(0); + }); + + it('uses the Azure service on logout', () => { + spyOn(azureAdB2CService, 'logout'); + + component.logout(); + + expect(azureAdB2CService.logout).toHaveBeenCalled(); + }); + }); diff --git a/src/app/modules/shared/components/user/user.component.ts b/src/app/modules/shared/components/user/user.component.ts index 5df0c83d0..8848e9c6e 100644 --- a/src/app/modules/shared/components/user/user.component.ts +++ b/src/app/modules/shared/components/user/user.component.ts @@ -1,4 +1,5 @@ import { Component, OnInit } from '@angular/core'; +import { AzureAdB2CService } from '../../../login/services/azure.ad.b2c.service'; @Component({ selector: 'app-user', @@ -7,9 +8,18 @@ import { Component, OnInit } from '@angular/core'; }) export class UserComponent implements OnInit { - constructor() { } + name: string; + + constructor(private azureAdB2CService: AzureAdB2CService) { } ngOnInit(): void { + if (this.azureAdB2CService.isLogin()) { + this.name = this.azureAdB2CService.getName(); + } + } + + logout() { + this.azureAdB2CService.logout(); } } diff --git a/src/app/modules/shared/pipes/group-by-date/group-by-date.pipe.spec.ts b/src/app/modules/shared/pipes/group-by-date/group-by-date.pipe.spec.ts index b8b4906ff..c42f295a8 100644 --- a/src/app/modules/shared/pipes/group-by-date/group-by-date.pipe.spec.ts +++ b/src/app/modules/shared/pipes/group-by-date/group-by-date.pipe.spec.ts @@ -1,8 +1,74 @@ import { GroupByDatePipe } from './group-by-date.pipe'; +import { Pipe } from '@angular/core'; describe('GroupByDatePipe', () => { it('create an instance', () => { const pipe = new GroupByDatePipe(); expect(pipe).toBeTruthy(); }); + + it('create an instance', () => { + const pipe = new GroupByDatePipe(); + const date = '2020-02-05T15:36:15.887Z'; + const result = pipe.getDate(date); + expect(result).toEqual(new Date('2/5/2020').toLocaleDateString()); + }); + + it('should group two entries as one', () => { + const entryList = [ + { + id: 'entry_1', + project: 'Mido - 05 de Febrero', + startDate: '2020-02-05T15:36:15.887Z', + endDate: '2020-02-05T18:36:15.887Z', + activity: 'development', + technology: 'Angular, TypeScript', + comments: 'No comments', + ticket: 'EY-25' + }, + { + id: 'entry_2', + project: 'Mido 15 de Marzo', + startDate: '2020-03-15T20:36:15.887Z', + endDate: '2020-03-15T23:36:15.887Z', + activity: 'development', + technology: 'Angular, TypeScript', + comments: 'No comments', + ticket: 'EY-38' + }, + { + id: 'entry_3', + project: 'GoSpace 15 y 16 de Marzo', + startDate: '2020-03-15T23:36:15.887Z', + endDate: '2020-03-16T05:36:15.887Z', + activity: 'development', + technology: 'Angular, TypeScript', + comments: 'No comments', + ticket: 'EY-225' + }, + { + id: 'entry_4', + project: 'Mido 16 de Marzo', + startDate: '2020-03-16T15:36:15.887Z', + endDate: '2020-03-16T18:36:15.887Z', + activity: 'development', + technology: 'Angular, TypeScript', + comments: 'No comments', + ticket: 'EY-89' + }, + { + id: 'entry_5', + project: 'Ernst&Young 01 de Abril', + startDate: '2020-04-01T09:36:15.887Z', + endDate: '2020-04-01T15:36:15.887Z', + activity: 'development', + technology: 'Angular, TypeScript', + comments: 'No comments', + ticket: 'EY-59' + } + ]; + const pipe = new GroupByDatePipe(); + const result = pipe.transform(entryList); + expect(result.length).toBe(entryList.length - 1); + }); }); diff --git a/src/app/modules/time-entries/pages/time-entries.component.spec.ts b/src/app/modules/time-entries/pages/time-entries.component.spec.ts index b80ba7d36..4d5516a4d 100644 --- a/src/app/modules/time-entries/pages/time-entries.component.spec.ts +++ b/src/app/modules/time-entries/pages/time-entries.component.spec.ts @@ -61,7 +61,7 @@ describe('TimeEntriesComponent', () => { it('should call dataByMonth in ngOnInit()', async(() => { component.ngOnInit(); - expect(component.dataByMonth.length).toEqual(3); + expect(component.dataByMonth.length).toEqual(1); })); it('should open Delete Modal', () => { @@ -91,9 +91,9 @@ describe('TimeEntriesComponent', () => { }); it('should delete a Entry', () => { - const entryId = 'entry_2'; + const entryId = 'entry_5'; component.removeEntry(entryId); - expect(component.dataByMonth.length).toBe(2); + expect(component.dataByMonth.length).toBe(0); }); it('should get the entry List by Month', () => { diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 3612073bc..84b2f5826 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,3 +1,9 @@ +import * as keys from './.keys.json'; + export const environment = { production: true }; + +export const AUTHORITY = keys.authority; +export const CLIENT_ID = keys.client_id; +export const SCOPES = keys.scopes; \ No newline at end of file diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 7b4f817ad..3d84f6290 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -1,11 +1,16 @@ // This file can be replaced during build by using the `fileReplacements` array. // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. // The list of file replacements can be found in `angular.json`. +import * as keys from './.keys.json'; export const environment = { production: false }; +export const AUTHORITY = keys.authority; +export const CLIENT_ID = keys.client_id; +export const SCOPES = keys.scopes; + /* * For easier debugging in development mode, you can import the following file * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. diff --git a/src/environments/keys.example.json b/src/environments/keys.example.json new file mode 100644 index 000000000..c65a76e7f --- /dev/null +++ b/src/environments/keys.example.json @@ -0,0 +1,6 @@ +{ + "authority": "XXXXX", + "client_id": "XXXXX", + "scopes": ["XXXXX"] + } + \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 30956ae7e..582f5574d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,7 @@ }, "angularCompilerOptions": { "fullTemplateTypeCheck": true, - "strictInjectionParameters": true + "strictInjectionParameters": true, + "resolveJsonModule": true } }