]> git.proxmox.com Git - ceph.git/blobdiff - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts
import 15.2.0 Octopus source
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / cluster / osd / osd-list / osd-list.component.spec.ts
index 5163bb6b70850f23b0a131189f71f467c65ed562..501943515e1a92f3fc453bbcd962a3621672c680 100644 (file)
@@ -4,9 +4,10 @@ import { ReactiveFormsModule } from '@angular/forms';
 import { By } from '@angular/platform-browser';
 import { RouterTestingModule } from '@angular/router/testing';
 
-import _ = require('lodash');
+import * as _ from 'lodash';
 import { BsModalService } from 'ngx-bootstrap/modal';
 import { TabsModule } from 'ngx-bootstrap/tabs';
+import { ToastrModule } from 'ngx-toastr';
 import { EMPTY, of } from 'rxjs';
 
 import {
@@ -14,18 +15,19 @@ import {
   i18nProviders,
   PermissionHelper
 } from '../../../../../testing/unit-test-helper';
+import { CoreModule } from '../../../../core/core.module';
+import { OrchestratorService } from '../../../../shared/api/orchestrator.service';
 import { OsdService } from '../../../../shared/api/osd.service';
 import { ConfirmationModalComponent } from '../../../../shared/components/confirmation-modal/confirmation-modal.component';
 import { CriticalConfirmationModalComponent } from '../../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { FormModalComponent } from '../../../../shared/components/form-modal/form-modal.component';
 import { TableActionsComponent } from '../../../../shared/datatable/table-actions/table-actions.component';
 import { CdTableAction } from '../../../../shared/models/cd-table-action';
 import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
 import { Permissions } from '../../../../shared/models/permissions';
 import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
-import { SharedModule } from '../../../../shared/shared.module';
+import { CephModule } from '../../../ceph.module';
 import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module';
-import { OsdDetailsComponent } from '../osd-details/osd-details.component';
-import { OsdPerformanceHistogramComponent } from '../osd-performance-histogram/osd-performance-histogram.component';
 import { OsdReweightModalComponent } from '../osd-reweight-modal/osd-reweight-modal.component';
 import { OsdListComponent } from './osd-list.component';
 
@@ -37,26 +39,29 @@ describe('OsdListComponent', () => {
 
   const fakeAuthStorageService = {
     getPermissions: () => {
-      return new Permissions({ osd: ['read', 'update', 'create', 'delete'] });
+      return new Permissions({
+        'config-opt': ['read', 'update', 'create', 'delete'],
+        osd: ['read', 'update', 'create', 'delete']
+      });
     }
   };
 
-  const getTableAction = (name) => component.tableActions.find((action) => action.name === name);
+  const getTableAction = (name: string) =>
+    component.tableActions.find((action) => action.name === name);
 
   const setFakeSelection = () => {
     // Default data and selection
-    const selection = [{ id: 1 }];
-    const data = [{ id: 1 }];
+    const selection = [{ id: 1, tree: { device_class: 'ssd' } }];
+    const data = [{ id: 1, tree: { device_class: 'ssd' } }];
 
     // Table data and selection
     component.selection = new CdTableSelection();
     component.selection.selected = selection;
-    component.selection.update();
     component.osds = data;
     component.permissions = fakeAuthStorageService.getPermissions();
   };
 
-  const openActionModal = (actionName) => {
+  const openActionModal = (actionName: string) => {
     setFakeSelection();
     getTableAction(actionName).click();
   };
@@ -68,20 +73,33 @@ describe('OsdListComponent', () => {
    */
   const mockSafeToDestroy = () => {
     spyOn(TestBed.get(OsdService), 'safeToDestroy').and.callFake(() =>
-      of({ 'safe-to-destroy': true })
+      of({ is_safe_to_destroy: true })
+    );
+  };
+
+  const mockSafeToDelete = () => {
+    spyOn(TestBed.get(OsdService), 'safeToDelete').and.callFake(() =>
+      of({ is_safe_to_delete: true })
     );
   };
 
+  const mockOrchestratorStatus = () => {
+    spyOn(TestBed.get(OrchestratorService), 'status').and.callFake(() => of({ available: true }));
+  };
+
   configureTestBed({
     imports: [
       HttpClientTestingModule,
       PerformanceCounterModule,
       TabsModule.forRoot(),
-      SharedModule,
+      ToastrModule.forRoot(),
+      CephModule,
       ReactiveFormsModule,
+      RouterTestingModule,
+      CoreModule,
       RouterTestingModule
     ],
-    declarations: [OsdListComponent, OsdDetailsComponent, OsdPerformanceHistogramComponent],
+    declarations: [],
     providers: [
       { provide: AuthStorageService, useValue: fakeAuthStorageService },
       TableActionsComponent,
@@ -92,7 +110,6 @@ describe('OsdListComponent', () => {
 
   beforeEach(() => {
     fixture = TestBed.createComponent(OsdListComponent);
-    fixture.detectChanges();
     component = fixture.componentInstance;
     osdService = TestBed.get(OsdService);
     modalServiceShowSpy = spyOn(TestBed.get(BsModalService), 'show').and.stub();
@@ -104,24 +121,34 @@ describe('OsdListComponent', () => {
   });
 
   it('should have columns that are sortable', () => {
-    expect(component.columns.every((column) => Boolean(column.prop))).toBeTruthy();
+    fixture.detectChanges();
+    expect(
+      component.columns
+        .filter((column) => !column.checkboxable)
+        .every((column) => Boolean(column.prop))
+    ).toBeTruthy();
   });
 
   describe('getOsdList', () => {
-    let osds;
-
-    const createOsd = (n: number) => ({
-      in: 'in',
-      up: 'up',
-      stats_history: {
-        op_out_bytes: [[n, n], [n * 2, n * 2]],
-        op_in_bytes: [[n * 3, n * 3], [n * 4, n * 4]]
-      },
-      stats: {
-        stat_bytes_used: n * n,
-        stat_bytes: n * n * n
-      }
-    });
+    let osds: any[];
+
+    const createOsd = (n: number) =>
+      <Record<string, any>>{
+        in: 'in',
+        up: 'up',
+        tree: {
+          device_class: 'ssd'
+        },
+        stats_history: {
+          op_out_bytes: [[n, n], [n * 2, n * 2]],
+          op_in_bytes: [[n * 3, n * 3], [n * 4, n * 4]]
+        },
+        stats: {
+          stat_bytes_used: n * n,
+          stat_bytes: n * n * n
+        },
+        state: []
+      };
 
     const expectAttributeOnEveryOsd = (attr: string) =>
       expect(component.osds.every((osd) => Boolean(_.get(osd, attr)))).toBeTruthy();
@@ -147,6 +174,15 @@ describe('OsdListComponent', () => {
       expect(component.osds[0].collectedStates).toEqual(['in', 'up']);
     });
 
+    it('should have "destroyed" state in "collectedStates"', () => {
+      osds[0].state.push('destroyed');
+      osds[0].up = 0;
+      component.getOsdList();
+
+      expectAttributeOnEveryOsd('collectedStates');
+      expect(component.osds[0].collectedStates).toEqual(['in', 'destroyed']);
+    });
+
     it('should have custom attribute "stats_history.out_bytes"', () => {
       expectAttributeOnEveryOsd('stats_history.out_bytes');
       expect(component.osds[0].stats_history.out_bytes).toEqual([1, 2]);
@@ -170,37 +206,140 @@ describe('OsdListComponent', () => {
     });
   });
 
-  describe('show table actions as defined', () => {
-    let tableActions: TableActionsComponent;
-    let scenario: { fn; empty; single };
-    let permissionHelper: PermissionHelper;
-
-    const getTableActionComponent = () => {
+  describe('show osd actions as defined', () => {
+    const getOsdActions = () => {
       fixture.detectChanges();
-      return fixture.debugElement.query(By.directive(TableActionsComponent)).componentInstance;
+      return fixture.debugElement.query(By.css('#cluster-wide-actions')).componentInstance
+        .dropDownActions;
     };
 
-    beforeEach(() => {
-      permissionHelper = new PermissionHelper(component.permissions.osd, () =>
-        getTableActionComponent()
+    it('shows osd actions after osd-actions', () => {
+      fixture.detectChanges();
+      expect(fixture.debugElement.query(By.css('#cluster-wide-actions'))).toBe(
+        fixture.debugElement.queryAll(By.directive(TableActionsComponent))[1]
       );
-      scenario = {
-        fn: () => tableActions.getCurrentButton().name,
-        single: 'Scrub',
-        empty: 'Scrub'
-      };
-      tableActions = permissionHelper.setPermissionsAndGetActions(1, 1, 1);
     });
 
-    it('shows action button', () => permissionHelper.testScenarios(scenario));
+    it('shows both osd actions', () => {
+      const osdActions = getOsdActions();
+      expect(osdActions).toEqual(component.clusterWideActions);
+      expect(osdActions.length).toBe(3);
+    });
 
-    it('shows all actions', () => {
-      expect(tableActions.tableActions.length).toBe(9);
-      expect(tableActions.tableActions).toEqual(component.tableActions);
+    it('shows only "Flags" action', () => {
+      component.permissions.configOpt.read = false;
+      const osdActions = getOsdActions();
+      expect(osdActions[0].name).toBe('Flags');
+      expect(osdActions.length).toBe(1);
+    });
+
+    it('shows only "Recovery Priority" action', () => {
+      component.permissions.osd.read = false;
+      const osdActions = getOsdActions();
+      expect(osdActions[0].name).toBe('Recovery Priority');
+      expect(osdActions[1].name).toBe('PG scrub');
+      expect(osdActions.length).toBe(2);
+    });
+
+    it('shows no osd actions', () => {
+      component.permissions.configOpt.read = false;
+      component.permissions.osd.read = false;
+      const osdActions = getOsdActions();
+      expect(osdActions).toEqual([]);
+    });
+  });
+
+  it('should test all TableActions combinations', () => {
+    const permissionHelper: PermissionHelper = new PermissionHelper(component.permissions.osd);
+    const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
+      component.tableActions
+    );
+
+    expect(tableActions).toEqual({
+      'create,update,delete': {
+        actions: [
+          'Create',
+          'Edit',
+          'Scrub',
+          'Deep Scrub',
+          'Reweight',
+          'Mark Out',
+          'Mark In',
+          'Mark Down',
+          'Mark Lost',
+          'Purge',
+          'Destroy',
+          'Delete'
+        ],
+        primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
+      },
+      'create,update': {
+        actions: [
+          'Create',
+          'Edit',
+          'Scrub',
+          'Deep Scrub',
+          'Reweight',
+          'Mark Out',
+          'Mark In',
+          'Mark Down'
+        ],
+        primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
+      },
+      'create,delete': {
+        actions: ['Create', 'Mark Lost', 'Purge', 'Destroy', 'Delete'],
+        primary: {
+          multiple: 'Create',
+          executing: 'Mark Lost',
+          single: 'Mark Lost',
+          no: 'Create'
+        }
+      },
+      create: {
+        actions: ['Create'],
+        primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+      },
+      'update,delete': {
+        actions: [
+          'Edit',
+          'Scrub',
+          'Deep Scrub',
+          'Reweight',
+          'Mark Out',
+          'Mark In',
+          'Mark Down',
+          'Mark Lost',
+          'Purge',
+          'Destroy',
+          'Delete'
+        ],
+        primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
+      },
+      update: {
+        actions: ['Edit', 'Scrub', 'Deep Scrub', 'Reweight', 'Mark Out', 'Mark In', 'Mark Down'],
+        primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
+      },
+      delete: {
+        actions: ['Mark Lost', 'Purge', 'Destroy', 'Delete'],
+        primary: {
+          multiple: 'Mark Lost',
+          executing: 'Mark Lost',
+          single: 'Mark Lost',
+          no: 'Mark Lost'
+        }
+      },
+      'no-permissions': {
+        actions: [],
+        primary: { multiple: '', executing: '', single: '', no: '' }
+      }
     });
   });
 
   describe('test table actions in submenu', () => {
+    beforeEach(() => {
+      fixture.detectChanges();
+    });
+
     beforeEach(fakeAsync(() => {
       // The menu needs a click to render the dropdown!
       const dropDownToggle = fixture.debugElement.query(By.css('.dropdown-toggle'));
@@ -209,13 +348,16 @@ describe('OsdListComponent', () => {
       fixture.detectChanges();
     }));
 
-    it('has all menu entries disabled', () => {
+    it('has all menu entries disabled except create', () => {
       const tableActionElement = fixture.debugElement.query(By.directive(TableActionsComponent));
       const toClassName = TestBed.get(TableActionsComponent).toClassName;
       const getActionClasses = (action: CdTableAction) =>
-        tableActionElement.query(By.css('.' + toClassName(action.name))).classes;
+        tableActionElement.query(By.css(`.${toClassName(action.name)} .dropdown-item`)).classes;
 
       component.tableActions.forEach((action) => {
+        if (action.name === 'Create') {
+          return;
+        }
         expect(getActionClasses(action).disabled).toBe(true);
       });
     });
@@ -227,7 +369,7 @@ describe('OsdListComponent', () => {
      *
      * @param modalClass - The expected class of the modal
      */
-    const expectOpensModal = (actionName: string, modalClass): void => {
+    const expectOpensModal = (actionName: string, modalClass: any): void => {
       openActionModal(actionName);
 
       // @TODO: check why tsc is complaining when passing 'expectationFailOutput' param.
@@ -241,6 +383,10 @@ describe('OsdListComponent', () => {
       expectOpensModal('Reweight', OsdReweightModalComponent);
     });
 
+    it('opens the form modal', () => {
+      expectOpensModal('Edit', FormModalComponent);
+    });
+
     it('opens all confirmation modals', () => {
       const modalClass = ConfirmationModalComponent;
       expectOpensModal('Mark Out', modalClass);
@@ -254,13 +400,23 @@ describe('OsdListComponent', () => {
       expectOpensModal('Mark Lost', modalClass);
       expectOpensModal('Purge', modalClass);
       expectOpensModal('Destroy', modalClass);
+      mockOrchestratorStatus();
+      mockSafeToDelete();
+      expectOpensModal('Delete', modalClass);
     });
   });
 
   describe('tests if the correct methods are called on confirmation', () => {
     const expectOsdServiceMethodCalled = (
       actionName: string,
-      osdServiceMethodName: 'markOut' | 'markIn' | 'markDown' | 'markLost' | 'purge' | 'destroy'
+      osdServiceMethodName:
+        | 'markOut'
+        | 'markIn'
+        | 'markDown'
+        | 'markLost'
+        | 'purge'
+        | 'destroy'
+        | 'delete'
     ): void => {
       const osdServiceSpy = spyOn(osdService, osdServiceMethodName).and.callFake(() => EMPTY);
       openActionModal(actionName);
@@ -287,6 +443,9 @@ describe('OsdListComponent', () => {
       expectOsdServiceMethodCalled('Mark Lost', 'markLost');
       expectOsdServiceMethodCalled('Purge', 'purge');
       expectOsdServiceMethodCalled('Destroy', 'destroy');
+      mockOrchestratorStatus();
+      mockSafeToDelete();
+      expectOsdServiceMethodCalled('Delete', 'delete');
     });
   });
 });