1 import { HttpClientTestingModule } from '@angular/common/http/testing';
2 import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
3 import { ReactiveFormsModule } from '@angular/forms';
4 import { By } from '@angular/platform-browser';
5 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
6 import { RouterTestingModule } from '@angular/router/testing';
8 import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
9 import _ from 'lodash';
10 import { ToastrModule } from 'ngx-toastr';
11 import { EMPTY, of } from 'rxjs';
13 import { CephModule } from '~/app/ceph/ceph.module';
14 import { PerformanceCounterModule } from '~/app/ceph/performance-counter/performance-counter.module';
15 import { CoreModule } from '~/app/core/core.module';
16 import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
17 import { OsdService } from '~/app/shared/api/osd.service';
18 import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
19 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
20 import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
21 import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
22 import { CdTableAction } from '~/app/shared/models/cd-table-action';
23 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
24 import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
25 import { Permissions } from '~/app/shared/models/permissions';
26 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
27 import { ModalService } from '~/app/shared/services/modal.service';
33 } from '~/testing/unit-test-helper';
34 import { OsdReweightModalComponent } from '../osd-reweight-modal/osd-reweight-modal.component';
35 import { OsdListComponent } from './osd-list.component';
37 describe('OsdListComponent', () => {
38 let component: OsdListComponent;
39 let fixture: ComponentFixture<OsdListComponent>;
40 let modalServiceShowSpy: jasmine.Spy;
41 let osdService: OsdService;
42 let orchService: OrchestratorService;
44 const fakeAuthStorageService = {
45 getPermissions: () => {
46 return new Permissions({
47 'config-opt': ['read', 'update', 'create', 'delete'],
48 osd: ['read', 'update', 'create', 'delete']
53 const getTableAction = (name: string) =>
54 component.tableActions.find((action) => action.name === name);
56 const setFakeSelection = () => {
57 // Default data and selection
58 const selection = [{ id: 1, tree: { device_class: 'ssd' } }];
59 const data = [{ id: 1, tree: { device_class: 'ssd' } }];
61 // Table data and selection
62 component.selection = new CdTableSelection();
63 component.selection.selected = selection;
64 component.osds = data;
65 component.permissions = fakeAuthStorageService.getPermissions();
68 const openActionModal = (actionName: string) => {
70 getTableAction(actionName).click();
74 * The following modals are called after the information about their
75 * safety to destroy/remove/mark them lost has been retrieved, hence
76 * we will have to fake its request to be able to open those modals.
78 const mockSafeToDestroy = () => {
79 spyOn(TestBed.inject(OsdService), 'safeToDestroy').and.callFake(() =>
80 of({ is_safe_to_destroy: true })
84 const mockSafeToDelete = () => {
85 spyOn(TestBed.inject(OsdService), 'safeToDelete').and.callFake(() =>
86 of({ is_safe_to_delete: true })
90 const mockOrch = () => {
91 const features = [OrchestratorFeature.OSD_CREATE, OrchestratorFeature.OSD_DELETE];
92 OrchestratorHelper.mockStatus(true, features);
97 BrowserAnimationsModule,
98 HttpClientTestingModule,
99 PerformanceCounterModule,
100 ToastrModule.forRoot(),
109 { provide: AuthStorageService, useValue: fakeAuthStorageService },
110 TableActionsComponent,
116 fixture = TestBed.createComponent(OsdListComponent);
117 component = fixture.componentInstance;
118 osdService = TestBed.inject(OsdService);
119 modalServiceShowSpy = spyOn(TestBed.inject(ModalService), 'show').and.returnValue({
120 // mock the close function, it might be called if there are async tests.
123 orchService = TestBed.inject(OrchestratorService);
126 it('should create', () => {
127 fixture.detectChanges();
128 expect(component).toBeTruthy();
131 it('should have columns that are sortable', () => {
132 fixture.detectChanges();
135 .filter((column) => !(column.prop === undefined))
136 .every((column) => Boolean(column.prop))
140 describe('getOsdList', () => {
142 let flagsSpy: jasmine.Spy;
144 const createOsd = (n: number) =>
145 <Record<string, any>>{
162 stat_bytes_used: n * n,
163 stat_bytes: n * n * n
168 const expectAttributeOnEveryOsd = (attr: string) =>
169 expect(component.osds.every((osd) => Boolean(_.get(osd, attr)))).toBeTruthy();
172 spyOn(osdService, 'getList').and.callFake(() => of(osds));
173 flagsSpy = spyOn(osdService, 'getFlags').and.callFake(() => of([]));
174 osds = [createOsd(1), createOsd(2), createOsd(3)];
175 component.getOsdList();
178 it('should replace "this.osds" with new data', () => {
179 expect(component.osds.length).toBe(3);
180 expect(osdService.getList).toHaveBeenCalledTimes(1);
182 osds = [createOsd(4)];
183 component.getOsdList();
184 expect(component.osds.length).toBe(1);
185 expect(osdService.getList).toHaveBeenCalledTimes(2);
188 it('should have custom attribute "collectedStates"', () => {
189 expectAttributeOnEveryOsd('collectedStates');
190 expect(component.osds[0].collectedStates).toEqual(['in', 'up']);
193 it('should have "destroyed" state in "collectedStates"', () => {
194 osds[0].state.push('destroyed');
196 component.getOsdList();
198 expectAttributeOnEveryOsd('collectedStates');
199 expect(component.osds[0].collectedStates).toEqual(['in', 'destroyed']);
202 it('should have custom attribute "stats_history.out_bytes"', () => {
203 expectAttributeOnEveryOsd('stats_history.out_bytes');
204 expect(component.osds[0].stats_history.out_bytes).toEqual([1, 2]);
207 it('should have custom attribute "stats_history.in_bytes"', () => {
208 expectAttributeOnEveryOsd('stats_history.in_bytes');
209 expect(component.osds[0].stats_history.in_bytes).toEqual([3, 4]);
212 it('should have custom attribute "stats.usage"', () => {
213 expectAttributeOnEveryOsd('stats.usage');
214 expect(component.osds[0].stats.usage).toBe(1);
215 expect(component.osds[1].stats.usage).toBe(0.5);
216 expect(component.osds[2].stats.usage).toBe(3 / 9);
219 it('should have custom attribute "cdIsBinary" to be true', () => {
220 expectAttributeOnEveryOsd('cdIsBinary');
221 expect(component.osds[0].cdIsBinary).toBe(true);
224 it('should return valid individual flags only', () => {
225 const osd1 = createOsd(1);
226 const osd2 = createOsd(2);
227 osd1.state = ['noup', 'exists', 'up'];
228 osd2.state = ['noup', 'exists', 'up', 'noin'];
230 component.getOsdList();
232 expect(component.osds[0].cdIndivFlags).toStrictEqual(['noup']);
233 expect(component.osds[1].cdIndivFlags).toStrictEqual(['noup', 'noin']);
236 it('should not fail on empty individual flags list', () => {
237 expect(component.osds[0].cdIndivFlags).toStrictEqual([]);
240 it('should not return disabled cluster-wide flags', () => {
241 flagsSpy.and.callFake(() => of(['noout', 'nodown', 'sortbitwise']));
242 component.getOsdList();
243 expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
245 flagsSpy.and.callFake(() => of(['noout', 'purged_snapdirs', 'nodown']));
246 component.getOsdList();
247 expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
249 flagsSpy.and.callFake(() => of(['recovery_deletes', 'noout', 'pglog_hardlimit', 'nodown']));
250 component.getOsdList();
251 expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
254 it('should not fail on empty cluster-wide flags list', () => {
255 flagsSpy.and.callFake(() => of([]));
256 component.getOsdList();
257 expect(component.osds[0].cdClusterFlags).toStrictEqual([]);
260 it('should have custom attribute "cdExecuting"', () => {
261 osds[1].operational_status = 'unmanaged';
262 osds[2].operational_status = 'deleting';
263 component.getOsdList();
264 expect(component.osds[0].cdExecuting).toBeUndefined();
265 expect(component.osds[1].cdExecuting).toBeUndefined();
266 expect(component.osds[2].cdExecuting).toBe('deleting');
270 describe('show osd actions as defined', () => {
271 const getOsdActions = () => {
272 fixture.detectChanges();
273 return fixture.debugElement.query(By.css('#cluster-wide-actions')).componentInstance
277 it('shows osd actions after osd-actions', () => {
278 fixture.detectChanges();
279 expect(fixture.debugElement.query(By.css('#cluster-wide-actions'))).toBe(
280 fixture.debugElement.queryAll(By.directive(TableActionsComponent))[1]
284 it('shows both osd actions', () => {
285 const osdActions = getOsdActions();
286 expect(osdActions).toEqual(component.clusterWideActions);
287 expect(osdActions.length).toBe(3);
290 it('shows only "Flags" action', () => {
291 component.permissions.configOpt.read = false;
292 const osdActions = getOsdActions();
293 expect(osdActions[0].name).toBe('Flags');
294 expect(osdActions.length).toBe(1);
297 it('shows only "Recovery Priority" action', () => {
298 component.permissions.osd.read = false;
299 const osdActions = getOsdActions();
300 expect(osdActions[0].name).toBe('Recovery Priority');
301 expect(osdActions[1].name).toBe('PG scrub');
302 expect(osdActions.length).toBe(2);
305 it('shows no osd actions', () => {
306 component.permissions.configOpt.read = false;
307 component.permissions.osd.read = false;
308 const osdActions = getOsdActions();
309 expect(osdActions).toEqual([]);
313 it('should test all TableActions combinations', () => {
314 const permissionHelper: PermissionHelper = new PermissionHelper(component.permissions.osd);
315 const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
316 component.tableActions
319 expect(tableActions).toEqual({
320 'create,update,delete': {
336 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
350 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
353 actions: ['Create', 'Mark Lost', 'Purge', 'Destroy', 'Delete'],
356 executing: 'Mark Lost',
363 primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
380 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
393 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
396 actions: ['Mark Lost', 'Purge', 'Destroy', 'Delete'],
398 multiple: 'Mark Lost',
399 executing: 'Mark Lost',
406 primary: { multiple: '', executing: '', single: '', no: '' }
411 describe('test table actions in submenu', () => {
413 fixture.detectChanges();
416 beforeEach(fakeAsync(() => {
417 // The menu needs a click to render the dropdown!
418 const dropDownToggle = fixture.debugElement.query(By.css('.dropdown-toggle'));
419 dropDownToggle.triggerEventHandler('click', null);
421 fixture.detectChanges();
424 it('has all menu entries disabled except create', () => {
425 const tableActionElement = fixture.debugElement.query(By.directive(TableActionsComponent));
426 const toClassName = TestBed.inject(TableActionsComponent).toClassName;
427 const getActionClasses = (action: CdTableAction) =>
428 tableActionElement.query(By.css(`[ngbDropdownItem].${toClassName(action)}`)).classes;
430 component.tableActions.forEach((action) => {
431 if (action.name === 'Create') {
434 expect(getActionClasses(action).disabled).toBe(true);
439 describe('tests if all modals are opened correctly', () => {
441 * Helper function to check if a function opens a modal
443 * @param modalClass - The expected class of the modal
445 const expectOpensModal = (actionName: string, modalClass: any): void => {
446 openActionModal(actionName);
448 // @TODO: check why tsc is complaining when passing 'expectationFailOutput' param.
449 expect(modalServiceShowSpy.calls.any()).toBeTruthy();
450 expect(modalServiceShowSpy.calls.first().args[0]).toBe(modalClass);
452 modalServiceShowSpy.calls.reset();
455 it('opens the reweight modal', () => {
456 expectOpensModal('Reweight', OsdReweightModalComponent);
459 it('opens the form modal', () => {
460 expectOpensModal('Edit', FormModalComponent);
463 it('opens all confirmation modals', () => {
464 const modalClass = ConfirmationModalComponent;
465 expectOpensModal('Mark Out', modalClass);
466 expectOpensModal('Mark In', modalClass);
467 expectOpensModal('Mark Down', modalClass);
470 it('opens all critical confirmation modals', () => {
471 const modalClass = CriticalConfirmationModalComponent;
473 expectOpensModal('Mark Lost', modalClass);
474 expectOpensModal('Purge', modalClass);
475 expectOpensModal('Destroy', modalClass);
478 expectOpensModal('Delete', modalClass);
482 describe('tests if the correct methods are called on confirmation', () => {
483 const expectOsdServiceMethodCalled = (
485 osdServiceMethodName:
494 const osdServiceSpy = spyOn(osdService, osdServiceMethodName).and.callFake(() => EMPTY);
495 openActionModal(actionName);
496 const initialState = modalServiceShowSpy.calls.first().args[1];
497 const submit = initialState.onSubmit || initialState.submitAction;
498 submit.call(component);
500 expect(osdServiceSpy.calls.count()).toBe(1);
501 expect(osdServiceSpy.calls.first().args[0]).toBe(1);
503 // Reset spies to be able to recreate them
504 osdServiceSpy.calls.reset();
505 modalServiceShowSpy.calls.reset();
508 it('calls the corresponding service methods in confirmation modals', () => {
509 expectOsdServiceMethodCalled('Mark Out', 'markOut');
510 expectOsdServiceMethodCalled('Mark In', 'markIn');
511 expectOsdServiceMethodCalled('Mark Down', 'markDown');
514 it('calls the corresponding service methods in critical confirmation modals', () => {
516 expectOsdServiceMethodCalled('Mark Lost', 'markLost');
517 expectOsdServiceMethodCalled('Purge', 'purge');
518 expectOsdServiceMethodCalled('Destroy', 'destroy');
521 expectOsdServiceMethodCalled('Delete', 'delete');
525 describe('table actions', () => {
526 const fakeOsds = require('./fixtures/osd_list_response.json');
529 component.permissions = fakeAuthStorageService.getPermissions();
530 spyOn(osdService, 'getList').and.callFake(() => of(fakeOsds));
531 spyOn(osdService, 'getFlags').and.callFake(() => of([]));
534 const testTableActions = async (
536 features: OrchestratorFeature[],
537 tests: { selectRow?: number; expectResults: any }[]
539 OrchestratorHelper.mockStatus(orch, features);
540 fixture.detectChanges();
541 await fixture.whenStable();
543 for (const test of tests) {
544 if (test.selectRow) {
545 component.selection = new CdTableSelection();
546 component.selection.selected = [test.selectRow];
548 await TableActionHelper.verifyTableActions(
550 component.tableActions,
556 it('should have correct states when Orchestrator is enabled', async () => {
560 Create: { disabled: false, disableDesc: '' },
561 Delete: { disabled: true, disableDesc: '' }
565 selectRow: fakeOsds[0],
567 Create: { disabled: false, disableDesc: '' },
568 Delete: { disabled: false, disableDesc: '' }
572 selectRow: fakeOsds[1], // Select a row that is not managed.
574 Create: { disabled: false, disableDesc: '' },
575 Delete: { disabled: true, disableDesc: '' }
579 selectRow: fakeOsds[2], // Select a row that is being deleted.
581 Create: { disabled: false, disableDesc: '' },
582 Delete: { disabled: true, disableDesc: '' }
588 OrchestratorFeature.OSD_CREATE,
589 OrchestratorFeature.OSD_DELETE,
590 OrchestratorFeature.OSD_GET_REMOVE_STATUS
592 await testTableActions(true, features, tests);
595 it('should have correct states when Orchestrator is disabled', async () => {
596 const resultNoOrchestrator = {
598 disableDesc: orchService.disableMessages.noOrchestrator
603 Create: resultNoOrchestrator,
604 Delete: { disabled: true, disableDesc: '' }
608 selectRow: fakeOsds[0],
610 Create: resultNoOrchestrator,
611 Delete: resultNoOrchestrator
615 await testTableActions(false, [], tests);
618 it('should have correct states when Orchestrator features are missing', async () => {
619 const resultMissingFeatures = {
621 disableDesc: orchService.disableMessages.missingFeature
626 Create: resultMissingFeatures,
627 Delete: { disabled: true, disableDesc: '' }
631 selectRow: fakeOsds[0],
633 Create: resultMissingFeatures,
634 Delete: resultMissingFeatures
638 await testTableActions(true, [], tests);