From ccb5444ad171e087d44ba17f1adcc5030d2bb5d5 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Thu, 11 Jan 2024 17:52:27 +0100 Subject: [PATCH 1/7] add base template and validator --- .../ldn-service-form.component.html | 23 ++++++++++++++++ .../ldn-service-form.component.ts | 5 ++++ src/app/shared/utils/ipV4.validator.ts | 27 +++++++++++++++++++ src/assets/i18n/en.json5 | 3 +++ 4 files changed, 58 insertions(+) create mode 100644 src/app/shared/utils/ipV4.validator.ts diff --git a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html index 52b877c0cf..289285b080 100644 --- a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html +++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html @@ -48,6 +48,29 @@ + +
+ +
+ + +
+ +
+ {{ 'ldn-new-service.form.error.ipRange' | translate }} +
+
+
diff --git a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts index b7e0f8e7eb..ba654538a3 100644 --- a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts +++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts @@ -29,6 +29,7 @@ import {combineLatestWith, Observable, Subscription} from 'rxjs'; import {PaginationService} from '../../../core/pagination/pagination.service'; import {FindListOptions} from '../../../core/data/find-list-options.model'; import {NotifyServicePattern} from '../ldn-services-model/ldn-service-patterns.model'; +import { IpV4Validator } from '../../../shared/utils/ipV4.validator'; /** @@ -63,6 +64,8 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy { @Input() public description: string; @Input() public url: string; @Input() public ldnUrl: string; + @Input() public ipRangeMin: string; + @Input() public ipRangeMax: string; @Input() public score: number; @Input() public inboundPattern: string; @Input() public constraint: string; @@ -99,6 +102,8 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy { description: [''], url: ['', Validators.required], ldnUrl: ['', Validators.required], + ipRangeMin: ['', [Validators.required, new IpV4Validator()]], + ipRangeMax: ['', [new IpV4Validator()]], score: ['', [Validators.required, Validators.pattern('^0*(\.[0-9]+)?$|^1(\.0+)?$')]], inboundPattern: [''], constraintPattern: [''], enabled: [''], diff --git a/src/app/shared/utils/ipV4.validator.ts b/src/app/shared/utils/ipV4.validator.ts new file mode 100644 index 0000000000..66b49ec680 --- /dev/null +++ b/src/app/shared/utils/ipV4.validator.ts @@ -0,0 +1,27 @@ +import {Directive} from '@angular/core'; +import {NG_VALIDATORS, Validator, UntypedFormControl} from '@angular/forms'; + +@Directive({ + // eslint-disable-next-line @angular-eslint/directive-selector + selector: '[ipV4format]', + providers: [ + { provide: NG_VALIDATORS, useExisting: IpV4Validator, multi: true }, + ] +}) +/** + * Validator to validate if an Ip is in the right format + */ +export class IpV4Validator implements Validator { + validate(formControl: UntypedFormControl): {[key: string]: boolean} | null { + const ipv4Regex = /^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + const ipValue = formControl.value; + const ipParts = ipValue?.split('.'); + const numberOfParts = ipParts.length; + + if (ipValue && (numberOfParts !== 4 || !ipv4Regex.test(ipValue))) { + return {isValidIp: false}; + } + + return null; + } +} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 8e7ce147f3..87273a199a 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -931,11 +931,13 @@ "ldn-new-service.form.label.name": "Name", "ldn-new-service.form.label.description": "Description", "ldn-new-service.form.label.url": "Service URL", + "ldn-new-service.form.label.ip-range": "Service IP range", "ldn-new-service.form.label.score": "Level of trust", "ldn-new-service.form.label.ldnUrl": "LDN Inbox URL", "ldn-new-service.form.placeholder.name": "Please provide service name", "ldn-new-service.form.placeholder.description": "Please provide a description regarding your service", "ldn-new-service.form.placeholder.url": "Please input the URL for users to check out more information about the service", + "ldn-new-service.form.placeholder.ipRange": "Please input the IPv4 range of the service", "ldn-new-service.form.placeholder.ldnUrl": "Please specify the URL of the LDN Inbox", "ldn-new-service.form.placeholder.score": "Please enter a value between 0 and 1. Use the “.” as decimal separator", "ldn-service.form.label.placeholder.default-select": "Select a pattern", @@ -997,6 +999,7 @@ "ldn-new-service.form.label.automatic": "Automatic", "ldn-new-service.form.error.name": "Name is required", "ldn-new-service.form.error.url": "URL is required", + "ldn-new-service.form.error.ipRange": "Please enter a valid IP range", "ldn-new-service.form.error.ldnurl": "LDN URL is required", "ldn-new-service.form.error.patterns": "At least a pattern is required", "ldn-new-service.form.error.score": "Please enter a valid score (between 0 and 1). Use the “.” as decimal separator", From 1002a4d8a0c12e4aecc380d15ef0e20892663b81 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Thu, 11 Jan 2024 18:06:27 +0100 Subject: [PATCH 2/7] add to shared module --- src/app/shared/shared.module.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 7f59016f8c..fce242914a 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -285,6 +285,7 @@ import { NgxPaginationModule } from 'ngx-pagination'; import { SplitPipe } from './utils/split.pipe'; import { ThemedUserMenuComponent } from './auth-nav-menu/user-menu/themed-user-menu.component'; import { ThemedLangSwitchComponent } from './lang-switch/themed-lang-switch.component'; +import { IpV4Validator } from "./utils/ipV4.validator"; const MODULES = [ CommonModule, @@ -495,6 +496,7 @@ const DIRECTIVES = [ MetadataFieldValidator, HoverClassDirective, ContextHelpDirective, + IpV4Validator ]; @NgModule({ From d1e5287d606b1c8d2861aaa52584c79cb819ff10 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Fri, 12 Jan 2024 09:37:57 +0100 Subject: [PATCH 3/7] refactor add tests for validator --- src/app/shared/utils/ipV4.validator.spec.ts | 37 +++++++++++++++++++++ src/app/shared/utils/ipV4.validator.ts | 9 +++-- 2 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 src/app/shared/utils/ipV4.validator.spec.ts diff --git a/src/app/shared/utils/ipV4.validator.spec.ts b/src/app/shared/utils/ipV4.validator.spec.ts new file mode 100644 index 0000000000..a8678fec56 --- /dev/null +++ b/src/app/shared/utils/ipV4.validator.spec.ts @@ -0,0 +1,37 @@ +import { MarkdownPipe } from './markdown.pipe'; +import { IpV4Validator } from "./ipV4.validator"; +import { TestBed } from "@angular/core/testing"; +import { UntypedFormControl, UntypedFormGroup } from "@angular/forms"; + + +describe('IpV4 validator', () => { + + let ipV4Validator: IpV4Validator; + const validIp = '192.168.0.1'; + const formGroup = new UntypedFormGroup({ + ip: new UntypedFormControl(''), + }); + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + IpV4Validator, + ], + }).compileComponents(); + + ipV4Validator = TestBed.inject(IpV4Validator); + }); + + it('should return null for valid ipV4', () => { + formGroup.controls.ip.setValue(validIp); + expect(ipV4Validator.validate(formGroup.controls.ip as UntypedFormControl)).toBeNull() + }); + + it('should return {isValidIp: false} for invalid Ip', () => { + formGroup.controls.ip.setValue('100.260.45.1'); + expect(ipV4Validator.validate(formGroup.controls.ip as UntypedFormControl)).toEqual({isValidIp: false}) + formGroup.controls.ip.setValue('100'); + expect(ipV4Validator.validate(formGroup.controls.ip as UntypedFormControl)).toEqual({isValidIp: false}) + formGroup.controls.ip.setValue('testString'); + expect(ipV4Validator.validate(formGroup.controls.ip as UntypedFormControl)).toEqual({isValidIp: false}) + }); +}); diff --git a/src/app/shared/utils/ipV4.validator.ts b/src/app/shared/utils/ipV4.validator.ts index 66b49ec680..a5caa144ea 100644 --- a/src/app/shared/utils/ipV4.validator.ts +++ b/src/app/shared/utils/ipV4.validator.ts @@ -13,15 +13,14 @@ import {NG_VALIDATORS, Validator, UntypedFormControl} from '@angular/forms'; */ export class IpV4Validator implements Validator { validate(formControl: UntypedFormControl): {[key: string]: boolean} | null { - const ipv4Regex = /^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/; const ipValue = formControl.value; const ipParts = ipValue?.split('.'); - const numberOfParts = ipParts.length; - if (ipValue && (numberOfParts !== 4 || !ipv4Regex.test(ipValue))) { - return {isValidIp: false}; + if (ipv4Regex.test(ipValue) && ipParts.every(part => parseInt(part) <= 255)) { + return null; } - return null; + return {isValidIp: false} } } From 7a5b52f691fa1fbf331d94f66cf40ea85fb10025 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Fri, 12 Jan 2024 11:15:42 +0100 Subject: [PATCH 4/7] fix lint, change translation, clean up component --- .../ldn-service-form.component.html | 33 ++++---- .../ldn-service-form.component.ts | 81 +++++++------------ .../ldn-services-model/ldn-services.model.ts | 6 ++ src/app/shared/shared.module.ts | 2 +- src/app/shared/utils/ipV4.validator.spec.ts | 14 ++-- src/app/shared/utils/ipV4.validator.ts | 4 +- src/assets/i18n/en.json5 | 4 +- 7 files changed, 68 insertions(+), 76 deletions(-) diff --git a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html index 45fb1d68a5..61d387ef3b 100644 --- a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html +++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html @@ -48,27 +48,30 @@
- +
- +
- -
-
+
{{ 'ldn-new-service.form.error.ipRange' | translate }}
+
+ {{ 'ldn-new-service.form.hint.ipRange' | translate }} +
@@ -107,7 +110,7 @@
- +
@@ -168,7 +171,7 @@
+ *ngIf="formModel.get('notifyServiceInboundPatterns')['controls'][i].value.pattern">
diff --git a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts index ba654538a3..41fdd71160 100644 --- a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts +++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.ts @@ -1,11 +1,8 @@ import { ChangeDetectorRef, Component, - EventEmitter, - Input, OnDestroy, OnInit, - Output, TemplateRef, ViewChild } from '@angular/core'; @@ -49,37 +46,26 @@ import { IpV4Validator } from '../../../shared/utils/ipV4.validator'; }) export class LdnServiceFormComponent implements OnInit, OnDestroy { formModel: FormGroup; + @ViewChild('confirmModal', {static: true}) confirmModal: TemplateRef; @ViewChild('resetFormModal', {static: true}) resetFormModal: TemplateRef; public inboundPatterns: string[] = notifyPatterns; public isNewService: boolean; public areControlsInitialized: boolean; - itemfiltersRD$: Observable>>; - config: FindListOptions = Object.assign(new FindListOptions(), { + public itemfiltersRD$: Observable>>; + public config: FindListOptions = Object.assign(new FindListOptions(), { elementsPerPage: 20 }); + public markedForDeletionInboundPattern: number[] = []; + public selectedInboundPatterns: string[]; + public selectedInboundItemfilters: string[]; - @Input() public name: string; - @Input() public description: string; - @Input() public url: string; - @Input() public ldnUrl: string; - @Input() public ipRangeMin: string; - @Input() public ipRangeMax: string; - @Input() public score: number; - @Input() public inboundPattern: string; - @Input() public constraint: string; - @Input() public automatic: boolean; - @Input() public headerKey: string; - @Output() submitForm: EventEmitter = new EventEmitter(); - @Output() cancelForm: EventEmitter = new EventEmitter(); - markedForDeletionInboundPattern: number[] = []; - selectedInboundPatterns: string[]; - selectedInboundItemfilters: string[]; protected serviceId: string; + private deletedInboundPatterns: number[] = []; private modalRef: any; - private service: LdnService; + private ldnService: LdnService; private selectPatternDefaultLabeli18Key = 'ldn-service.form.label.placeholder.default-select'; private routeSubscription: Subscription; @@ -102,8 +88,8 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy { description: [''], url: ['', Validators.required], ldnUrl: ['', Validators.required], - ipRangeMin: ['', [Validators.required, new IpV4Validator()]], - ipRangeMax: ['', [new IpV4Validator()]], + lowerIp: ['', [Validators.required, new IpV4Validator()]], + upperIp: ['', [Validators.required, new IpV4Validator()]], score: ['', [Validators.required, Validators.pattern('^0*(\.[0-9]+)?$|^1(\.0+)?$')]], inboundPattern: [''], constraintPattern: [''], enabled: [''], @@ -144,15 +130,9 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy { */ createService() { this.formModel.markAllAsTouched(); - - const name = this.formModel.get('name').value; - const url = this.formModel.get('url').value; - const score = this.formModel.get('score').value; - const ldnUrl = this.formModel.get('ldnUrl').value; - const hasInboundPattern = this.checkPatterns(this.formModel.get('notifyServiceInboundPatterns') as FormArray); - if (!name || !url || !ldnUrl || (!score && score !== 0) || this.formModel.get('score').invalid) { + if (this.formModel.invalid) { this.closeModal(); return; } @@ -182,9 +162,8 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy { if (rd.hasSucceeded) { this.notificationService.success(this.translateService.get('ldn-service-notification.created.success.title'), this.translateService.get('ldn-service-notification.created.success.body')); - - this.sendBack(); this.closeModal(); + this.sendBack(); } else { this.notificationService.error(this.translateService.get('ldn-service-notification.created.failure.title'), this.translateService.get('ldn-service-notification.created.failure.body')); @@ -219,18 +198,21 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy { ).subscribe( (data: RemoteData) => { if (data.hasSucceeded) { - this.service = data.payload; + this.ldnService = data.payload; this.formModel.patchValue({ - id: this.service.id, - name: this.service.name, - description: this.service.description, - url: this.service.url, - score: this.service.score, ldnUrl: this.service.ldnUrl, - type: this.service.type, - enabled: this.service.enabled + id: this.ldnService.id, + name: this.ldnService.name, + description: this.ldnService.description, + url: this.ldnService.url, + score: this.ldnService.score, + ldnUrl: this.ldnService.ldnUrl, + type: this.ldnService.type, + enabled: this.ldnService.enabled, + lowerIp: this.ldnService.lowerIp, + upperIp: this.ldnService.upperIp, }); - this.filterPatternObjectsAndPickLabel('notifyServiceInboundPatterns'); + this.filterPatternObjectsAndAssignLabel('notifyServiceInboundPatterns'); } }, ); @@ -240,11 +222,11 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy { * Filters pattern objects, initializes form groups, assigns labels, and adds them to the specified form array so the correct string is shown in the dropdown.. * @param formArrayName - The name of the form array to be populated */ - filterPatternObjectsAndPickLabel(formArrayName: string) { + filterPatternObjectsAndAssignLabel(formArrayName: string) { const PatternsArray = this.formModel.get(formArrayName) as FormArray; PatternsArray.clear(); - let servicesToUse; - servicesToUse = this.service.notifyServiceInboundPatterns; + + let servicesToUse = this.ldnService.notifyServiceInboundPatterns; servicesToUse.forEach((patternObj: NotifyServicePattern) => { let patternFormGroup; @@ -258,8 +240,6 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy { PatternsArray.push(patternFormGroup); this.cdRef.detectChanges(); }); - - } /** @@ -274,6 +254,8 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy { this.createReplaceOperation(patchOperations, 'ldnUrl', '/ldnurl'); this.createReplaceOperation(patchOperations, 'url', '/url'); this.createReplaceOperation(patchOperations, 'score', '/score'); + this.createReplaceOperation(patchOperations, 'lowerIp', '/lowerIp'); + this.createReplaceOperation(patchOperations, 'upperIp', '/upperIp'); this.handlePatterns(patchOperations, 'notifyServiceInboundPatterns'); this.deletedInboundPatterns.forEach(index => { @@ -347,11 +329,10 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy { value: newStatus, }; - this.ldnServicesService.patch(this.service, [patchOperation]).pipe( + this.ldnServicesService.patch(this.ldnService, [patchOperation]).pipe( getFirstCompletedRemoteData() ).subscribe( () => { - this.formModel.get('enabled').setValue(newStatus); this.cdRef.detectChanges(); } @@ -407,7 +388,7 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy { return; } - this.ldnServicesService.patch(this.service, patchOperations).pipe( + this.ldnServicesService.patch(this.ldnService, patchOperations).pipe( getFirstCompletedRemoteData() ).subscribe( (rd: RemoteData) => { diff --git a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-services.model.ts b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-services.model.ts index 315d3ff89e..0fcde59d01 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-model/ldn-services.model.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-model/ldn-services.model.ts @@ -41,6 +41,12 @@ export class LdnService extends CacheableObject { @autoserialize ldnUrl: string; + @autoserialize + lowerIp: string; + + @autoserialize + upperIp: string; + @autoserialize notifyServiceInboundPatterns?: NotifyServicePattern[]; diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index fce242914a..f348d0c981 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -285,7 +285,7 @@ import { NgxPaginationModule } from 'ngx-pagination'; import { SplitPipe } from './utils/split.pipe'; import { ThemedUserMenuComponent } from './auth-nav-menu/user-menu/themed-user-menu.component'; import { ThemedLangSwitchComponent } from './lang-switch/themed-lang-switch.component'; -import { IpV4Validator } from "./utils/ipV4.validator"; +import { IpV4Validator } from './utils/ipV4.validator'; const MODULES = [ CommonModule, diff --git a/src/app/shared/utils/ipV4.validator.spec.ts b/src/app/shared/utils/ipV4.validator.spec.ts index fa3e5fd06f..93f5ee86e9 100644 --- a/src/app/shared/utils/ipV4.validator.spec.ts +++ b/src/app/shared/utils/ipV4.validator.spec.ts @@ -1,6 +1,6 @@ -import { IpV4Validator } from "./ipV4.validator"; -import { TestBed } from "@angular/core/testing"; -import { UntypedFormControl, UntypedFormGroup } from "@angular/forms"; +import { IpV4Validator } from './ipV4.validator'; +import { TestBed } from '@angular/core/testing'; +import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; describe('IpV4 validator', () => { @@ -22,15 +22,15 @@ describe('IpV4 validator', () => { it('should return null for valid ipV4', () => { formGroup.controls.ip.setValue(validIp); - expect(ipV4Validator.validate(formGroup.controls.ip as UntypedFormControl)).toBeNull() + expect(ipV4Validator.validate(formGroup.controls.ip as UntypedFormControl)).toBeNull(); }); it('should return {isValidIp: false} for invalid Ip', () => { formGroup.controls.ip.setValue('100.260.45.1'); - expect(ipV4Validator.validate(formGroup.controls.ip as UntypedFormControl)).toEqual({isValidIp: false}) + expect(ipV4Validator.validate(formGroup.controls.ip as UntypedFormControl)).toEqual({isValidIp: false}); formGroup.controls.ip.setValue('100'); - expect(ipV4Validator.validate(formGroup.controls.ip as UntypedFormControl)).toEqual({isValidIp: false}) + expect(ipV4Validator.validate(formGroup.controls.ip as UntypedFormControl)).toEqual({isValidIp: false}); formGroup.controls.ip.setValue('testString'); - expect(ipV4Validator.validate(formGroup.controls.ip as UntypedFormControl)).toEqual({isValidIp: false}) + expect(ipV4Validator.validate(formGroup.controls.ip as UntypedFormControl)).toEqual({isValidIp: false}); }); }); diff --git a/src/app/shared/utils/ipV4.validator.ts b/src/app/shared/utils/ipV4.validator.ts index a5caa144ea..170dbeb547 100644 --- a/src/app/shared/utils/ipV4.validator.ts +++ b/src/app/shared/utils/ipV4.validator.ts @@ -17,10 +17,10 @@ export class IpV4Validator implements Validator { const ipValue = formControl.value; const ipParts = ipValue?.split('.'); - if (ipv4Regex.test(ipValue) && ipParts.every(part => parseInt(part) <= 255)) { + if (ipv4Regex.test(ipValue) && ipParts.every(part => parseInt(part, 10) <= 255)) { return null; } - return {isValidIp: false} + return {isValidIp: false}; } } diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 87273a199a..785dbe11b9 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -937,7 +937,8 @@ "ldn-new-service.form.placeholder.name": "Please provide service name", "ldn-new-service.form.placeholder.description": "Please provide a description regarding your service", "ldn-new-service.form.placeholder.url": "Please input the URL for users to check out more information about the service", - "ldn-new-service.form.placeholder.ipRange": "Please input the IPv4 range of the service", + "ldn-new-service.form.placeholder.lowerIp": "IPv4 range lower bound", + "ldn-new-service.form.placeholder.upperIp": "IPv4 range upper bound", "ldn-new-service.form.placeholder.ldnUrl": "Please specify the URL of the LDN Inbox", "ldn-new-service.form.placeholder.score": "Please enter a value between 0 and 1. Use the “.” as decimal separator", "ldn-service.form.label.placeholder.default-select": "Select a pattern", @@ -1000,6 +1001,7 @@ "ldn-new-service.form.error.name": "Name is required", "ldn-new-service.form.error.url": "URL is required", "ldn-new-service.form.error.ipRange": "Please enter a valid IP range", + "ldn-new-service.form.hint.ipRange": "Please enter a valid IpV4 in both range bounds (note: for single IP, please enter the same value in both fields)", "ldn-new-service.form.error.ldnurl": "LDN URL is required", "ldn-new-service.form.error.patterns": "At least a pattern is required", "ldn-new-service.form.error.score": "Please enter a valid score (between 0 and 1). Use the “.” as decimal separator", From 96a6b80d2ccd69cf48bbb46223c8ff87a6a11fde Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Fri, 12 Jan 2024 11:35:15 +0100 Subject: [PATCH 5/7] add test fix mocks --- .../ldnServicesRD$-mock.ts | 6 ++ .../ldn-itemfilter-data.service.spec.ts | 85 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilter-data.service.spec.ts diff --git a/src/app/admin/admin-ldn-services/ldn-service-serviceMock/ldnServicesRD$-mock.ts b/src/app/admin/admin-ldn-services/ldn-service-serviceMock/ldnServicesRD$-mock.ts index 3b3f3723a7..d8534dde03 100644 --- a/src/app/admin/admin-ldn-services/ldn-service-serviceMock/ldnServicesRD$-mock.ts +++ b/src/app/admin/admin-ldn-services/ldn-service-serviceMock/ldnServicesRD$-mock.ts @@ -10,6 +10,8 @@ export const mockLdnService: LdnService = { enabled: false, score: 0, id: 1, + lowerIp: '192.0.2.146', + upperIp: '192.0.2.255', name: 'Service Name', description: 'Service Description', url: 'Service URL', @@ -45,6 +47,8 @@ export const mockLdnServices: LdnService[] = [{ enabled: false, score: 0, id: 1, + lowerIp: '192.0.2.146', + upperIp: '192.0.2.255', name: 'Service Name', description: 'Service Description', url: 'Service URL', @@ -75,6 +79,8 @@ export const mockLdnServices: LdnService[] = [{ enabled: false, score: 0, id: 2, + lowerIp: '192.0.2.146', + upperIp: '192.0.2.255', name: 'Service Name', description: 'Service Description', url: 'Service URL', diff --git a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilter-data.service.spec.ts b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilter-data.service.spec.ts new file mode 100644 index 0000000000..7c4d541201 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilter-data.service.spec.ts @@ -0,0 +1,85 @@ +import { TestScheduler } from "rxjs/testing"; +import { LdnItemfiltersService } from "./ldn-itemfilters-data.service"; +import { RequestService } from "../../../core/data/request.service"; +import { RemoteDataBuildService } from "../../../core/cache/builders/remote-data-build.service"; +import { ObjectCacheService } from "../../../core/cache/object-cache.service"; +import { HALEndpointService } from "../../../core/shared/hal-endpoint.service"; +import { NotificationsService } from "../../../shared/notifications/notifications.service"; +import { RequestEntry } from "../../../core/data/request-entry.model"; +import { RemoteData } from "../../../core/data/remote-data"; +import { RequestEntryState } from "../../../core/data/request-entry-state.model"; +import { cold, getTestScheduler } from "jasmine-marbles"; +import { RestResponse } from "../../../core/cache/response.models"; +import { of } from "rxjs"; +import { createSuccessfulRemoteDataObject$ } from "../../../shared/remote-data.utils"; +import { FindAllData } from "../../../core/data/base/find-all-data"; +import { testFindAllDataImplementation } from "../../../core/data/base/find-all-data.spec"; + +describe('LdnItemfiltersService test', () => { + let scheduler: TestScheduler; + let service: LdnItemfiltersService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: HALEndpointService; + let notificationsService: NotificationsService; + let responseCacheEntry: RequestEntry; + + const endpointURL = `https://rest.api/rest/api/ldn/itemfilters`; + const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; + + const remoteDataMocks = { + Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200), + }; + + function initTestService() { + return new LdnItemfiltersService( + requestService, + rdbService, + objectCache, + halService, + notificationsService, + ); + } + + beforeEach(() => { + scheduler = getTestScheduler(); + + objectCache = {} as ObjectCacheService; + notificationsService = {} as NotificationsService; + responseCacheEntry = new RequestEntry(); + responseCacheEntry.request = { href: 'https://rest.api/' } as any; + responseCacheEntry.response = new RestResponse(true, 200, 'Success'); + + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + send: true, + removeByHrefSubstring: {}, + getByHref: of(responseCacheEntry), + getByUUID: of(responseCacheEntry), + }); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: of(endpointURL) + }); + + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: createSuccessfulRemoteDataObject$({}, 500), + buildList: cold('a', { a: remoteDataMocks.Success }) + }); + + + service = initTestService(); + }); + + describe('composition', () => { + const initFindAllService = () => new LdnItemfiltersService(null, null, null, null, null) as unknown as FindAllData; + testFindAllDataImplementation(initFindAllService); + }); + + describe('get endpoint', () => { + service.getEndpoint() + expect(halService.getEndpoint).toHaveBeenCalledWith('linkPath') + }); + +}); From 11a80771a89d36518705def9392098ebc74277bd Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Fri, 12 Jan 2024 15:08:08 +0100 Subject: [PATCH 6/7] add ldn services test --- .../ldn-itemfilter-data.service.spec.ts | 40 +++--- .../ldn-services-data.service.spec.ts | 116 ++++++++++++++++++ .../ldn-services-data.service.ts | 37 +++--- 3 files changed, 161 insertions(+), 32 deletions(-) create mode 100644 src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.spec.ts diff --git a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilter-data.service.spec.ts b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilter-data.service.spec.ts index 7c4d541201..b5b0881727 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilter-data.service.spec.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilter-data.service.spec.ts @@ -1,19 +1,19 @@ -import { TestScheduler } from "rxjs/testing"; -import { LdnItemfiltersService } from "./ldn-itemfilters-data.service"; -import { RequestService } from "../../../core/data/request.service"; -import { RemoteDataBuildService } from "../../../core/cache/builders/remote-data-build.service"; -import { ObjectCacheService } from "../../../core/cache/object-cache.service"; -import { HALEndpointService } from "../../../core/shared/hal-endpoint.service"; -import { NotificationsService } from "../../../shared/notifications/notifications.service"; -import { RequestEntry } from "../../../core/data/request-entry.model"; -import { RemoteData } from "../../../core/data/remote-data"; -import { RequestEntryState } from "../../../core/data/request-entry-state.model"; -import { cold, getTestScheduler } from "jasmine-marbles"; -import { RestResponse } from "../../../core/cache/response.models"; -import { of } from "rxjs"; -import { createSuccessfulRemoteDataObject$ } from "../../../shared/remote-data.utils"; -import { FindAllData } from "../../../core/data/base/find-all-data"; -import { testFindAllDataImplementation } from "../../../core/data/base/find-all-data.spec"; +import { TestScheduler } from 'rxjs/testing'; +import { LdnItemfiltersService } from './ldn-itemfilters-data.service'; +import { RequestService } from '../../../core/data/request.service'; +import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { RequestEntry } from '../../../core/data/request-entry.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { RequestEntryState } from '../../../core/data/request-entry-state.model'; +import { cold, getTestScheduler } from 'jasmine-marbles'; +import { RestResponse } from '../../../core/cache/response.models'; +import { of } from 'rxjs'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { FindAllData } from '../../../core/data/base/find-all-data'; +import { testFindAllDataImplementation } from '../../../core/data/base/find-all-data.spec'; describe('LdnItemfiltersService test', () => { let scheduler: TestScheduler; @@ -78,8 +78,12 @@ describe('LdnItemfiltersService test', () => { }); describe('get endpoint', () => { - service.getEndpoint() - expect(halService.getEndpoint).toHaveBeenCalledWith('linkPath') + it('should retrieve correct endpoint', (done) => { + service.getEndpoint().subscribe(() => { + expect(halService.getEndpoint).toHaveBeenCalledWith('itemfilters'); + done(); + }); + }); }); }); diff --git a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.spec.ts b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.spec.ts new file mode 100644 index 0000000000..806ac7775c --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.spec.ts @@ -0,0 +1,116 @@ +import { TestScheduler } from 'rxjs/testing'; +import { RequestService } from '../../../core/data/request.service'; +import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { RequestEntry } from '../../../core/data/request-entry.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { RequestEntryState } from '../../../core/data/request-entry-state.model'; +import { cold, getTestScheduler } from 'jasmine-marbles'; +import { RestResponse } from '../../../core/cache/response.models'; +import { of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { FindAllData } from '../../../core/data/base/find-all-data'; +import { testFindAllDataImplementation } from '../../../core/data/base/find-all-data.spec'; +import { LdnServicesService } from './ldn-services-data.service'; +import { testDeleteDataImplementation } from '../../../core/data/base/delete-data.spec'; +import { DeleteData } from '../../../core/data/base/delete-data'; +import { testSearchDataImplementation } from '../../../core/data/base/search-data.spec'; +import { SearchData } from '../../../core/data/base/search-data'; +import { testPatchDataImplementation } from '../../../core/data/base/patch-data.spec'; +import { PatchData } from '../../../core/data/base/patch-data'; +import { CreateData } from '../../../core/data/base/create-data'; +import { testCreateDataImplementation } from '../../../core/data/base/create-data.spec'; +import { FindListOptions } from '../../../core/data/find-list-options.model'; +import { RequestParam } from '../../../core/cache/models/request-param.model'; +import { mockLdnService } from '../ldn-service-serviceMock/ldnServicesRD$-mock'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; + + +describe('LdnServicesService test', () => { + let scheduler: TestScheduler; + let service: LdnServicesService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: HALEndpointService; + let notificationsService: NotificationsService; + let responseCacheEntry: RequestEntry; + + const endpointURL = `https://rest.api/rest/api/ldn/ldnservices`; + const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; + + const remoteDataMocks = { + Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200), + }; + + function initTestService() { + return new LdnServicesService( + requestService, + rdbService, + objectCache, + halService, + notificationsService, + ); + } + + beforeEach(() => { + scheduler = getTestScheduler(); + + objectCache = {} as ObjectCacheService; + notificationsService = {} as NotificationsService; + responseCacheEntry = new RequestEntry(); + responseCacheEntry.request = { href: 'https://rest.api/' } as any; + responseCacheEntry.response = new RestResponse(true, 200, 'Success'); + + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + send: true, + removeByHrefSubstring: {}, + getByHref: observableOf(responseCacheEntry), + getByUUID: observableOf(responseCacheEntry), + }); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: observableOf(endpointURL) + }); + + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: createSuccessfulRemoteDataObject$({}, 500), + buildList: cold('a', { a: remoteDataMocks.Success }) + }); + + + service = initTestService(); + }); + + describe('composition', () => { + const initFindAllService = () => new LdnServicesService(null, null, null, null, null) as unknown as FindAllData; + const initDeleteService = () => new LdnServicesService(null, null, null, null, null) as unknown as DeleteData; + const initSearchService = () => new LdnServicesService(null, null, null, null, null) as unknown as SearchData; + const initPatchService = () => new LdnServicesService(null, null, null, null, null) as unknown as PatchData; + const initCreateService = () => new LdnServicesService(null, null, null, null, null) as unknown as CreateData; + + testFindAllDataImplementation(initFindAllService); + testDeleteDataImplementation(initDeleteService); + testSearchDataImplementation(initSearchService); + testPatchDataImplementation(initPatchService); + testCreateDataImplementation(initCreateService); + }); + + describe('custom methods', () => { + it('should find service by inbound pattern', (done) => { + const params = [new RequestParam('pattern', 'testPattern')]; + const findListOptions = Object.assign(new FindListOptions(), {}, {searchParams: params}); + spyOn(service, 'searchBy').and.returnValue(observableOf(null)); + spyOn((service as any).searchData, 'searchBy').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([mockLdnService]))); + + service.findByInboundPattern('testPattern').subscribe((result) => { + expect(service.searchBy).toHaveBeenCalledWith('byInboundPattern', findListOptions, undefined, undefined ); + done(); + }); + }); + }); + +}); diff --git a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts index e7c2f47159..d1541e6bd8 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.ts @@ -29,8 +29,6 @@ import {Operation} from 'fast-json-patch'; import {RestRequestMethod} from '../../../core/data/rest-request-method'; import {CreateData, CreateDataImpl} from '../../../core/data/base/create-data'; import {LdnServiceConstrain} from '../ldn-services-model/ldn-service.constrain.model'; -import {getFirstCompletedRemoteData} from '../../../core/shared/operators'; -import {hasValue} from '../../../shared/empty.util'; import {SearchDataImpl} from '../../../core/data/base/search-data'; import {RequestParam} from '../../../core/cache/models/request-param.model'; @@ -77,10 +75,11 @@ export class LdnServicesService extends IdentifiableDataService impl * Creates an LDN service by sending a POST request to the REST API. * * @param {LdnService} object - The LDN service object to be created. + * @param params Array with additional params to combine with query string * @returns {Observable>} - Observable containing the result of the creation operation. */ - create(object: LdnService): Observable> { - return this.createData.create(object); + create(object: LdnService, ...params: RequestParam[]): Observable> { + return this.createData.create(object, ...params); } /** @@ -149,7 +148,7 @@ export class LdnServicesService extends IdentifiableDataService impl findByInboundPattern(pattern: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { const params = [new RequestParam('pattern', pattern)]; const findListOptions = Object.assign(new FindListOptions(), options, {searchParams: params}); - return this.searchData.searchBy(this.findByPatternEndpoint, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + return this.searchBy(this.findByPatternEndpoint, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** @@ -174,6 +173,25 @@ export class LdnServicesService extends IdentifiableDataService impl return this.deleteData.deleteByHref(href, copyVirtualMetadata); } + + /** + * Make a new FindListRequest with given search method + * + * @param searchMethod The search method for the object + * @param options The [[FindListOptions]] object + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved + * @return {Observable>} + * Return an observable that emits response from the server + */ + public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + public invoke(serviceName: string, serviceId: string, parameters: LdnServiceConstrain[], files: File[]): Observable> { const requestId = this.requestService.generateRequestId(); this.getBrowseEndpoint().pipe( @@ -188,15 +206,6 @@ export class LdnServicesService extends IdentifiableDataService impl return this.rdbService.buildFromRequestUUID(requestId); } - public ldnServiceWithNameExistsAndCanExecute(scriptName: string): Observable { - return this.findById(scriptName).pipe( - getFirstCompletedRemoteData(), - map((rd: RemoteData) => { - return hasValue(rd.payload); - }), - ); - } - private getInvocationFormData(constrain: LdnServiceConstrain[], files: File[]): FormData { const form: FormData = new FormData(); form.set('properties', JSON.stringify(constrain)); From 3a87044600149ab0142cd065454ebdf65861d8e7 Mon Sep 17 00:00:00 2001 From: FrancescoMolinaro Date: Fri, 12 Jan 2024 15:08:35 +0100 Subject: [PATCH 7/7] remove unused param --- .../ldn-services-data/ldn-services-data.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.spec.ts b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.spec.ts index 806ac7775c..9d17fc244c 100644 --- a/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.spec.ts +++ b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-services-data.service.spec.ts @@ -106,7 +106,7 @@ describe('LdnServicesService test', () => { spyOn(service, 'searchBy').and.returnValue(observableOf(null)); spyOn((service as any).searchData, 'searchBy').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([mockLdnService]))); - service.findByInboundPattern('testPattern').subscribe((result) => { + service.findByInboundPattern('testPattern').subscribe(() => { expect(service.searchBy).toHaveBeenCalledWith('byInboundPattern', findListOptions, undefined, undefined ); done(); });