]> git.proxmox.com Git - ceph.git/blobdiff - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts
update ceph source to reef 18.2.1
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / block / rbd-form / rbd-form.component.spec.ts
index e9727240292bf6dd64016838bb194334d80e9c2d..33b512e4d9ee9231eb367aeca9a39da5fde7c652 100644 (file)
@@ -1,28 +1,52 @@
 import { HttpClientTestingModule } from '@angular/common/http/testing';
-import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
 import { ReactiveFormsModule } from '@angular/forms';
+import { By } from '@angular/platform-browser';
 import { ActivatedRoute, Router } from '@angular/router';
 import { RouterTestingModule } from '@angular/router/testing';
-import { TooltipModule } from 'ngx-bootstrap/tooltip';
 
 import { ToastrModule } from 'ngx-toastr';
-
-import { By } from '@angular/platform-browser';
-import { of } from 'rxjs';
-import { ActivatedRouteStub } from '../../../../testing/activated-route-stub';
-import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper';
-import { RbdService } from '../../../shared/api/rbd.service';
-import { SharedModule } from '../../../shared/shared.module';
+import { NEVER, of } from 'rxjs';
+import { delay } from 'rxjs/operators';
+
+import { Pool } from '~/app/ceph/pool/pool';
+import { PoolService } from '~/app/shared/api/pool.service';
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { ImageSpec } from '~/app/shared/models/image-spec';
+import { SharedModule } from '~/app/shared/shared.module';
+import { ActivatedRouteStub } from '~/testing/activated-route-stub';
+import { configureTestBed } from '~/testing/unit-test-helper';
 import { RbdConfigurationFormComponent } from '../rbd-configuration-form/rbd-configuration-form.component';
+import { RbdImageFeature } from './rbd-feature.interface';
 import { RbdFormMode } from './rbd-form-mode.enum';
