diff --git a/security-c4po-angular/src/app/app.module.ts b/security-c4po-angular/src/app/app.module.ts index e3bc9bc..19e02b6 100644 --- a/security-c4po-angular/src/app/app.module.ts +++ b/security-c4po-angular/src/app/app.module.ts @@ -6,7 +6,7 @@ import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import { NbLayoutModule, NbToastrModule, - NbIconModule, NbCardModule, NbButtonModule, + NbIconModule, NbCardModule, NbButtonModule, NbDialogService, NbDialogModule, } from '@nebular/theme'; import {NbEvaIconsModule} from '@nebular/eva-icons'; import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; @@ -26,6 +26,7 @@ import {HomeModule} from './home/home.module'; import {KeycloakService} from 'keycloak-angular'; import {httpInterceptorProviders} from '@shared/interceptors'; import {FlexLayoutModule} from '@angular/flex-layout'; +import {DialogService} from '@shared/services/dialog-service/dialog.service'; @NgModule({ declarations: [ @@ -53,6 +54,7 @@ import {FlexLayoutModule} from '@angular/flex-layout'; deps: [HttpClient] } }), + NbDialogModule.forRoot(), HeaderModule, HomeModule, FlexLayoutModule @@ -67,7 +69,9 @@ import {FlexLayoutModule} from '@angular/flex-layout'; }, KeycloakService, httpInterceptorProviders, - NotificationService + NotificationService, + NbDialogService, + DialogService ], bootstrap: [ AppComponent diff --git a/security-c4po-angular/src/app/login/login.component.spec.ts b/security-c4po-angular/src/app/login/login.component.spec.ts index fa2f99c..faaaa72 100644 --- a/security-c4po-angular/src/app/login/login.component.spec.ts +++ b/security-c4po-angular/src/app/login/login.component.spec.ts @@ -48,7 +48,6 @@ describe('LoginComponent', () => { BrowserAnimationsModule, ReactiveFormsModule, NbInputModule, - NbCardModule, NbButtonModule, NbLayoutModule, ThemeModule.forRoot(), diff --git a/security-c4po-angular/src/app/project-overview/project-overview.component.spec.ts b/security-c4po-angular/src/app/project-overview/project-overview.component.spec.ts index 062e6c2..a800ff4 100644 --- a/security-c4po-angular/src/app/project-overview/project-overview.component.spec.ts +++ b/security-c4po-angular/src/app/project-overview/project-overview.component.spec.ts @@ -22,6 +22,8 @@ import {ProjectServiceMock} from '@shared/services/project.service.mock'; import {ThemeModule} from '@assets/@theme/theme.module'; import {LoadingSpinnerComponent} from '@shared/widgets/loading-spinner/loading-spinner.component'; import {KeycloakService} from 'keycloak-angular'; +import {DialogService} from '@shared/services/dialog-service/dialog.service'; +import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock'; describe('ProjectOverviewComponent', () => { let component: ProjectOverviewComponent; @@ -61,6 +63,7 @@ describe('ProjectOverviewComponent', () => { providers: [ KeycloakService, {provide: ProjectService, useValue: new ProjectServiceMock()}, + {provide: DialogService, useClass: DialogServiceMock}, {provide: NotificationService, useValue: new NotificationServiceMock()} ] }) diff --git a/security-c4po-angular/src/app/project-overview/project-overview.component.ts b/security-c4po-angular/src/app/project-overview/project-overview.component.ts index f8ddbac..75704e7 100644 --- a/security-c4po-angular/src/app/project-overview/project-overview.component.ts +++ b/security-c4po-angular/src/app/project-overview/project-overview.component.ts @@ -1,11 +1,14 @@ import {Component, OnDestroy, OnInit} from '@angular/core'; import * as FA from '@fortawesome/free-solid-svg-icons'; -import {Project} from '@shared/models/project.model'; +import {Project, SaveProjectDialogBody} from '@shared/models/project.model'; import {BehaviorSubject, Observable} from 'rxjs'; import {untilDestroyed} from 'ngx-take-until-destroy'; import {ProjectService} from '@shared/services/project.service'; import {NotificationService, PopupType} from '@shared/services/notification.service'; -import {tap} from 'rxjs/operators'; +import {filter, mergeMap, tap} from 'rxjs/operators'; +import {DialogService} from '@shared/services/dialog-service/dialog.service'; +import {ProjectDialogComponent} from '@shared/modules/project-dialog/project-dialog.component'; +import {NB_DIALOG_CONFIG} from '@nebular/theme/components/dialog/dialog-config'; @Component({ selector: 'app-project-overview', @@ -21,20 +24,21 @@ export class ProjectOverviewComponent implements OnInit, OnDestroy { constructor( private readonly projectService: ProjectService, + private readonly dialogService: DialogService, private readonly notificationService: NotificationService) { } ngOnInit(): void { - this.getProjects(); + this.loadProjects(); } - getProjects(): void { + loadProjects(): void { this.projectService.getProjects() .pipe( untilDestroyed(this), tap(() => this.loading$.next(true)) - ) - .subscribe( { + ) + .subscribe({ next: (projects) => { this.projects.next(projects); this.loading$.next(false); @@ -48,7 +52,28 @@ export class ProjectOverviewComponent implements OnInit, OnDestroy { } onClickAddProject(): void { - console.log('to be implemented...'); + this.dialogService.openCustomDialog( + ProjectDialogComponent, + { + closeOnEsc: false, + hasScroll: false, + autoFocus: false, + closeOnBackdropClick: false + } + ).onClose.pipe( + filter(value => !!value), + mergeMap((value: SaveProjectDialogBody) => this.projectService.saveProject(value)), + untilDestroyed(this) + ).subscribe({ + next: () => { + this.loadProjects(); + this.notificationService.showPopup('project.popup.save.success', PopupType.SUCCESS); + }, + error: error => { + console.error(error); + this.notificationService.showPopup('project.popup.save.failed', PopupType.FAILURE); + } + }); } onClickEditProject(): void { diff --git a/security-c4po-angular/src/app/project-overview/project-overview.module.ts b/security-c4po-angular/src/app/project-overview/project-overview.module.ts index c8ac3b5..2c55822 100644 --- a/security-c4po-angular/src/app/project-overview/project-overview.module.ts +++ b/security-c4po-angular/src/app/project-overview/project-overview.module.ts @@ -2,12 +2,15 @@ import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {ProjectOverviewComponent} from './project-overview.component'; import {ProjectOverviewRoutingModule} from './project-overview-routing.module'; -import {NbButtonModule, NbCardModule, NbProgressBarModule} from '@nebular/theme'; +import {NbButtonModule, NbCardModule, NbDialogService, NbProgressBarModule} from '@nebular/theme'; import {FlexLayoutModule} from '@angular/flex-layout'; import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; import {TranslateModule} from '@ngx-translate/core'; import {DateTimeFormatPipe} from '@shared/pipes/date-time-format.pipe'; import {ProjectModule} from './project'; +import {ProjectDialogComponent} from '@shared/modules/project-dialog/project-dialog.component'; +import {DialogService} from '@shared/services/dialog-service/dialog.service'; +import {ProjectDialogModule} from '@shared/modules/project-dialog/project-dialog.module'; @NgModule({ declarations: [ @@ -23,7 +26,12 @@ import {ProjectModule} from './project'; FontAwesomeModule, TranslateModule, NbProgressBarModule, - ProjectModule + ProjectModule, + ProjectDialogModule + ], + providers: [ + DialogService, + NbDialogService ] }) export class ProjectOverviewModule { diff --git a/security-c4po-angular/src/assets/i18n/de-DE.json b/security-c4po-angular/src/assets/i18n/de-DE.json index 65a9cb8..1fbf84b 100644 --- a/security-c4po-angular/src/assets/i18n/de-DE.json +++ b/security-c4po-angular/src/assets/i18n/de-DE.json @@ -2,6 +2,8 @@ "global": { "action.login": "Einloggen", "action.retry": "Erneut Versuchen", + "action.save": "Speichern", + "action.cancel": "Abbrechen", "username": "Nutzername", "password": "Passwort" }, @@ -28,9 +30,18 @@ "add.project": "Projekt hinzufügen", "no.projects": "Keine Projekte verfügbar" }, - "popup": { - "not.found": "Keine Projekte gefunden" + "create": { + "header": "Neues Projekt erstellen" }, + "popup": { + "not.found": "Keine Projekte gefunden", + "save.success": "Projekt erfolgreich gespeichert", + "save.failed": "Projekt konnte nicht gespeichert werden" + }, + "title.label": "Projekt Titel", + "client.label": "Name des Auftraggebers", + "tester.label": "Name des Pentester", + "title": "Titel", "client": "Klient", "tester": "Tester", "createdAt": "Erstellt am" diff --git a/security-c4po-angular/src/assets/i18n/en-US.json b/security-c4po-angular/src/assets/i18n/en-US.json index 9e7d8a5..b7c1f4e 100644 --- a/security-c4po-angular/src/assets/i18n/en-US.json +++ b/security-c4po-angular/src/assets/i18n/en-US.json @@ -2,6 +2,8 @@ "global": { "action.login": "Login", "action.retry": "Try again", + "action.save": "Save", + "action.cancel": "Cancel", "username": "Username", "password": "Password" }, @@ -28,9 +30,18 @@ "add.project": "Add project", "no.projects": "No projects available" }, - "popup": { - "not.found": "No projects found" + "create": { + "header": "Create New Project" }, + "popup": { + "not.found": "No projects found", + "save.success": "Project saved successfully", + "save.failed": "Project could not be saved" + }, + "title.label": "Project Title", + "client.label": "Name of Client", + "tester.label": "Name of Pentester", + "title": "Title", "client": "Client", "tester": "Tester", "createdAt": "Created at" diff --git a/security-c4po-angular/src/shared/models/project.model.ts b/security-c4po-angular/src/shared/models/project.model.ts index 971f868..8741e42 100644 --- a/security-c4po-angular/src/shared/models/project.model.ts +++ b/security-c4po-angular/src/shared/models/project.model.ts @@ -20,3 +20,9 @@ export class Project { this.createdBy = createdBy; } } + +export interface SaveProjectDialogBody { + title: string; + client: string; + tester: string; +} diff --git a/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.html b/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.html new file mode 100644 index 0000000..a4f6b5a --- /dev/null +++ b/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.html @@ -0,0 +1,49 @@ + + + {{ 'project.create.header' | translate }} + + +
+ + + + + + + + + + + + + + +
+
+ + + + +
diff --git a/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.scss b/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.scss new file mode 100644 index 0000000..a1ef017 --- /dev/null +++ b/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.scss @@ -0,0 +1,23 @@ +.project-dialog { + width: 24rem; + height: 31rem; + + .project-dialog-header { + height: 8vh; + font-size: 1.5rem; + } + + nb-form-field { + padding: 0.5rem 0 0.75rem; + } + + .label { + display: block; + font-size: 0.95rem; + padding-bottom: 0.5rem; + } + + .input { + width: 15rem; + } +} diff --git a/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.spec.ts b/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.spec.ts new file mode 100644 index 0000000..4f0802e --- /dev/null +++ b/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.spec.ts @@ -0,0 +1,66 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ProjectDialogComponent} from './project-dialog.component'; +import {CommonModule} from '@angular/common'; +import {NbButtonModule, NbCardModule, NbDialogRef, NbFormFieldModule, NbInputModule, NbLayoutModule} from '@nebular/theme'; +import {FlexLayoutModule} from '@angular/flex-layout'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {ThemeModule} from '@assets/@theme/theme.module'; +import {TranslateLoader, TranslateModule} from '@ngx-translate/core'; +import {HttpLoaderFactory} from '../../../app/common-app.module'; +import {HttpClient, HttpClientModule} from '@angular/common/http'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NotificationService} from '@shared/services/notification.service'; +import {NotificationServiceMock} from '@shared/services/notification.service.mock'; +import {DialogService} from '@shared/services/dialog-service/dialog.service'; +import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock'; +import {ReactiveFormsModule} from '@angular/forms'; + +describe('ProjectDialogComponent', () => { + let component: ProjectDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ + ProjectDialogComponent + ], + imports: [ + CommonModule, + NbLayoutModule, + NbCardModule, + NbButtonModule, + FlexLayoutModule, + NbInputModule, + NbFormFieldModule, + ReactiveFormsModule, + BrowserAnimationsModule, + ThemeModule.forRoot(), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient] + } + }), + HttpClientModule, + HttpClientTestingModule + ], + providers: [ + {provide: NotificationService, useValue: new NotificationServiceMock()}, + {provide: DialogService, useClass: DialogServiceMock}, + {provide: NbDialogRef, useValue: {}} + ] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProjectDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.ts b/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.ts new file mode 100644 index 0000000..2d7a718 --- /dev/null +++ b/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.component.ts @@ -0,0 +1,82 @@ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {NbDialogRef} from '@nebular/theme'; +import {AbstractControl, FormBuilder, FormGroup, Validators} from '@angular/forms'; +import {FieldStatus} from '@shared/models/form-field-status.model'; +import {untilDestroyed} from 'ngx-take-until-destroy'; + +@Component({ + selector: 'app-project-dialog', + templateUrl: './project-dialog.component.html', + styleUrls: ['./project-dialog.component.scss'] +}) +export class ProjectDialogComponent implements OnInit, OnDestroy { + // form control elements + projectFormGroup: FormGroup; + projectTitleCtrl: AbstractControl; + projectClientCtrl: AbstractControl; + projectTesterCtrl: AbstractControl; + + formCtrlStatus = FieldStatus.BASIC; + + invalidProjectTitle: string; + invalidProjectClient: string; + invalidProjectTester: string; + + readonly MIN_LENGTH: number = 2; + + constructor( + private fb: FormBuilder, + protected dialogRef: NbDialogRef + ) { + } + + ngOnInit(): void { + this.projectFormGroup = this.fb.group({ + projectTitle: ['', [Validators.required, Validators.minLength(this.MIN_LENGTH)]], + projectClient: ['', [Validators.required, Validators.minLength(this.MIN_LENGTH)]], + projectTester: ['', [Validators.required, Validators.minLength(this.MIN_LENGTH)]] + }); + + this.projectTitleCtrl = this.projectFormGroup.get('projectTitle'); + this.projectClientCtrl = this.projectFormGroup.get('projectClient'); + this.projectTesterCtrl = this.projectFormGroup.get('projectTester'); + + this.projectFormGroup.valueChanges + .pipe(untilDestroyed(this)) + .subscribe(() => { + this.formCtrlStatus = FieldStatus.BASIC; + }); + } + + onClickSave(value): void { + this.dialogRef.close({ + title: value.projectTitle, + client: value.projectClient, + tester: value.projectTester + }); + } + + onClickClose(): void { + this.dialogRef.close(); + } + + formIsEmptyOrInvalid(): boolean { + return this.isEmpty(this.projectTitleCtrl.value) + || this.isEmpty(this.projectClientCtrl.value) + || this.isEmpty(this.projectTesterCtrl.value) + || this.projectTitleCtrl.invalid + || this.projectClientCtrl.invalid + || this.projectTesterCtrl.invalid; + } + + /** + * @param ctrlValue of type string + * @return if ctrlValue is empty or not + */ + isEmpty(ctrlValue: string): boolean { + return ctrlValue === ''; + } + + ngOnDestroy(): void { + } +} diff --git a/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.module.ts b/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.module.ts new file mode 100644 index 0000000..6e46c61 --- /dev/null +++ b/security-c4po-angular/src/shared/modules/project-dialog/project-dialog.module.ts @@ -0,0 +1,34 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import {ProjectDialogComponent} from '@shared/modules/project-dialog/project-dialog.component'; +import {NbButtonModule, NbCardModule, NbDialogService, NbFormFieldModule, NbInputModule} from '@nebular/theme'; +import {FlexLayoutModule} from '@angular/flex-layout'; +import {FontAwesomeModule} from '@fortawesome/angular-fontawesome'; +import {TranslateModule} from '@ngx-translate/core'; +import {DialogService} from '@shared/services/dialog-service/dialog.service'; +import {ReactiveFormsModule} from '@angular/forms'; + +@NgModule({ + declarations: [ + ProjectDialogComponent + ], + imports: [ + CommonModule, + NbCardModule, + NbButtonModule, + FlexLayoutModule, + FontAwesomeModule, + TranslateModule, + ReactiveFormsModule, + NbFormFieldModule, + NbInputModule, + ], + providers: [ + DialogService, + NbDialogService + ], + entryComponents: [ + ProjectDialogComponent + ] +}) +export class ProjectDialogModule { } diff --git a/security-c4po-angular/src/shared/services/dialog-service/dialog.service.mock.ts b/security-c4po-angular/src/shared/services/dialog-service/dialog.service.mock.ts new file mode 100644 index 0000000..c37ba51 --- /dev/null +++ b/security-c4po-angular/src/shared/services/dialog-service/dialog.service.mock.ts @@ -0,0 +1,16 @@ +import {DialogService} from '@shared/services/dialog-service/dialog.service'; +import {ComponentType} from '@angular/cdk/overlay'; +import {TemplateRef} from '@angular/core'; +import {NbDialogConfig, NbDialogRef} from '@nebular/theme'; + +export class DialogServiceMock implements Required { + + dialog: any; + + openCustomDialog( + componentOrTemplateRef: ComponentType | TemplateRef, + config?: Partial | string>> + ): NbDialogRef { + return null; + } +} diff --git a/security-c4po-angular/src/shared/services/dialog-service/dialog.service.spec.ts b/security-c4po-angular/src/shared/services/dialog-service/dialog.service.spec.ts new file mode 100644 index 0000000..9653480 --- /dev/null +++ b/security-c4po-angular/src/shared/services/dialog-service/dialog.service.spec.ts @@ -0,0 +1,30 @@ +import {TestBed} from '@angular/core/testing'; + +import {DialogService} from './dialog.service'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NbDialogModule, NbDialogRef} from '@nebular/theme'; +import {DialogServiceMock} from '@shared/services/dialog-service/dialog.service.mock'; + +describe('DialogService', () => { + let service: DialogService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + HttpClientTestingModule, + BrowserAnimationsModule, + NbDialogModule.forRoot() + ], + providers: [ + {provide: DialogService, useClass: DialogServiceMock}, + {provide: NbDialogRef, useValue: {}}, + ] + }); + service = TestBed.inject(DialogService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/security-c4po-angular/src/shared/services/dialog-service/dialog.service.ts b/security-c4po-angular/src/shared/services/dialog-service/dialog.service.ts new file mode 100644 index 0000000..9d04a9a --- /dev/null +++ b/security-c4po-angular/src/shared/services/dialog-service/dialog.service.ts @@ -0,0 +1,28 @@ +import {Injectable, TemplateRef} from '@angular/core'; +import {NbDialogConfig, NbDialogRef, NbDialogService} from '@nebular/theme'; +import {ComponentType} from '@angular/cdk/overlay'; + +@Injectable({ + providedIn: 'root' +}) +export class DialogService { + + constructor(private dialog: NbDialogService) { + } + + /** + * Opens a custom MatDialog + */ + openCustomDialog( + componentOrTemplateRef: ComponentType | TemplateRef, + config?: Partial | string>> + ): NbDialogRef { + return this.dialog.open(componentOrTemplateRef, { + context: config?.context || undefined, + closeOnEsc: config?.closeOnEsc || false, + hasScroll: config?.hasScroll || false, + autoFocus: config?.autoFocus || false, + closeOnBackdropClick: config?.closeOnBackdropClick || false, + }); + } +} diff --git a/security-c4po-angular/src/shared/services/notification.service.ts b/security-c4po-angular/src/shared/services/notification.service.ts index 4d1aac2..6311208 100644 --- a/security-c4po-angular/src/shared/services/notification.service.ts +++ b/security-c4po-angular/src/shared/services/notification.service.ts @@ -17,7 +17,7 @@ export class NotificationService { .subscribe((translationContainer) => { this.toastrService.show( '', - translationContainer[popupType] + ' ' + translationContainer[translationKey], { + translationContainer[translationKey] + ' ' + translationContainer[popupType], { position: NbGlobalPhysicalPosition.BOTTOM_RIGHT, duration: 5000, toastClass: createCssClassName(popupType) diff --git a/security-c4po-angular/src/shared/services/project.service.mock.ts b/security-c4po-angular/src/shared/services/project.service.mock.ts index 150d583..d2c7833 100644 --- a/security-c4po-angular/src/shared/services/project.service.mock.ts +++ b/security-c4po-angular/src/shared/services/project.service.mock.ts @@ -11,4 +11,8 @@ export class ProjectServiceMock implements Required { getProjects(): Observable { return of([]); } + + saveProject(): Observable { + return of(); + } } diff --git a/security-c4po-angular/src/shared/services/project.service.spec.ts b/security-c4po-angular/src/shared/services/project.service.spec.ts index 51c314c..9943c95 100644 --- a/security-c4po-angular/src/shared/services/project.service.spec.ts +++ b/security-c4po-angular/src/shared/services/project.service.spec.ts @@ -1,12 +1,18 @@ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { ProjectService } from './project.service'; -import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ProjectService} from './project.service'; +import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import {KeycloakService} from 'keycloak-angular'; +import {Project, SaveProjectDialogBody} from '@shared/models/project.model'; +import {environment} from '../../environments/environment'; describe('ProjectService', () => { let service: ProjectService; + let httpMock: HttpTestingController; + + const apiBaseURL = `${environment.apiEndpoint}/projects`; + const dummyDate = new Date('2019-01-10T09:00:00'); beforeEach(() => { TestBed.configureTestingModule({ @@ -19,9 +25,89 @@ describe('ProjectService', () => { ] }); service = TestBed.inject(ProjectService); + httpMock = TestBed.inject(HttpTestingController); }); it('should be created', () => { expect(service).toBeTruthy(); }); + + describe('getProjects', () => { + const mockProject: Project = { + id: '56c47c56-3bcd-45f1-a05b-c197dbd33111', + client: 'E Corp', + title: 'Some Mock API (v1.0) Scanning', + createdAt: dummyDate, + tester: 'Novatester', + createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110' + }; + + const httpResponse = [{ + id: '56c47c56-3bcd-45f1-a05b-c197dbd33111', + client: 'E Corp', + title: 'Some Mock API (v1.0) Scanning', + createdAt: dummyDate, + tester: 'Novatester', + createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110' + }]; + + it('should get Projects', (done) => { + service.getProjects().subscribe((projects) => { + expect(projects[0].id).toEqual(mockProject.id); + expect(projects[0].client).toEqual(mockProject.client); + expect(projects[0].title).toEqual(mockProject.title); + expect(projects[0].createdAt).toBe(mockProject.createdAt); + expect(projects[0].tester).toEqual(mockProject.tester); + expect(projects[0].createdBy).toEqual(mockProject.createdBy); + done(); + }); + + const mockReq = httpMock.expectOne(`${apiBaseURL}`); + expect(mockReq.cancelled).toBe(false); + expect(mockReq.request.responseType).toEqual('json'); + mockReq.flush(httpResponse); + + httpMock.verify(); + }); + }); + + describe('saveProject', () => { + const mockSaveProjectDialogBody: SaveProjectDialogBody = { + client: 'E Corp', + title: 'Some Mock API (v1.0) Scanning', + tester: 'Novatester', + }; + + const mockProject: Project = { + id: '56c47c56-3bcd-45f1-a05b-c197dbd33111', + client: 'E Corp', + title: 'Some Mock API (v1.0) Scanning', + createdAt: dummyDate, + tester: 'Novatester', + createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110' + }; + + const httpResponse = { + id: '56c47c56-3bcd-45f1-a05b-c197dbd33111', + client: 'E Corp', + title: 'Some Mock API (v1.0) Scanning', + createdAt: dummyDate, + tester: 'Novatester', + createdBy: '11c47c56-3bcd-45f1-a05b-c197dbd33110' + }; + + it('should save project', (done) => { + + service.saveProject(mockSaveProjectDialogBody).subscribe( + value => { + expect(value).toEqual(mockProject); + done(); + }, + fail); + + const req = httpMock.expectOne(`${apiBaseURL}`); + expect(req.request.method).toBe('POST'); + req.flush(mockProject); + }); + }); }); diff --git a/security-c4po-angular/src/shared/services/project.service.ts b/security-c4po-angular/src/shared/services/project.service.ts index 83ae315..2ebc3ee 100644 --- a/security-c4po-angular/src/shared/services/project.service.ts +++ b/security-c4po-angular/src/shared/services/project.service.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; import {environment} from '../../environments/environment'; import {HttpClient} from '@angular/common/http'; -import {Project} from '../models/project.model'; +import {Project, SaveProjectDialogBody} from '../models/project.model'; import {Observable} from 'rxjs'; @Injectable({ @@ -17,4 +17,12 @@ export class ProjectService { public getProjects(): Observable { return this.http.get(`${this.apiBaseURL}`); } + + /** + * Save Project + * @param project the information of the project + */ + public saveProject(project: SaveProjectDialogBody): Observable { + return this.http.post(`${this.apiBaseURL}`, project); + } } diff --git a/security-c4po-api/security-c4po-api.postman_collection.json b/security-c4po-api/security-c4po-api.postman_collection.json index 5c1fe42..165b433 100644 --- a/security-c4po-api/security-c4po-api.postman_collection.json +++ b/security-c4po-api/security-c4po-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "58021f5f-0ae9-4f64-990b-f09dcc2d3bc2", + "_postman_id": "a20516f0-5f7f-4d15-9f26-ada358993ff8", "name": "security-c4po-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -71,7 +71,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"client\": \"Novatec\",\n \"title\": \"log4j Pentest\",\n \"tester\": \"Stipe\",\n \"createyBy\": \"10e06d7a-8dd0-4ecd-8963-056b45079c4f\"\n}", + "raw": "{\n \"client\": \"Novatec\",\n \"title\": \"log4j Pentest\",\n \"tester\": \"Stipe\"\n}", "options": { "raw": { "language": "json" diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/Project.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/Project.kt index 4f4328c..b5686ad 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/Project.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/Project.kt @@ -41,8 +41,7 @@ fun ProjectOverview.toProjectOverviewResponseBody(): ResponseBody { data class ProjectRequestBody( val client: String, val title: String, - val tester: String? = null, - val createdBy: String + val tester: String? = null ) fun ProjectRequestBody.toProject(): Project { @@ -52,6 +51,7 @@ fun ProjectRequestBody.toProject(): Project { title = this.title, createdAt = Instant.now().toString(), tester = this.tester, - createdBy = this.createdBy + // ToDo: Should be changed to SUB from Token after adding AUTH Header + createdBy = UUID.randomUUID().toString() ) } diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectController.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectController.kt index 812d4ef..9ae7a50 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectController.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/project/ProjectController.kt @@ -7,6 +7,7 @@ import com.securityc4po.api.ResponseBody import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.* import reactor.core.publisher.Mono +import java.util.* @RestController @RequestMapping("/projects") diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerDocumentationTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerDocumentationTest.kt index 73e424f..79dd7d0 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerDocumentationTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerDocumentationTest.kt @@ -45,49 +45,60 @@ class ProjectControllerDocumentationTest : BaseDocumentationIntTest() { @Test fun getProjects() { webTestClient.get().uri("/projects") - .header("Authorization", "Bearer $tokenAdmin") - .exchange() - .expectStatus().isOk - .expectHeader().doesNotExist("") - .expectBody().json(Json.write(getProjectsResponse())) - .consumeWith(WebTestClientRestDocumentation.document("{methodName}", - Preprocessors.preprocessRequest(Preprocessors.prettyPrint(), - Preprocessors.modifyUris().removePort(), - Preprocessors.removeHeaders("Host", "Content-Length")), - Preprocessors.preprocessResponse( - Preprocessors.prettyPrint() - ), - PayloadDocumentation.relaxedResponseFields( - PayloadDocumentation.fieldWithPath("[].id").type(JsonFieldType.STRING).description("The id of the requested project"), - PayloadDocumentation.fieldWithPath("[].client").type(JsonFieldType.STRING).description("The name of the client of the requested project"), - PayloadDocumentation.fieldWithPath("[].title").type(JsonFieldType.STRING).description("The title of the requested project"), - PayloadDocumentation.fieldWithPath("[].createdAt").type(JsonFieldType.STRING).description("The date where the project was created at"), - PayloadDocumentation.fieldWithPath("[].tester").type(JsonFieldType.STRING).description("The user that is assigned as a tester in the project"), - PayloadDocumentation.fieldWithPath("[].createdBy").type(JsonFieldType.STRING).description("The id of the user that created the project") - ) - )) + .header("Authorization", "Bearer $tokenAdmin") + .exchange() + .expectStatus().isOk + .expectHeader().doesNotExist("") + .expectBody().json(Json.write(getProjectsResponse())) + .consumeWith( + WebTestClientRestDocumentation.document( + "{methodName}", + Preprocessors.preprocessRequest( + Preprocessors.prettyPrint(), + Preprocessors.modifyUris().removePort(), + Preprocessors.removeHeaders("Host", "Content-Length") + ), + Preprocessors.preprocessResponse( + Preprocessors.prettyPrint() + ), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation.fieldWithPath("[].id").type(JsonFieldType.STRING) + .description("The id of the requested project"), + PayloadDocumentation.fieldWithPath("[].client").type(JsonFieldType.STRING) + .description("The name of the client of the requested project"), + PayloadDocumentation.fieldWithPath("[].title").type(JsonFieldType.STRING) + .description("The title of the requested project"), + PayloadDocumentation.fieldWithPath("[].createdAt").type(JsonFieldType.STRING) + .description("The date where the project was created at"), + PayloadDocumentation.fieldWithPath("[].tester").type(JsonFieldType.STRING) + .description("The user that is assigned as a tester in the project"), + PayloadDocumentation.fieldWithPath("[].createdBy").type(JsonFieldType.STRING) + .description("The id of the user that created the project") + ) + ) + ) } val projectOne = Project( - id = "4f6567a8-76fd-487b-8602-f82d0ca4d1f9", - client = "E Corp", - title = "Some Mock API (v1.0) Scanning", - createdAt = "2021-01-10T18:05:00Z", - tester = "Novatester", - createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032" + id = "4f6567a8-76fd-487b-8602-f82d0ca4d1f9", + client = "E Corp", + title = "Some Mock API (v1.0) Scanning", + createdAt = "2021-01-10T18:05:00Z", + tester = "Novatester", + createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032" ) val projectTwo = Project( - id = "61360a47-796b-4b3f-abf9-c46c668596c5", - client = "Allsafe", - title = "CashMyData (iOS)", - createdAt = "2021-01-10T18:05:00Z", - tester = "Elliot", - createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032" + id = "61360a47-796b-4b3f-abf9-c46c668596c5", + client = "Allsafe", + title = "CashMyData (iOS)", + createdAt = "2021-01-10T18:05:00Z", + tester = "Elliot", + createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032" ) private fun getProjectsResponse() = listOf( - projectOne.toProjectResponseBody(), - projectTwo.toProjectResponseBody() + projectOne.toProjectResponseBody(), + projectTwo.toProjectResponseBody() ) } @@ -102,29 +113,39 @@ class ProjectControllerDocumentationTest : BaseDocumentationIntTest() { .expectStatus().isAccepted .expectHeader().valueEquals("Application-Name", "SecurityC4PO") .expectBody().json(Json.write(project)) - .consumeWith(WebTestClientRestDocumentation.document("{methodName}", - Preprocessors.preprocessRequest(Preprocessors.prettyPrint(), - Preprocessors.modifyUris().removePort(), - Preprocessors.removeHeaders("Host", "Content-Length")), - Preprocessors.preprocessResponse( - Preprocessors.prettyPrint() - ), - PayloadDocumentation.relaxedResponseFields( - PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING).description("The id of the requested project"), - PayloadDocumentation.fieldWithPath("client").type(JsonFieldType.STRING).description("The name of the client of the requested project"), - PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING).description("The title of the requested project"), - PayloadDocumentation.fieldWithPath("createdAt").type(JsonFieldType.STRING).description("The date where the project was created at"), - PayloadDocumentation.fieldWithPath("tester").type(JsonFieldType.STRING).description("The user that is assigned as a tester in the project"), - PayloadDocumentation.fieldWithPath("createdBy").type(JsonFieldType.STRING).description("The id of the user that created the project") + .consumeWith( + WebTestClientRestDocumentation.document( + "{methodName}", + Preprocessors.preprocessRequest( + Preprocessors.prettyPrint(), + Preprocessors.modifyUris().removePort(), + Preprocessors.removeHeaders("Host", "Content-Length") + ), + Preprocessors.preprocessResponse( + Preprocessors.prettyPrint() + ), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING) + .description("The id of the requested project"), + PayloadDocumentation.fieldWithPath("client").type(JsonFieldType.STRING) + .description("The name of the client of the requested project"), + PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING) + .description("The title of the requested project"), + PayloadDocumentation.fieldWithPath("createdAt").type(JsonFieldType.STRING) + .description("The date where the project was created at"), + PayloadDocumentation.fieldWithPath("tester").type(JsonFieldType.STRING) + .description("The user that is assigned as a tester in the project"), + PayloadDocumentation.fieldWithPath("createdBy").type(JsonFieldType.STRING) + .description("The id of the user that created the project") + ) ) - )) + ) } val project = ProjectRequestBody( client = "Novatec", title = "log4j Pentest", - tester = "Stipe", - createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032" + tester = "Stipe" ) } diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerIntTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerIntTest.kt index 4c2dc87..aae009e 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerIntTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/project/ProjectControllerIntTest.kt @@ -98,11 +98,12 @@ class ProjectControllerIntTest : BaseIntTest() { .exchange() .expectStatus().isAccepted .expectHeader().valueEquals("Application-Name", "SecurityC4PO") - .expectBody().json(Json.write(project)) + .expectBody() .jsonPath("$.client").isEqualTo("Novatec") .jsonPath("$.title").isEqualTo("log4j Pentest") .jsonPath("$.tester").isEqualTo("Stipe") - .jsonPath("$.createdBy").isEqualTo("f8aab31f-4925-4242-a6fa-f98135b4b032") + // ToDo: Should be changed to SUB from Token after adding AUTH Header + /*.jsonPath("$.createdBy").isEqualTo("f8aab31f-4925-4242-a6fa-f98135b4b032")*/ } val project = Project( @@ -111,7 +112,7 @@ class ProjectControllerIntTest : BaseIntTest() { title = "log4j Pentest", createdAt = "2021-04-10T18:05:00Z", tester = "Stipe", - createdBy = "f8aab31f-4925-4242-a6fa-f98135b4b032" + createdBy = "a8891ad2-5cf5-4519-a89e-9ef8eec9e10c" ) }