From f5e34722f5f524592e6cd93299f0bc1a93cb88de Mon Sep 17 00:00:00 2001 From: Marcel Haag Date: Fri, 23 Dec 2022 13:52:27 +0100 Subject: [PATCH] feat: As a user I want to edit my comment --- .../objective-table.component.html | 1 - .../pentest-comments.component.ts | 77 +++++++++++++-- .../pentest-findings.component.ts | 3 +- .../src/assets/i18n/de-DE.json | 3 +- .../src/assets/i18n/en-US.json | 3 +- .../src/shared/models/comment.model.ts | 2 +- .../src/shared/models/generic-dialog-data.ts | 1 + .../comment-dialog.component.html | 15 +-- .../comment-dialog.component.spec.ts | 3 +- .../comment-dialog.component.ts | 92 ++++-------------- .../service/comment-dialog.service.mock.ts | 3 +- .../service/comment-dialog.service.ts | 25 ++++- .../src/shared/services/comment.service.ts | 18 ++++ .../security-c4po-api.postman_collection.json | 83 +++++++++++++++- .../src/main/asciidoc/SecurityC4PO.adoc | 47 ++++++++- .../api/pentest/comment/CommentController.kt | 23 ++++- .../api/pentest/comment/CommentService.kt | 73 ++++++++++++-- .../CommentControllerDocumentationTest.kt | 96 +++++++++++++++++++ ...ionTest.kt => CommentControllerIntTest.kt} | 49 +++++++++- .../FindingControllerDocumentationTest.kt | 2 +- .../finding/FindingControllerIntTest.kt | 4 +- 21 files changed, 500 insertions(+), 123 deletions(-) rename security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/{CommentControllerIntegrationTest.kt => CommentControllerIntTest.kt} (79%) diff --git a/security-c4po-angular/src/app/objective-overview/objective-table/objective-table.component.html b/security-c4po-angular/src/app/objective-overview/objective-table/objective-table.component.html index 656bed1..194518b 100644 --- a/security-c4po-angular/src/app/objective-overview/objective-table/objective-table.component.html +++ b/security-c4po-angular/src/app/objective-overview/objective-table/objective-table.component.html @@ -54,5 +54,4 @@ - diff --git a/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.ts b/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.ts index 194b9a4..3674bc8 100644 --- a/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.ts +++ b/security-c4po-angular/src/app/pentest/pentest-content/pentest-comments/pentest-comments.component.ts @@ -9,7 +9,7 @@ import {catchError, filter, mergeMap, switchMap, tap} from 'rxjs/operators'; import { Comment, CommentDialogBody, - CommentEntry, + CommentEntry, RelatedFindingOption, transformCommentsToObjectiveEntries, transformCommentToRequestBody } from '@shared/models/comment.model'; @@ -17,11 +17,13 @@ import {isNotNullOrUndefined} from 'codelyzer/util/isNotNullOrUndefined'; import {ProjectState} from '@shared/stores/project-state/project-state'; import {Store} from '@ngxs/store'; import {PentestStatus} from '@shared/models/pentest-status.model'; -import {FindingDialogComponent} from '@shared/modules/finding-dialog/finding-dialog.component'; import {DialogService} from '@shared/services/dialog-service/dialog.service'; import {CommentDialogService} from '@shared/modules/comment-dialog/service/comment-dialog.service'; import {CommentService} from '@shared/services/comment.service'; -import {UpdatePentestComments, UpdatePentestFindings} from '@shared/stores/project-state/project-state.actions'; +import {UpdatePentestComments} from '@shared/stores/project-state/project-state.actions'; +import {CommentDialogComponent} from '@shared/modules/comment-dialog/comment-dialog.component'; +import {Finding} from '@shared/models/finding.model'; +import {FindingService} from '@shared/services/finding.service'; @UntilDestroy() @Component({ @@ -36,6 +38,7 @@ export class PentestCommentsComponent implements OnInit { notStartedStatus: PentestStatus = PentestStatus.NOT_STARTED; pentestInfo$: BehaviorSubject = new BehaviorSubject(null); + objectiveFindings: RelatedFindingOption[] = []; // comments$: BehaviorSubject = new BehaviorSubject(null); loading$: BehaviorSubject = new BehaviorSubject(true); @@ -43,7 +46,6 @@ export class PentestCommentsComponent implements OnInit { CommentColumns.COMMENT_ID, CommentColumns.TITLE, CommentColumns.DESCRIPTION, CommentColumns.RELATED_FINDINGS, CommentColumns.ACTIONS ]; dataSource: NbTreeGridDataSource; - data: CommentEntry[] = []; getters: NbGetters = { @@ -53,6 +55,7 @@ export class PentestCommentsComponent implements OnInit { }; constructor(private readonly commentService: CommentService, + private readonly findingService: FindingService, private dataSourceBuilder: NbTreeGridDataSourceBuilder, private notificationService: NotificationService, private dialogService: DialogService, @@ -68,6 +71,7 @@ export class PentestCommentsComponent implements OnInit { next: (selectedPentest: Pentest) => { this.pentestInfo$.next(selectedPentest); this.loadCommentsData(); + this.requestFindingsData(selectedPentest.id); }, error: err => { console.error(err); @@ -103,8 +107,9 @@ export class PentestCommentsComponent implements OnInit { onClickAddComment(): void { this.commentDialogService.openCommentDialog( - FindingDialogComponent, + CommentDialogComponent, this.pentestInfo$.getValue().findingIds, + this.objectiveFindings, null, { closeOnEsc: false, @@ -114,7 +119,6 @@ export class PentestCommentsComponent implements OnInit { } ).pipe( filter(value => !!value), - tap((value) => console.warn('CommentDialogBody: ', value)), mergeMap((value: CommentDialogBody) => this.commentService.saveComment( this.pentestInfo$.getValue() ? this.pentestInfo$.getValue().id : '', @@ -126,19 +130,74 @@ export class PentestCommentsComponent implements OnInit { next: (newComment: Comment) => { this.store.dispatch(new UpdatePentestComments(newComment.id)); this.loadCommentsData(); - // Todo: Fix trans keys this.notificationService.showPopup('comment.popup.save.success', PopupType.SUCCESS); }, error: err => { console.error(err); - // Todo: Fix trans keys this.notificationService.showPopup('comment.popup.save.failed', PopupType.FAILURE); } }); } onClickEditComment(commentEntry): void { - console.info('Coming soon..', commentEntry); + this.commentService.getCommentById(commentEntry.data.commentId).pipe( + filter(isNotNullOrUndefined), + untilDestroyed(this) + ).subscribe({ + next: (existingComment: Comment) => { + if (existingComment) { + this.commentDialogService.openCommentDialog( + CommentDialogComponent, + this.pentestInfo$.getValue().findingIds, + this.objectiveFindings, + existingComment, + { + closeOnEsc: false, + hasScroll: false, + autoFocus: false, + closeOnBackdropClick: false + } + ).pipe( + filter(value => !!value), + mergeMap((value: CommentDialogBody) => + this.commentService.updateComment( + commentEntry.data.commentId, + transformCommentToRequestBody(value) + ) + ), + untilDestroyed(this) + ).subscribe({ + next: (updatedComment: Comment) => { + this.loadCommentsData(); + this.notificationService.showPopup('comment.popup.update.success', PopupType.SUCCESS); + }, + error: err => { + console.error(err); + this.notificationService.showPopup('comment.popup.update.failed', PopupType.FAILURE); + } + }); + } else { + this.notificationService.showPopup('comment.popup.not.available', PopupType.INFO); + } + }, + error: err => { + console.error(err); + } + }); + } + + requestFindingsData(pentestId: string): void { + this.objectiveFindings = []; + this.findingService.getFindingsByPentestId(pentestId).pipe( + untilDestroyed(this) + ).subscribe({ + next: (findings: Finding[]) => { + findings.forEach(finding => this.objectiveFindings.push({id: finding.id, title: finding.title} as RelatedFindingOption)); + }, + error: err => { + console.error(err); + } + }); } onClickDeleteComment(commentEntry): void { diff --git a/security-c4po-angular/src/app/pentest/pentest-content/pentest-findings/pentest-findings.component.ts b/security-c4po-angular/src/app/pentest/pentest-content/pentest-findings/pentest-findings.component.ts index 08f47fe..a2117f1 100644 --- a/security-c4po-angular/src/app/pentest/pentest-content/pentest-findings/pentest-findings.component.ts +++ b/security-c4po-angular/src/app/pentest/pentest-content/pentest-findings/pentest-findings.component.ts @@ -163,7 +163,6 @@ export class PentestFindingsComponent implements OnInit { untilDestroyed(this) ).subscribe({ next: (updatedFinding: Finding) => { - this.store.dispatch(new UpdatePentestFindings(updatedFinding.id)); this.loadFindingsData(); this.notificationService.showPopup('finding.popup.update.success', PopupType.SUCCESS); }, @@ -173,7 +172,7 @@ export class PentestFindingsComponent implements OnInit { } }); } else { - this.notificationService.showPopup('finding.popup.not.available', PopupType.FAILURE); + this.notificationService.showPopup('finding.popup.not.available', PopupType.INFO); } }, error: err => { diff --git a/security-c4po-angular/src/assets/i18n/de-DE.json b/security-c4po-angular/src/assets/i18n/de-DE.json index 1044e31..4ac1d99 100644 --- a/security-c4po-angular/src/assets/i18n/de-DE.json +++ b/security-c4po-angular/src/assets/i18n/de-DE.json @@ -181,7 +181,8 @@ "update.success": "Kommentar erfolgreich aktualisiert", "update.failed": "Kommentar konnte nicht aktualisiert werden", "delete.success": "Kommentar erfolgreich gelöscht", - "delete.failed": "Kommentar konnte nicht gelöscht werden" + "delete.failed": "Kommentar konnte nicht gelöscht werden", + "not.available": "Kommentar ist nicht mehr verfügbar" } }, "pentest": { diff --git a/security-c4po-angular/src/assets/i18n/en-US.json b/security-c4po-angular/src/assets/i18n/en-US.json index f08f30f..be3f87d 100644 --- a/security-c4po-angular/src/assets/i18n/en-US.json +++ b/security-c4po-angular/src/assets/i18n/en-US.json @@ -181,7 +181,8 @@ "update.success": "Comment updated successfully", "update.failed": "Comment could not be updated", "delete.success": "Comment deleted successfully", - "delete.failed": "Comment could not be deleted" + "delete.failed": "Comment could not be deleted", + "not.available": "Comment is not available anymore" } }, "pentest": { diff --git a/security-c4po-angular/src/shared/models/comment.model.ts b/security-c4po-angular/src/shared/models/comment.model.ts index 3d10c74..1d49c06 100644 --- a/security-c4po-angular/src/shared/models/comment.model.ts +++ b/security-c4po-angular/src/shared/models/comment.model.ts @@ -49,7 +49,7 @@ export function transformCommentToRequestBody(comment: CommentDialogBody | Comme title: comment.title, description: comment.description, // Transforms related findings from RelatedFindingOption to list of finding ids - relatedFindings: comment.relatedFindings ? comment.relatedFindings.map(finding => finding.value.id) : [], + relatedFindings: comment.relatedFindings ? comment.relatedFindings.map(finding => finding.id) : [], /* Remove Table Entry Object Properties */ childEntries: undefined, kind: undefined, diff --git a/security-c4po-angular/src/shared/models/generic-dialog-data.ts b/security-c4po-angular/src/shared/models/generic-dialog-data.ts index 6cede10..72af17f 100644 --- a/security-c4po-angular/src/shared/models/generic-dialog-data.ts +++ b/security-c4po-angular/src/shared/models/generic-dialog-data.ts @@ -23,4 +23,5 @@ export interface GenericFormFieldOption { headerLabelKey: string; buttonKey: string; accentColor: string; + additionalData?: any; } diff --git a/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.html b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.html index fcae7b1..f010830 100644 --- a/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.html +++ b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.html @@ -57,13 +57,16 @@ {{formArray[2].labelKey | translate}} - - {{'global.action.reset' | translate}} - - {{finding.title}} - + multiple fullWidth shape="semi-round" filled status="info" + [size]="'large'" class="form-field relatedFindings"> + {{'global.action.reset' | translate}} + + {{finding.title}} + diff --git a/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.spec.ts b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.spec.ts index 178b4d1..213c0a1 100644 --- a/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.spec.ts +++ b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.spec.ts @@ -182,7 +182,8 @@ export const mockedCommentDialogData = { { headerLabelKey: 'comment.create.header', buttonKey: 'global.action.save', - accentColor: 'info' + accentColor: 'info', + additionalData: [] }, ] }; diff --git a/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.ts b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.ts index c0c4cbd..cf5fca1 100644 --- a/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.ts +++ b/security-c4po-angular/src/shared/modules/comment-dialog/comment-dialog.component.ts @@ -1,18 +1,11 @@ -import {ChangeDetectionStrategy, Component, Inject, OnChanges, OnInit} from '@angular/core'; +import {ChangeDetectionStrategy, Component, Inject, OnInit} from '@angular/core'; import {FormBuilder, FormGroup} from '@angular/forms'; import {GenericDialogData, GenericFormFieldConfig} from '@shared/models/generic-dialog-data'; import * as FA from '@fortawesome/free-solid-svg-icons'; import deepEqual from 'deep-equal'; import {NB_DIALOG_CONFIG, NbDialogRef} from '@nebular/theme'; -import {PentestService} from '@shared/services/pentest.service'; -import {ProjectState} from '@shared/stores/project-state/project-state'; -import {UntilDestroy, untilDestroyed} from '@ngneat/until-destroy'; -import {Pentest} from '@shared/models/pentest.model'; -import {Store} from '@ngxs/store'; -import {Finding} from '@shared/models/finding.model'; +import {UntilDestroy} from '@ngneat/until-destroy'; import {RelatedFindingOption} from '@shared/models/comment.model'; -import {BehaviorSubject} from 'rxjs'; -import {FindingService} from '@shared/services/finding.service'; @Component({ selector: 'app-comment-dialog', @@ -39,28 +32,14 @@ export class CommentDialogComponent implements OnInit { constructor( @Inject(NB_DIALOG_CONFIG) private data: GenericDialogData, private fb: FormBuilder, - protected dialogRef: NbDialogRef, - private readonly findingService: FindingService, - private store: Store + protected dialogRef: NbDialogRef ) { } ngOnInit(): void { - this.commentFormGroup = this.generateFormCreationFieldArray(); this.dialogData = this.data; - // Resets related Findings input fields when comment was found in dialog context - // tslint:disable-next-line:no-string-literal - // this.commentFormGroup.controls['commentRelatedFindings'].reset(''); - } - - changeSelected($event): void { - // Latest Value - console.info($event[$event.length - 1].value); - // use this element at the end - // tslint:disable-next-line:no-string-literal - console.warn(this.commentFormGroup.controls['commentRelatedFindings'].value); - // tslint:disable-next-line:no-string-literal - this.selectedFindings = this.commentFormGroup.controls['commentRelatedFindings'].value; + this.relatedFindings = this.dialogData.options[0].additionalData; + this.commentFormGroup = this.generateFormCreationFieldArray(); } generateFormCreationFieldArray(): FormGroup { @@ -70,52 +49,27 @@ export class CommentDialogComponent implements OnInit { [currentValue?.fieldName]: currentValue?.controlsConfig }), {}); // tslint:disable-next-line:no-string-literal - const relatedFindings = this.data.form['commentRelatedFindings'].controlsConfig[0].value; - if (relatedFindings && relatedFindings.length > 0) { - // ToDo: Select included findings here (selectedFindings / initialSelectedFindings) - console.warn('IF (EDIT)', relatedFindings); - this.renderRelatedFindings(relatedFindings); - } else { - this.renderRelatedFindings([]); + const preSelectedRelatedFindings = this.data.form['commentRelatedFindings'].controlsConfig[0].value; + if (preSelectedRelatedFindings && preSelectedRelatedFindings.length > 0) { + this.relatedFindings.forEach(finding => { + if (preSelectedRelatedFindings.includes(finding)) { + this.initialSelectedFindings.push(finding); + this.selectedFindings.push(finding); + } + }); } return this.fb.group(config); } - renderRelatedFindings(relatedFindings: any): void { - this.store.select(ProjectState.pentest).pipe( - untilDestroyed(this) - ).subscribe({ - next: (selectedPentest: Pentest) => { - // console.warn(selectedPentest.findingIds); - this.requestRelatedFindingsData(selectedPentest.id, relatedFindings); - }, - error: err => { - console.error(err); - } - }); - } - - requestRelatedFindingsData(pentestId: string, relatedFindings: any): void { - this.findingService.getFindingsByPentestId(pentestId).pipe( - untilDestroyed(this) - ).subscribe({ - next: (findings: Finding[]) => { - findings.forEach(finding => this.relatedFindings.push({id: finding.id, title: finding.title} as RelatedFindingOption)); - // ToDo: Only add the findings that were included in - console.info('initialSelectedFindings OnINIT ', relatedFindings); - // findings.forEach(finding => this.initialSelectedFindings.push({id: finding.id, title: finding.title} as RelatedFindingOption)); - }, - error: err => { - console.error(err); - } - }); + changeSelected($event): void { + // tslint:disable-next-line:no-string-literal + this.selectedFindings = this.commentFormGroup.controls['commentRelatedFindings'].value; } onClickSave(value: any): void { this.dialogRef.close({ title: value.commentTitle, description: value.commentDescription, - // ToDo: Refactor this to only include the ids this.commentFormGroup.controls['commentRelatedFindings'].value relatedFindings: this.selectedFindings ? this.selectedFindings : [] }); } @@ -141,11 +95,6 @@ export class CommentDialogComponent implements OnInit { newCommentData[key] = ''; } }); - /* ToDo: Use for EDIT implementation - console.info('initialSelectedFindings: ', this.initialSelectedFindings); - console.info('selectedFindings: ', this.selectedFindings); - console.info('deepEqual related Findings: ', deepEqual(this.initialSelectedFindings, this.selectedFindings)); - */ const didChange = !deepEqual(oldCommentData, newCommentData) || !deepEqual(this.initialSelectedFindings, this.selectedFindings); return didChange; } @@ -155,17 +104,16 @@ export class CommentDialogComponent implements OnInit { * @return parsed findingData */ private parseInitializedCommentDialogData(dialogData: GenericDialogData): any { - const findingData = {}; + const commentData = {}; Object.entries(dialogData.form).forEach(entry => { const [key, value] = entry; - // console.info(key); - findingData[key] = value.controlsConfig[0] ? + commentData[key] = value.controlsConfig[0] ? (value.controlsConfig[0].value ? value.controlsConfig[0].value : value.controlsConfig[0]) : ''; // Related Findings form field can be ignored since changes here will be recognised inside commentRelatedFindings of tag-list if (key === 'commentRelatedFindings') { - findingData[key] = ''; + commentData[key] = ''; } }); - return findingData; + return commentData; } } diff --git a/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.mock.ts b/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.mock.ts index 853f3ec..aa90573 100644 --- a/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.mock.ts +++ b/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.mock.ts @@ -2,7 +2,7 @@ import {CommentDialogService} from '@shared/modules/comment-dialog/service/comme import {ComponentType} from '@angular/cdk/overlay'; import {NbDialogConfig} from '@nebular/theme'; import {Observable, of} from 'rxjs'; -import {Comment} from '@shared/models/comment.model'; +import {Comment, RelatedFindingOption} from '@shared/models/comment.model'; export class CommentDialogServiceMock implements Required { @@ -11,6 +11,7 @@ export class CommentDialogServiceMock implements Required openCommentDialog( componentOrTemplateRef: ComponentType, findingIds: [], + relatedFindings: RelatedFindingOption[], comment: Comment | undefined, config: Partial | string>> | undefined): Observable { return of(undefined); diff --git a/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.ts b/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.ts index b49e009..3e40ba8 100644 --- a/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.ts +++ b/security-c4po-angular/src/shared/modules/comment-dialog/service/comment-dialog.service.ts @@ -1,11 +1,11 @@ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; import {NbDialogConfig, NbDialogService} from '@nebular/theme'; import {GenericDialogData} from '@shared/models/generic-dialog-data'; import {ComponentType} from '@angular/cdk/overlay'; import {Observable} from 'rxjs'; import {Validators} from '@angular/forms'; import {CommentDialogComponent} from '@shared/modules/comment-dialog/comment-dialog.component'; -import {Comment} from '@shared/models/comment.model'; +import {Comment, RelatedFindingOption} from '@shared/models/comment.model'; @Injectable() export class CommentDialogService { @@ -30,10 +30,20 @@ export class CommentDialogService { public openCommentDialog(componentOrTemplateRef: ComponentType, findingIds: string[], + relatedFindings: RelatedFindingOption[], comment?: Comment, config?: Partial | string>>): Observable { let dialogOptions: Partial | string>>; let dialogData: GenericDialogData; + // Preselect related findings + const selectedRelatedFindings: RelatedFindingOption[] = []; + if (comment && comment.relatedFindings.length > 0 && relatedFindings) { + relatedFindings.forEach(finding => { + if (comment.relatedFindings.includes(finding.id)) { + selectedRelatedFindings.push(finding); + } + }); + } // Setup CommentDialogBody dialogData = { form: { @@ -69,7 +79,10 @@ export class CommentDialogService { labelKey: 'comment.relatedFindings.label', placeholder: findingIds.length === 0 ? 'comment.noFindingsInObjectivePlaceholder' : 'comment.relatedFindingsPlaceholder', controlsConfig: [ - {value: comment ? comment.relatedFindings : [], disabled: findingIds.length === 0}, + { + value: comment ? selectedRelatedFindings : [], + disabled: findingIds.length === 0 + }, [] ], errors: [ @@ -84,7 +97,8 @@ export class CommentDialogService { { headerLabelKey: 'comment.edit.header', buttonKey: 'global.action.update', - accentColor: 'warning' + accentColor: 'warning', + additionalData: relatedFindings }, ]; } else { @@ -92,7 +106,8 @@ export class CommentDialogService { { headerLabelKey: 'comment.create.header', buttonKey: 'global.action.save', - accentColor: 'info' + accentColor: 'info', + additionalData: relatedFindings }, ]; } diff --git a/security-c4po-angular/src/shared/services/comment.service.ts b/security-c4po-angular/src/shared/services/comment.service.ts index dbd888e..4b7bc29 100644 --- a/security-c4po-angular/src/shared/services/comment.service.ts +++ b/security-c4po-angular/src/shared/services/comment.service.ts @@ -4,6 +4,7 @@ import {HttpClient} from '@angular/common/http'; import {Store} from '@ngxs/store'; import {Observable} from 'rxjs'; import {Comment} from '@shared/models/comment.model'; +import {Finding} from '@shared/models/finding.model'; @Injectable({ providedIn: 'root' @@ -26,6 +27,14 @@ export class CommentService { return this.http.get(`${this.apiBaseURL}/${pentestId}/comments`); } + /** + * Get Comment by Id + * @param commentId the id of the comment + */ + public getCommentById(commentId: string): Observable { + return this.http.get(`${this.apiBaseURL}/${commentId}/comment`); + } + /** * Save Comment * @param pentestId the id of the pentest @@ -35,6 +44,15 @@ export class CommentService { return this.http.post(`${this.apiBaseURL}/${pentestId}/comment`, comment); } + /** + * Update Comment + * @param commentId the id of the comment + * @param comment the information of the comment + */ + public updateComment(commentId: string, comment: Comment): Observable { + return this.http.patch(`${this.apiBaseURL}/${commentId}/comment`, comment); + } + /** * Delete Comment * @param pentestId the id of the pentest diff --git a/security-c4po-api/security-c4po-api.postman_collection.json b/security-c4po-api/security-c4po-api.postman_collection.json index 92bb33c..6f7b2ac 100644 --- a/security-c4po-api/security-c4po-api.postman_collection.json +++ b/security-c4po-api/security-c4po-api.postman_collection.json @@ -481,7 +481,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Test Comment\",\n \"description\": \"Test Comment Description\",\n \"affectedUrls\": []\n}", + "raw": "{\n \"title\": \"Test Comment\",\n \"description\": \"Test Comment Description\",\n \"relatedFindings\": []\n}", "options": { "raw": { "language": "json" @@ -512,7 +512,7 @@ "bearer": [ { "key": "token", - "value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzE3MTM3MzQsImlhdCI6MTY3MTcxMzQzNCwiYXV0aF90aW1lIjoxNjcxNzEyNjkwLCJqdGkiOiJjNWYxYWZiZi1mZTczLTQ0NTAtYjA4YS1lMGEwMDcyNjMyOTgiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0Ojg4ODgvYXV0aC9yZWFsbXMvYzRwb19yZWFsbV9sb2NhbCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiIxMGUwNmQ3YS04ZGQwLTRlY2QtODk2My0wNTZiNDUwNzljNGYiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJjNHBvX2xvY2FsIiwibm9uY2UiOiIyZTYyNjNhNC1lM2U2LTRlMzUtYjQ5Yy1lMjYyNzM1ZTk2MGQiLCJzZXNzaW9uX3N0YXRlIjoiMGNmYmY4MGEtNzAxMS00NmQzLTllNGQtNTUxYWU4NTA5NjZmIiwiYWNyIjoiMCIsImFsbG93ZWQtb3JpZ2lucyI6WyIqIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjNHBvX3VzZXIiLCJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsiYzRwb19sb2NhbCI6eyJyb2xlcyI6WyJ1c2VyIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50IiwibWFuYWdlLWFjY291bnQtbGlua3MiLCJ2aWV3LXByb2ZpbGUiXX19LCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIGVtYWlsIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoidGVzdCB1c2VyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidHR0IiwiZ2l2ZW5fbmFtZSI6InRlc3QiLCJmYW1pbHlfbmFtZSI6InVzZXIifQ.se43hq_vPjzAG6MpIxBiHb9vJHZmbLEko0tiN5m2hbhzd8s3YiBWpeiI6kgZ5kzl23iBQyMnXN4Sqpbt2ERKbKyUusezWcXhGTP22usi3b1vzFOAY9mqCI32i15sxCM2UDRYDFYcAblaKPxKsQf6EWduXpcn4L9_kQE4EpoLyWWWqFThGvFPSvkPGodffcEOz8BrnYDVUnwkodFsOWAnQmQHaR7jq1Y0hhZzWi3IlrRWlnRi0TKVWCZgUwO0PJttNq5wYZPsxgiS-khUCC1qtbKrRgBK_3sefxPkWDOQEubu0Kjyjq4rVZnq66anO3Qw82CSLn0nSCu-AL5Xd4Xchw", + "value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzIyMzc2MjQsImlhdCI6MTY3MjIzNzMyNCwianRpIjoiMTI2MTBmOWUtMjY1OC00ZmE3LTlkNTItZGU4NzNiZDVjZGE3IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiI4NzQ5MTFkNS00ZTZlLTRjNmMtYmQxYy1hMzlmZjE2OTI0YWIiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImM0cG9fdXNlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJjNHBvX2xvY2FsIjp7InJvbGVzIjpbInVzZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6InRlc3QgdXNlciIsInByZWZlcnJlZF91c2VybmFtZSI6InR0dCIsImdpdmVuX25hbWUiOiJ0ZXN0IiwiZmFtaWx5X25hbWUiOiJ1c2VyIn0.FOZNfhuWutaKUsVnr3A2jxJ6PM7fpX5BdB63SI_3CI7LFgnVmruprQC3ibhYGD77yX59yRWlGlIDlXvybd6v4tujKEL1Kuf4J-MSjj3dJcIx29PtqMe91I49MkIsjr3M24YW4bgOtdbUTYvT1l0IUisW1V_-t23qW_tsbXxviNr_9HSiJYZJZ7a47tmEptJaDZtAwjBaQc8s4BVIqiPbIcYE1Mj1Giu56C3k_v_boSxcl3rrRMXgIWTSO4TtV_jm2UfERzp82B6dZYdq0ZxeyaD0nCzXSkZ41pOeqFK4Qsm6ttCKe5OJ-RGpqvi0KH4YNuhUKHpzisVOL0cPDiWmLg", "type": "string" }, { @@ -574,6 +574,85 @@ } }, "response": [] + }, + { + "name": "getCommentById", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzIyMzc2MjQsImlhdCI6MTY3MjIzNzMyNCwianRpIjoiMTI2MTBmOWUtMjY1OC00ZmE3LTlkNTItZGU4NzNiZDVjZGE3IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiI4NzQ5MTFkNS00ZTZlLTRjNmMtYmQxYy1hMzlmZjE2OTI0YWIiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImM0cG9fdXNlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJjNHBvX2xvY2FsIjp7InJvbGVzIjpbInVzZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6InRlc3QgdXNlciIsInByZWZlcnJlZF91c2VybmFtZSI6InR0dCIsImdpdmVuX25hbWUiOiJ0ZXN0IiwiZmFtaWx5X25hbWUiOiJ1c2VyIn0.FOZNfhuWutaKUsVnr3A2jxJ6PM7fpX5BdB63SI_3CI7LFgnVmruprQC3ibhYGD77yX59yRWlGlIDlXvybd6v4tujKEL1Kuf4J-MSjj3dJcIx29PtqMe91I49MkIsjr3M24YW4bgOtdbUTYvT1l0IUisW1V_-t23qW_tsbXxviNr_9HSiJYZJZ7a47tmEptJaDZtAwjBaQc8s4BVIqiPbIcYE1Mj1Giu56C3k_v_boSxcl3rrRMXgIWTSO4TtV_jm2UfERzp82B6dZYdq0ZxeyaD0nCzXSkZ41pOeqFK4Qsm6ttCKe5OJ-RGpqvi0KH4YNuhUKHpzisVOL0cPDiWmLg", + "type": "string" + }, + { + "key": "undefined", + "type": "any" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8443/pentests/df516de6-ca5e-44a6-ac50-db89bb17aac3/comment", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8443", + "path": [ + "pentests", + "df516de6-ca5e-44a6-ac50-db89bb17aac3", + "comment" + ] + } + }, + "response": [] + }, + { + "name": "updateComment", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICItdG1lbEV0ZHhGTnRSMW9aNXlRdE5jaFFpX0RVN2VNeV9YcU44aXY0S3hzIn0.eyJleHAiOjE2NzA0MTQ3ODYsImlhdCI6MTY3MDQxNDQ4NiwianRpIjoiM2FmOWU5M2MtY2YzNi00MjQwLTkzNWEtNDkxYTJkZTY2MWU4IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4ODg4L2F1dGgvcmVhbG1zL2M0cG9fcmVhbG1fbG9jYWwiLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMTBlMDZkN2EtOGRkMC00ZWNkLTg5NjMtMDU2YjQ1MDc5YzRmIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiYzRwb19sb2NhbCIsInNlc3Npb25fc3RhdGUiOiI5M2ExNTBlMC03ZWRkLTQxZTgtYWE4Yi0yZWY5YTgzOWU4NDciLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbImM0cG9fdXNlciIsIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iXX0sInJlc291cmNlX2FjY2VzcyI6eyJjNHBvX2xvY2FsIjp7InJvbGVzIjpbInVzZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6InRlc3QgdXNlciIsInByZWZlcnJlZF91c2VybmFtZSI6InR0dCIsImdpdmVuX25hbWUiOiJ0ZXN0IiwiZmFtaWx5X25hbWUiOiJ1c2VyIn0.QjUkCInyCJ5Wsz4q56gfsLqERr6pYlGjwNw-VsKNJ_3Jp-8Dazq9UmDGN8AmAkQ0sp0b-FMm3jArKMBpr84gKd65trvQx_qHvXev5x2MWBG4_9v3C9MmjxWcAYRVmfRdURUOhfto-4YfRwMwNRsKJfwMIjfS5VT8bHJWipcCDzaidN8h_LLORbmmQZ2o0l4Jnv5qrrWzUcSTeEeBpHGOjes1-T0gOlDJa34Z9x_xrsTsybKAylrmX03mDSI-f2h5XqqtgnrxtddtHXHatfxB1BHWq-FILDsGf0UG47FEQjqapFvn9bFiNyq0GVrgdK42miEO7ywOtCOKpCfAUnMwdQ", + "type": "string" + }, + { + "key": "undefined", + "type": "any" + } + ] + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Test Comment\",\n \"description\": \"Edited Test Comment Description\",\n \"relatedFindings\": []\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8443/pentests/df516de6-ca5e-44a6-ac50-db89bb17aac3/comment", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8443", + "path": [ + "pentests", + "df516de6-ca5e-44a6-ac50-db89bb17aac3", + "comment" + ] + } + }, + "response": [] } ] }, diff --git a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc index a959d4a..8d0a385 100644 --- a/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc +++ b/security-c4po-api/src/main/asciidoc/SecurityC4PO.adoc @@ -317,20 +317,63 @@ include::{snippets}/deleteCommentByPentestAndCommentId/http-response.adoc[] include::{snippets}/deleteCommentByPentestAndCommentId/response-fields.adoc[] +=== Get comment for id + +To get a comment by id, call the GET request /pentests/+{commentId}+/comment. + +==== Request example + +include::{snippets}/getCommentById/http-request.adoc[] + +==== Request structure + +include::{snippets}/getCommentById/path-parameters.adoc[] + +==== Response example + +include::{snippets}/getCommentById/http-response.adoc[] + +==== Response structure + +include::{snippets}/getCommentById/response-fields.adoc[] + +=== Update comment + +To update a comment, call the PATCH request /pentests/+{commentId}+/comment + +==== Request example + +include::{snippets}/updateCommentById/http-request.adoc[] + +==== Request structure + +include::{snippets}/updateCommentById/path-parameters.adoc[] + +==== Response example + +include::{snippets}/updateCommentById/http-response.adoc[] + +==== Response structure + +include::{snippets}/updateCommentById/response-fields.adoc[] + + == Change History |=== |Date |Change +|2022-12-28 +|Added GET, PATCH endpoint for Comment |2022-12-23 |Added DELETE endpoint for Comment |2022-12-22 -|Added GET, POST endpoint for Comment +|Added GET, POST endpoint for Comment(s) |2022-12-09 |Added DELETE endpoint for Finding |2022-12-08 |Added GET and PATCH endpoint for Finding |2022-12-02 -|Added GET and POST endpoint for Findings +|Added GET and POST endpoint for Finding(s) |2022-11-21 |Added GET, POST and PATCH endpoint for Pentests |2022-03-07 diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/comment/CommentController.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/comment/CommentController.kt index 628f7f3..ff12531 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/comment/CommentController.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/comment/CommentController.kt @@ -4,11 +4,7 @@ import com.securityc4po.api.configuration.BC_BAD_CAST_TO_ABSTRACT_COLLECTION import com.securityc4po.api.extensions.getLoggerFor import edu.umd.cs.findbugs.annotations.SuppressFBWarnings import com.securityc4po.api.ResponseBody -import com.securityc4po.api.configuration.error.handler.EntityNotFoundException -import com.securityc4po.api.configuration.error.handler.Errorcode -import com.securityc4po.api.pentest.finding.toFindingResponseBody import com.securityc4po.api.pentest.PentestService -import com.securityc4po.api.pentest.finding.toFindingDeleteResponseBody import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity.noContent import org.springframework.web.bind.annotation.* @@ -40,6 +36,13 @@ class CommentController(private val pentestService: PentestService, private val } } + @GetMapping("/{commentId}/comment") + fun getComment(@PathVariable(value = "commentId") commentId: String):Mono> { + return this.commentService.getCommentById(commentId).map { + ResponseEntity.ok().body(it.toCommentResponseBody()) + } + } + @PostMapping("/{pentestId}/comment") fun saveComment( @PathVariable(value = "pentestId") pentestId: String, @@ -50,8 +53,18 @@ class CommentController(private val pentestService: PentestService, private val } } + @PatchMapping("/{commentId}/comment") + fun updateComment( + @PathVariable(value = "commentId") commentId: String, + @RequestBody body: CommentRequestBody + ): Mono> { + return this.commentService.updateComment(commentId, body).map { + ResponseEntity.accepted().body(it.toCommentResponseBody()) + } + } + @DeleteMapping("/{pentestId}/comment/{commentId}") - fun deleteFinding( + fun deleteComment( @PathVariable(value = "pentestId") pentestId: String, @PathVariable(value = "commentId") commentId: String ): Mono> { diff --git a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/comment/CommentService.kt b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/comment/CommentService.kt index 513e945..f267628 100644 --- a/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/comment/CommentService.kt +++ b/security-c4po-api/src/main/kotlin/com/securityc4po/api/pentest/comment/CommentService.kt @@ -8,10 +8,12 @@ import com.securityc4po.api.configuration.error.handler.InvalidModelException import com.securityc4po.api.configuration.error.handler.TransactionInterruptedException import com.securityc4po.api.extensions.getLoggerFor import com.securityc4po.api.pentest.PentestService +import com.securityc4po.api.pentest.finding.* import edu.umd.cs.findbugs.annotations.SuppressFBWarnings import org.springframework.stereotype.Service import reactor.core.publisher.Mono import reactor.kotlin.core.publisher.switchIfEmpty +import java.time.Instant @Service @SuppressFBWarnings(BC_BAD_CAST_TO_ABSTRACT_COLLECTION, MESSAGE_BAD_CAST_TO_ABSTRACT_COLLECTION) @@ -19,6 +21,36 @@ class CommentService(private val commentRepository: CommentRepository, private v var logger = getLoggerFor() + /** + * Get all [Comments]s by commentId's + * + * @return list of [Comment]s + */ + fun getCommentsByIds(commentIds: List): Mono> { + return commentRepository.findCommentsByIds(commentIds).collectList().map { + it.map { commentEntity -> commentEntity.toComment() } + }.switchIfEmpty { + val msg = "Comment not found." + val ex = EntityNotFoundException(msg, Errorcode.CommentsNotFound) + logger.warn(msg, ex) + throw ex + } + } + + /** + * Get [Comment] by commentId + * + * @return of [Comment] + */ + fun getCommentById(commentId: String): Mono { + return this.commentRepository.findCommentById(commentId).switchIfEmpty { + logger.warn("Comment with id $commentId not found.") + val msg = "Comment with id $commentId not found." + val ex = EntityNotFoundException(msg, Errorcode.CommentNotFound) + throw ex + }.map { it.toComment() } + } + /** * Save [Comment] * @@ -38,7 +70,7 @@ class CommentService(private val commentRepository: CommentRepository, private v val commentEntity = CommentEntity(comment) return commentRepository.insert(commentEntity).flatMap { newCommentEntity: CommentEntity -> val comment = newCommentEntity.toComment() - // After successfully saving finding add id to pentest + // After successfully saving comment add id to pentest pentestService.updatePentestComment(pentestId, comment.id).onErrorMap { TransactionInterruptedException( "Pentest could not be updated in Database.", @@ -59,18 +91,39 @@ class CommentService(private val commentRepository: CommentRepository, private v } /** - * Get all [Comments]s by commentId's + * Update [Comment] * - * @return list of [Comment]s + * @throws [InvalidModelException] if the [Comment] is invalid + * @throws [TransactionInterruptedException] if the [Comment] could not be stored + * @return saved [Comment] */ - fun getCommentsByIds(commentIds: List): Mono> { - return commentRepository.findCommentsByIds(commentIds).collectList().map { - it.map { commentEntity -> commentEntity.toComment() } - }.switchIfEmpty { - val msg = "Comment not found." - val ex = EntityNotFoundException(msg, Errorcode.CommentsNotFound) - logger.warn(msg, ex) + fun updateComment(commentId: String, body: CommentRequestBody): Mono { + validate( + require = body.isValid(), + logging = { logger.warn("Comment not valid.") }, + mappedException = InvalidModelException( + "Comment not valid.", Errorcode.CommentInvalid + ) + ) + return this.commentRepository.findCommentById(commentId).switchIfEmpty { + logger.warn("Comment with id $commentId not found.") + val msg = "Comment with id $commentId not found." + val ex = EntityNotFoundException(msg, Errorcode.CommentNotFound) throw ex + }.flatMap { currentCommentEntity: CommentEntity -> + currentCommentEntity.lastModified = Instant.now() + currentCommentEntity.data = buildComment(body, currentCommentEntity) + commentRepository.save(currentCommentEntity).map { + it.toComment() + } + }.doOnError { + throw wrappedException( + logging = { logger.warn("Comment could not be updated in Database. Thrown exception: ", it) }, + mappedException = TransactionInterruptedException( + "Comment could not be stored.", + Errorcode.CommentInsertionFailed + ) + ) } } diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerDocumentationTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerDocumentationTest.kt index ee68d0b..957abf7 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerDocumentationTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerDocumentationTest.kt @@ -101,6 +101,54 @@ class CommentControllerDocumentationTest : BaseDocumentationIntTest() { ) } + @Nested + inner class GetComment { + @Test + fun getCommentById() { + val commentId = "ab62d365-1b1d-4da1-89bc-5496616e220f" + webTestClient.get() + .uri("/pentests/{commentId}/comment", commentId) + .header("Authorization", "Bearer $tokenAdmin") + .exchange() + .expectStatus().isOk + .expectHeader().doesNotExist("") + .expectBody().json(Json.write(commentOne.toCommentResponseBody())) + .consumeWith( + WebTestClientRestDocumentation.document( + "{methodName}", + Preprocessors.preprocessRequest( + Preprocessors.prettyPrint(), + Preprocessors.modifyUris().removePort(), + Preprocessors.removeHeaders("Host", "Content-Length") + ), + Preprocessors.preprocessResponse( + Preprocessors.prettyPrint() + ), + RequestDocumentation.relaxedPathParameters( + RequestDocumentation.parameterWithName("commentId").description("The id of the comment you want to get") + ), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING) + .description("The id of the requested comment"), + PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING) + .description("The title of the requested comment"), + PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING) + .description("The description number of the comment"), + PayloadDocumentation.fieldWithPath("relatedFindings").type(JsonFieldType.ARRAY) + .description("List of related findings of the comment") + ) + ) + ) + } + + private val commentOne = Comment( + id = "ab62d365-1b1d-4da1-89bc-5496616e220f", + title = "Found Bug", + description = "OTG-INFO-002 Bug", + relatedFindings = emptyList() + ) + } + @Nested inner class SaveComment { @Test @@ -149,6 +197,54 @@ class CommentControllerDocumentationTest : BaseDocumentationIntTest() { ) } + @Nested + inner class UpdateFinding { + @Test + fun updateCommentById() { + val commentId = "ab62d365-1b1d-4da1-89bc-5496616e220f" + webTestClient.patch() + .uri("/pentests/{commentId}/comment", commentId) + .header("Authorization", "Bearer $tokenAdmin") + .body(Mono.just(commentBody), CommentRequestBody::class.java) + .exchange() + .expectStatus().isAccepted + .expectHeader().doesNotExist("") + .expectBody().json(Json.write(commentBody)) + .consumeWith( + WebTestClientRestDocumentation.document( + "{methodName}", + Preprocessors.preprocessRequest( + Preprocessors.prettyPrint(), + Preprocessors.modifyUris().removePort(), + Preprocessors.removeHeaders("Host", "Content-Length") + ), + Preprocessors.preprocessResponse( + Preprocessors.prettyPrint() + ), + RequestDocumentation.relaxedPathParameters( + RequestDocumentation.parameterWithName("commentId").description("The id of the comment you want to update") + ), + PayloadDocumentation.relaxedResponseFields( + PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING) + .description("The id of the updated comment"), + PayloadDocumentation.fieldWithPath("title").type(JsonFieldType.STRING) + .description("The title of the requested comment"), + PayloadDocumentation.fieldWithPath("description").type(JsonFieldType.STRING) + .description("The description number of the comment"), + PayloadDocumentation.fieldWithPath("relatedFindings").type(JsonFieldType.ARRAY) + .description("List of related findings of the comment") + ) + ) + ) + } + + private val commentBody = CommentRequestBody( + title = "Updated Comment", + description = "Updated Description", + relatedFindings = emptyList() + ) + } + @Nested inner class DeleteComment { @Test diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerIntegrationTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerIntTest.kt similarity index 79% rename from security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerIntegrationTest.kt rename to security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerIntTest.kt index b56d150..ee09542 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerIntegrationTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/comment/CommentControllerIntTest.kt @@ -29,7 +29,7 @@ import java.time.Duration NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR, RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE ) -class CommentControllerIntegrationTest: BaseIntTest() { +class CommentControllerIntTest : BaseIntTest() { @LocalServerPort private var port = 0 @@ -85,6 +85,28 @@ class CommentControllerIntegrationTest: BaseIntTest() { ) } + @Nested + inner class GetComment { + @Test + fun `requesting comment by commentId successfully`() { + val commentId = "ab62d365-1b1d-4da1-89bc-5496616e220f" + webTestClient.get() + .uri("/pentests/{commentId}/comment", commentId) + .header("Authorization", "Bearer $tokenAdmin") + .exchange() + .expectStatus().isOk + .expectHeader().valueEquals("Application-Name", "SecurityC4PO") + .expectBody().json(Json.write(commentOne.toCommentResponseBody())) + } + + private val commentOne = Comment( + id = "ab62d365-1b1d-4da1-89bc-5496616e220f", + title = "Found Bug", + description = "OTG-INFO-002 Bug", + relatedFindings = emptyList() + ) + } + @Nested inner class SaveComment { @Test @@ -110,6 +132,31 @@ class CommentControllerIntegrationTest: BaseIntTest() { ) } + @Nested + inner class UpdateComment { + @Test + fun `update comment successfully`() { + val commentId = "ab62d365-1b1d-4da1-89bc-5496616e220f" + webTestClient.patch() + .uri("/pentests/{commentId}/comment", commentId) + .header("Authorization", "Bearer $tokenAdmin") + .body(Mono.just(commentBody), CommentRequestBody::class.java) + .exchange() + .expectStatus().isAccepted + .expectHeader().valueEquals("Application-Name", "SecurityC4PO") + .expectBody() + .jsonPath("$.title").isEqualTo("Updated Comment") + .jsonPath("$.description").isEqualTo("Updated Description") + .jsonPath("$.relatedFindings").isEmpty + } + + private val commentBody = CommentRequestBody( + title = "Updated Comment", + description = "Updated Description", + relatedFindings = emptyList() + ) + } + @Nested inner class DeleteComment { @Test diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/finding/FindingControllerDocumentationTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/finding/FindingControllerDocumentationTest.kt index 056bd36..1863fc5 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/finding/FindingControllerDocumentationTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/finding/FindingControllerDocumentationTest.kt @@ -137,7 +137,7 @@ class FindingControllerDocumentationTest: BaseDocumentationIntTest() { Preprocessors.prettyPrint() ), RequestDocumentation.relaxedPathParameters( - RequestDocumentation.parameterWithName("findingId").description("The id of the feinidng you want to get") + RequestDocumentation.parameterWithName("findingId").description("The id of the finding you want to get") ), PayloadDocumentation.relaxedResponseFields( PayloadDocumentation.fieldWithPath("id").type(JsonFieldType.STRING) diff --git a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/finding/FindingControllerIntTest.kt b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/finding/FindingControllerIntTest.kt index c4b6907..69f202f 100644 --- a/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/finding/FindingControllerIntTest.kt +++ b/security-c4po-api/src/test/kotlin/com/securityc4po/api/pentest/finding/FindingControllerIntTest.kt @@ -152,8 +152,8 @@ class FindingControllerIntTest: BaseIntTest() { inner class UpdateFinding { @Test fun `update finding successfully`() { - val findingId = "43fbc63c-f624-11ec-b939-0242ac120002" - webTestClient.post() + val findingId = "ab62d365-1b1d-4da1-89bc-5496616e220f" + webTestClient.patch() .uri("/pentests/{findingId}/finding", findingId) .header("Authorization", "Bearer $tokenAdmin") .body(Mono.just(findingBody), FindingRequestBody::class.java)