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 @@
+
+
+
+
+
+
+
+
+
+
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"
)
}