+import { RbdFormResponseModel } from './rbd-form-response.model';
 import { RbdFormComponent } from './rbd-form.component';
 
 describe('RbdFormComponent', () => {
+  const urlPrefix = {
+    create: '/block/rbd/create',
+    edit: '/block/rbd/edit',
+    clone: '/block/rbd/clone',
+    copy: '/block/rbd/copy'
+  };
   let component: RbdFormComponent;
   let fixture: ComponentFixture<RbdFormComponent>;
   let activatedRoute: ActivatedRouteStub;
-
-  const queryNativeElement = (cssSelector) =>
+  const mock: { rbd: RbdFormResponseModel; pools: Pool[]; defaultFeatures: string[] } = {
+    rbd: {} as RbdFormResponseModel,
+    pools: [],
+    defaultFeatures: []
+  };
+
+  const setRouterUrl = (
+    action: 'create' | 'edit' | 'clone' | 'copy',
+    poolName?: string,
+    imageName?: string
+  ) => {
+    component['routerUrl'] = [urlPrefix[action], poolName, imageName].filter((x) => x).join('/');
+  };
+
+  const queryNativeElement = (cssSelector: string) =>
     fixture.debugElement.query(By.css(cssSelector)).nativeElement;
 
   configureTestBed({
@@ -31,8 +55,7 @@ describe('RbdFormComponent', () => {
       ReactiveFormsModule,
       RouterTestingModule,
       ToastrModule.forRoot(),
-      SharedModule,
-      TooltipModule
+      SharedModule
     ],
     declarations: [RbdFormComponent, RbdConfigurationFormComponent],
     providers: [
@@ -40,7 +63,6 @@ describe('RbdFormComponent', () => {
         provide: ActivatedRoute,
         useValue: new ActivatedRouteStub({ pool: 'foo', name: 'bar', snap: undefined })
       },
-      i18nProviders,
       RbdService
     ]
   });
@@ -48,34 +70,224 @@ describe('RbdFormComponent', () => {
   beforeEach(() => {
     fixture = TestBed.createComponent(RbdFormComponent);
     component = fixture.componentInstance;
-    activatedRoute = TestBed.get(ActivatedRoute);
+    activatedRoute = <ActivatedRouteStub>TestBed.inject(ActivatedRoute);
+
+    component.loadingReady();
   });
 
   it('should create', () => {
     expect(component).toBeTruthy();
   });
 
+  describe('create/edit/clone/copy image', () => {
+    let createAction: jasmine.Spy;
+    let editAction: jasmine.Spy;
+    let cloneAction: jasmine.Spy;
+    let copyAction: jasmine.Spy;
+    let rbdServiceGetSpy: jasmine.Spy;
+    let routerNavigate: jasmine.Spy;
+
+    const DELAY = 100;
+
+    const getPool = (
+      pool_name: string,
+      type: 'replicated' | 'erasure',
+      flags_names: string,
+      application_metadata: string[]
+    ): Pool =>
+      ({
+        pool_name,
+        flags_names,
+        application_metadata,
+        type
+      } as Pool);
+
+    beforeEach(() => {
+      createAction = spyOn(component, 'createAction').and.returnValue(of(null));
+      editAction = spyOn(component, 'editAction');
+      editAction.and.returnValue(of(null));
+      cloneAction = spyOn(component, 'cloneAction').and.returnValue(of(null));
+      copyAction = spyOn(component, 'copyAction').and.returnValue(of(null));
+      spyOn(component, 'setResponse').and.stub();
+      routerNavigate = spyOn(TestBed.inject(Router), 'navigate').and.stub();
+      mock.pools = [
+        getPool('one', 'replicated', '', []),
+        getPool('two', 'replicated', '', ['rbd']),
+        getPool('three', 'replicated', '', ['rbd']),
+        getPool('four', 'erasure', '', ['rbd']),
+        getPool('four', 'erasure', 'ec_overwrites', ['rbd'])
+      ];
+      spyOn(TestBed.inject(PoolService), 'list').and.callFake(() => of(mock.pools));
+      rbdServiceGetSpy = spyOn(TestBed.inject(RbdService), 'get');
+      mock.rbd = ({ pool_name: 'foo', pool_image: 'bar' } as any) as RbdFormResponseModel;
+      rbdServiceGetSpy.and.returnValue(of(mock.rbd));
+      component.mode = undefined;
+    });
+
+    it('should create image', () => {
+      component.ngOnInit();
+      component.submit();
+
+      expect(createAction).toHaveBeenCalledTimes(1);
+      expect(editAction).toHaveBeenCalledTimes(0);
+      expect(cloneAction).toHaveBeenCalledTimes(0);
+      expect(copyAction).toHaveBeenCalledTimes(0);
+      expect(routerNavigate).toHaveBeenCalledTimes(1);
+    });
+
+    it('should unsubscribe right after image data is received', () => {
+      setRouterUrl('edit', 'foo', 'bar');
+      rbdServiceGetSpy.and.returnValue(of(mock.rbd));
+      editAction.and.returnValue(NEVER);
+      expect(component['rbdImage'].observers.length).toEqual(0);
+      component.ngOnInit(); // Subscribes to image once during init
+      component.submit();
+      expect(createAction).toHaveBeenCalledTimes(0);
+      expect(editAction).toHaveBeenCalledTimes(1);
+      expect(cloneAction).toHaveBeenCalledTimes(0);
+      expect(copyAction).toHaveBeenCalledTimes(0);
+      expect(routerNavigate).toHaveBeenCalledTimes(0);
+    });
+
+    it('should not edit image if no image data is received', fakeAsync(() => {
+      setRouterUrl('edit', 'foo', 'bar');
+      rbdServiceGetSpy.and.returnValue(of(mock.rbd).pipe(delay(DELAY)));
+      component.ngOnInit();
+      component.submit();
+
+      expect(createAction).toHaveBeenCalledTimes(0);
+      expect(editAction).toHaveBeenCalledTimes(0);
+      expect(cloneAction).toHaveBeenCalledTimes(0);
+      expect(copyAction).toHaveBeenCalledTimes(0);
+      expect(routerNavigate).toHaveBeenCalledTimes(0);
+
+      tick(DELAY);
+    }));
+
+    describe('disable data pools', () => {
+      beforeEach(() => {
+        component.ngOnInit();
+      });
+
+      it('should be enabled with more than 1 pool', () => {
+        component['handleExternalData'](mock);
+        expect(component.allDataPools.length).toBe(3);
+        expect(component.rbdForm.get('useDataPool').disabled).toBe(false);
+
+        mock.pools.pop();
+        component['handleExternalData'](mock);
+        expect(component.allDataPools.length).toBe(2);
+        expect(component.rbdForm.get('useDataPool').disabled).toBe(false);
+      });
+
+      it('should be disabled with 1 pool', () => {
+        mock.pools = [mock.pools[0]];
+        component['handleExternalData'](mock);
+        expect(component.rbdForm.get('useDataPool').disabled).toBe(true);
+      });
+
+      // Reason for 2 tests - useDataPool is not re-enabled anywhere else
+      it('should be disabled without any pool', () => {
+        mock.pools = [];
+        component['handleExternalData'](mock);
+        expect(component.rbdForm.get('useDataPool').disabled).toBe(true);
+      });
+    });
+
+    it('should edit image after image data is received', () => {
+      setRouterUrl('edit', 'foo', 'bar');
+      component.ngOnInit();
+      component.submit();
+
+      expect(createAction).toHaveBeenCalledTimes(0);
+      expect(editAction).toHaveBeenCalledTimes(1);
+      expect(cloneAction).toHaveBeenCalledTimes(0);
+      expect(copyAction).toHaveBeenCalledTimes(0);
+      expect(routerNavigate).toHaveBeenCalledTimes(1);
+    });
+
+    it('should not clone image if no image data is received', fakeAsync(() => {
+      setRouterUrl('clone', 'foo', 'bar');
+      rbdServiceGetSpy.and.returnValue(of(mock.rbd).pipe(delay(DELAY)));
+      component.ngOnInit();
+      component.submit();
+
+      expect(createAction).toHaveBeenCalledTimes(0);
+      expect(editAction).toHaveBeenCalledTimes(0);
+      expect(cloneAction).toHaveBeenCalledTimes(0);
+      expect(copyAction).toHaveBeenCalledTimes(0);
+      expect(routerNavigate).toHaveBeenCalledTimes(0);
+
+      tick(DELAY);
+    }));
+
+    it('should clone image after image data is received', () => {
+      setRouterUrl('clone', 'foo', 'bar');
+      component.ngOnInit();
+      component.submit();
+
+      expect(createAction).toHaveBeenCalledTimes(0);
+      expect(editAction).toHaveBeenCalledTimes(0);
+      expect(cloneAction).toHaveBeenCalledTimes(1);
+      expect(copyAction).toHaveBeenCalledTimes(0);
+      expect(routerNavigate).toHaveBeenCalledTimes(1);
+    });
+
+    it('should not copy image if no image data is received', fakeAsync(() => {
+      setRouterUrl('copy', 'foo', 'bar');
+      rbdServiceGetSpy.and.returnValue(of(mock.rbd).pipe(delay(DELAY)));
+      component.ngOnInit();
+      component.submit();
+
+      expect(createAction).toHaveBeenCalledTimes(0);
+      expect(editAction).toHaveBeenCalledTimes(0);
+      expect(cloneAction).toHaveBeenCalledTimes(0);
+      expect(copyAction).toHaveBeenCalledTimes(0);
+      expect(routerNavigate).toHaveBeenCalledTimes(0);
+
+      tick(DELAY);
+    }));
+
+    it('should copy image after image data is received', () => {
+      setRouterUrl('copy', 'foo', 'bar');
+      component.ngOnInit();
+      component.submit();
+
+      expect(createAction).toHaveBeenCalledTimes(0);
+      expect(editAction).toHaveBeenCalledTimes(0);
+      expect(cloneAction).toHaveBeenCalledTimes(0);
+      expect(copyAction).toHaveBeenCalledTimes(1);
+      expect(routerNavigate).toHaveBeenCalledTimes(1);
+    });
+  });
+
   describe('should test decodeURIComponent of params', () => {
     let rbdService: RbdService;
 
     beforeEach(() => {
-      rbdService = TestBed.get(RbdService);
+      rbdService = TestBed.inject(RbdService);
       component.mode = RbdFormMode.editing;
       fixture.detectChanges();
       spyOn(rbdService, 'get').and.callThrough();
     });
 
+    it('with namespace', () => {
+      activatedRoute.setParams({ image_spec: 'foo%2Fbar%2Fbaz' });
+
+      expect(rbdService.get).toHaveBeenCalledWith(new ImageSpec('foo', 'bar', 'baz'));
+    });
+
     it('without snapName', () => {
-      activatedRoute.setParams({ pool: 'foo%2Ffoo', name: 'bar%2Fbar', snap: undefined });
+      activatedRoute.setParams({ image_spec: 'foo%2Fbar', snap: undefined });
 
-      expect(rbdService.get).toHaveBeenCalledWith('foo/foo', 'bar/bar');
+      expect(rbdService.get).toHaveBeenCalledWith(new ImageSpec('foo', null, 'bar'));
       expect(component.snapName).toBeUndefined();
     });
 
     it('with snapName', () => {
-      activatedRoute.setParams({ pool: 'foo%2Ffoo', name: 'bar%2Fbar', snap: 'baz%2Fbaz' });
+      activatedRoute.setParams({ image_spec: 'foo%2Fbar', snap: 'baz%2Fbaz' });
 
-      expect(rbdService.get).toHaveBeenCalledWith('foo/foo', 'bar/bar');
+      expect(rbdService.get).toHaveBeenCalledWith(new ImageSpec('foo', null, 'bar'));
       expect(component.snapName).toBe('baz/baz');
     });
   });
@@ -83,12 +295,15 @@ describe('RbdFormComponent', () => {
   describe('test image configuration component', () => {
     it('is visible', () => {
       fixture.detectChanges();
-      expect(queryNativeElement('cd-rbd-configuration-form').parentElement.hidden).toBe(false);
+      expect(
+        fixture.debugElement.query(By.css('cd-rbd-configuration-form')).nativeElement.parentElement
+          .hidden
+      ).toBe(true);
     });
   });
 
   describe('tests for feature flags', () => {
-    let deepFlatten, layering, exclusiveLock, objectMap, journaling, fastDiff;
+    let deepFlatten: any, layering: any, exclusiveLock: any, objectMap: any, fastDiff: any;
     const defaultFeatures = [
       // Supposed to be enabled by default
       'deep-flatten',
@@ -102,10 +317,9 @@ describe('RbdFormComponent', () => {
       'layering',
       'exclusive-lock',
       'object-map',
-      'journaling',
       'fast-diff'
     ];
-    const setFeatures = (features) => {
+    const setFeatures = (features: Record<string, RbdImageFeature>) => {
       component.features = features;
       component.featuresList = component.objToArray(features);
       component.createForm();
@@ -127,7 +341,7 @@ describe('RbdFormComponent', () => {
 
     describe('test edit form flags', () => {
       const prepare = (pool: string, image: string, enabledFeatures: string[]): void => {
-        const rbdService = TestBed.get(RbdService);
+        const rbdService = TestBed.inject(RbdService);
         spyOn(rbdService, 'get').and.returnValue(
           of({
             name: image,
@@ -136,16 +350,9 @@ describe('RbdFormComponent', () => {
           })
         );
         spyOn(rbdService, 'defaultFeatures').and.returnValue(of(defaultFeatures));
-        component.router = { url: `/block/rbd/edit/${pool}/${image}` } as Router;
+        setRouterUrl('edit', pool, image);
         fixture.detectChanges();
-        [
-          deepFlatten,
-          layering,
-          exclusiveLock,
-          objectMap,
-          journaling,
-          fastDiff
-        ] = getFeatureNativeElements();
+        [deepFlatten, layering, exclusiveLock, objectMap, fastDiff] = getFeatureNativeElements();
       };
 
       it('should have the interlock feature for flags disabled, if one feature is not set', () => {
@@ -184,18 +391,11 @@ describe('RbdFormComponent', () => {
 
     describe('test create form flags', () => {
       beforeEach(() => {
-        const rbdService = TestBed.get(RbdService);
+        const rbdService = TestBed.inject(RbdService);
         spyOn(rbdService, 'defaultFeatures').and.returnValue(of(defaultFeatures));
-        component.router = { url: '/block/rbd/create' } as Router;
+        setRouterUrl('create');
         fixture.detectChanges();
-        [
-          deepFlatten,
-          layering,
-          exclusiveLock,
-          objectMap,
-          journaling,
-          fastDiff
-        ] = getFeatureNativeElements();
+        [deepFlatten, layering, exclusiveLock, objectMap, fastDiff] = getFeatureNativeElements();
       });
 
       it('should initialize the checkboxes correctly', () => {
@@ -203,21 +403,18 @@ describe('RbdFormComponent', () => {
         expect(layering.disabled).toBe(false);
         expect(exclusiveLock.disabled).toBe(false);
         expect(objectMap.disabled).toBe(false);
-        expect(journaling.disabled).toBe(false);
         expect(fastDiff.disabled).toBe(false);
 
         expect(deepFlatten.checked).toBe(true);
         expect(layering.checked).toBe(true);
         expect(exclusiveLock.checked).toBe(true);
         expect(objectMap.checked).toBe(true);
-        expect(journaling.checked).toBe(false);
         expect(fastDiff.checked).toBe(true);
       });
 
       it('should disable features if their requirements are not met (exclusive-lock)', () => {
         exclusiveLock.click(); // unchecks exclusive-lock
         expect(objectMap.disabled).toBe(true);
-        expect(journaling.disabled).toBe(true);
         expect(fastDiff.disabled).toBe(true);
       });
 
@@ -226,5 +423,62 @@ describe('RbdFormComponent', () => {
         expect(fastDiff.disabled).toBe(true);
       });
     });
+
+    describe('test mirroring options', () => {
+      beforeEach(() => {
+        component.ngOnInit();
+        fixture.detectChanges();
+        const mirroring = fixture.debugElement.query(By.css('#mirroring')).nativeElement;
+        mirroring.click();
+        fixture.detectChanges();
+      });
+
+      it('should verify two mirroring options are shown', () => {
+        const journal = fixture.debugElement.query(By.css('#journal')).nativeElement;
+        const snapshot = fixture.debugElement.query(By.css('#snapshot')).nativeElement;
+        expect(journal).not.toBeNull();
+        expect(snapshot).not.toBeNull();
+      });
+
+      it('should verify only snapshot is disabled for pools that are in pool mirror mode', () => {
+        component.poolMirrorMode = 'pool';
+        fixture.detectChanges();
+        const journal = fixture.debugElement.query(By.css('#journal')).nativeElement;
+        const snapshot = fixture.debugElement.query(By.css('#snapshot')).nativeElement;
+        expect(journal.disabled).toBe(false);
+        expect(snapshot.disabled).toBe(true);
+      });
+
+      it('should set and disable exclusive-lock only for the journal mode', () => {
+        component.poolMirrorMode = 'pool';
+        component.mirroring = true;
+        const journal = fixture.debugElement.query(By.css('#journal')).nativeElement;
+        journal.click();
+        fixture.detectChanges();
+        const exclusiveLocks = fixture.debugElement.query(By.css('#exclusive-lock')).nativeElement;
+        expect(exclusiveLocks.checked).toBe(true);
+        expect(exclusiveLocks.disabled).toBe(true);
+      });
+
+      it('should have journaling feature for journaling mirror mode on createRequest', () => {
+        component.mirroring = true;
+        fixture.detectChanges();
+        const journal = fixture.debugElement.query(By.css('#journal')).nativeElement;
+        journal.click();
+        expect(journal.checked).toBe(true);
+        const request = component.createRequest();
+        expect(request.features).toContain('journaling');
+      });
+
+      it('should have journaling feature for journaling mirror mode on editRequest', () => {
+        component.mirroring = true;
+        fixture.detectChanges();
+        const journal = fixture.debugElement.query(By.css('#journal')).nativeElement;
+        journal.click();
+        expect(journal.checked).toBe(true);
+        const request = component.editRequest();
+        expect(request.features).toContain('journaling');
+      });
+    });
   });
 });