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 6b4f8085953..61d387ef3b2 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,32 @@

{{ isNewService ? ('ldn-create-service.title' | translat + +
+ +
+ + +
+ +
+ {{ 'ldn-new-service.form.error.ipRange' | translate }} +
+
+ {{ 'ldn-new-service.form.hint.ipRange' | translate }} +
+
+
@@ -84,7 +110,7 @@

{{ isNewService ? ('ldn-create-service.title' | translat
- +
@@ -145,7 +171,7 @@

{{ isNewService ? ('ldn-create-service.title' | translat
+ *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 b7e0f8e7eb1..41fdd71160c 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'; @@ -29,6 +26,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'; /** @@ -48,35 +46,26 @@ import {NotifyServicePattern} from '../ldn-services-model/ldn-service-patterns.m }) 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 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; @@ -99,6 +88,8 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy { description: [''], url: ['', Validators.required], ldnUrl: ['', Validators.required], + lowerIp: ['', [Validators.required, new IpV4Validator()]], + upperIp: ['', [Validators.required, new IpV4Validator()]], score: ['', [Validators.required, Validators.pattern('^0*(\.[0-9]+)?$|^1(\.0+)?$')]], inboundPattern: [''], constraintPattern: [''], enabled: [''], @@ -139,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; } @@ -177,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')); @@ -214,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'); } }, ); @@ -235,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; @@ -253,8 +240,6 @@ export class LdnServiceFormComponent implements OnInit, OnDestroy { PatternsArray.push(patternFormGroup); this.cdRef.detectChanges(); }); - - } /** @@ -269,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 => { @@ -342,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(); } @@ -402,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-service-serviceMock/ldnServicesRD$-mock.ts b/src/app/admin/admin-ldn-services/ldn-service-serviceMock/ldnServicesRD$-mock.ts index 3b3f3723a72..d8534dde037 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 00000000000..b5b08817271 --- /dev/null +++ b/src/app/admin/admin-ldn-services/ldn-services-data/ldn-itemfilter-data.service.spec.ts @@ -0,0 +1,89 @@ +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', () => { + 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 00000000000..9d17fc244c4 --- /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(() => { + 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 e7c2f471591..d1541e6bd81 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)); 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 d297e1e0af3..8beb7fda687 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 @@ -46,6 +46,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 7f59016f8cf..f348d0c9811 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({ 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 00000000000..93f5ee86e9a --- /dev/null +++ b/src/app/shared/utils/ipV4.validator.spec.ts @@ -0,0 +1,36 @@ +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 new file mode 100644 index 00000000000..170dbeb5470 --- /dev/null +++ b/src/app/shared/utils/ipV4.validator.ts @@ -0,0 +1,26 @@ +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 = /^(\d{1,3}\.){3}\d{1,3}$/; + const ipValue = formControl.value; + const ipParts = ipValue?.split('.'); + + if (ipv4Regex.test(ipValue) && ipParts.every(part => parseInt(part, 10) <= 255)) { + return null; + } + + return {isValidIp: false}; + } +} diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 2a014622fa0..9afc51c3855 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -935,11 +935,14 @@ "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.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", @@ -1001,6 +1004,8 @@ "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.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",