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 * as _ from 'lodash';
9 import { BsModalService } from 'ngx-bootstrap/modal';
10 import { TabsModule } from 'ngx-bootstrap/tabs';
11 import { ToastrModule } from 'ngx-toastr';
12 import { EMPTY, of } from 'rxjs';
18 } from '../../../../../testing/unit-test-helper';
19 import { CoreModule } from '../../../../core/core.module';
20 import { OrchestratorService } from '../../../../shared/api/orchestrator.service';
21 import { OsdService } from '../../../../shared/api/osd.service';
22 import { ConfirmationModalComponent } from '../../../../shared/components/confirmation-modal/confirmation-modal.component';
23 import { CriticalConfirmationModalComponent } from '../../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
24 import { FormModalComponent } from '../../../../shared/components/form-modal/form-modal.component';
25 import { TableActionsComponent } from '../../../../shared/datatable/table-actions/table-actions.component';
26 import { CdTableAction } from '../../../../shared/models/cd-table-action';
27 import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
28 import { Permissions } from '../../../../shared/models/permissions';
29 import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
30 import { CephModule } from '../../../ceph.module';
31 import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module';
32 import { OsdReweightModalComponent } from '../osd-reweight-modal/osd-reweight-modal.component';
33 import { OsdListComponent } from './osd-list.component';
35 describe('OsdListComponent', () => {
36 let component: OsdListComponent;
37 let fixture: ComponentFixture<OsdListComponent>;
38 let modalServiceShowSpy: jasmine.Spy;
39 let osdService: OsdService;
41 const fakeAuthStorageService = {
42 getPermissions: () => {
43 return new Permissions({
44 'config-opt': ['read', 'update', 'create', 'delete'],
45 osd: ['read', 'update', 'create', 'delete']
50 const getTableAction = (name: string) =>
51 component.tableActions.find((action) => action.name === name);
53 const setFakeSelection = () => {
54 // Default data and selection
55 const selection = [{ id: 1, tree: { device_class: 'ssd' } }];
56 const data = [{ id: 1, tree: { device_class: 'ssd' } }];
58 // Table data and selection
59 component.selection = new CdTableSelection();
60 component.selection.selected = selection;
61 component.osds = data;
62 component.permissions = fakeAuthStorageService.getPermissions();
65 const openActionModal = (actionName: string) => {
67 getTableAction(actionName).click();
71 * The following modals are called after the information about their
72 * safety to destroy/remove/mark them lost has been retrieved, hence
73 * we will have to fake its request to be able to open those modals.
75 const mockSafeToDestroy = () => {
76 spyOn(TestBed.get(OsdService), 'safeToDestroy').and.callFake(() =>
77 of({ is_safe_to_destroy: true })
81 const mockSafeToDelete = () => {
82 spyOn(TestBed.get(OsdService), 'safeToDelete').and.callFake(() =>
83 of({ is_safe_to_delete: true })
87 const mockOrchestratorStatus = () => {
88 spyOn(TestBed.get(OrchestratorService), 'status').and.callFake(() => of({ available: true }));
93 BrowserAnimationsModule,
94 HttpClientTestingModule,
95 PerformanceCounterModule,
97 ToastrModule.forRoot(),
106 { provide: AuthStorageService, useValue: fakeAuthStorageService },
107 TableActionsComponent,
114 fixture = TestBed.createComponent(OsdListComponent);
115 component = fixture.componentInstance;
116 osdService = TestBed.get(OsdService);
117 modalServiceShowSpy = spyOn(TestBed.get(BsModalService), 'show').and.stub();
120 it('should create', () => {
121 fixture.detectChanges();
122 expect(component).toBeTruthy();
125 it('should have columns that are sortable', () => {
126 fixture.detectChanges();
129 .filter((column) => !(column.prop === undefined))
130 .every((column) => Boolean(column.prop))
134 describe('getOsdList', () => {
136 let flagsSpy: jasmine.Spy;
138 const createOsd = (n: number) =>
139 <Record<string, any>>{
156 stat_bytes_used: n * n,
157 stat_bytes: n * n * n
162 const expectAttributeOnEveryOsd = (attr: string) =>
163 expect(component.osds.every((osd) => Boolean(_.get(osd, attr)))).toBeTruthy();
166 spyOn(osdService, 'getList').and.callFake(() => of(osds));
167 flagsSpy = spyOn(osdService, 'getFlags').and.callFake(() => of([]));
168 osds = [createOsd(1), createOsd(2), createOsd(3)];
169 component.getOsdList();
172 it('should replace "this.osds" with new data', () => {
173 expect(component.osds.length).toBe(3);
174 expect(osdService.getList).toHaveBeenCalledTimes(1);
176 osds = [createOsd(4)];
177 component.getOsdList();
178 expect(component.osds.length).toBe(1);
179 expect(osdService.getList).toHaveBeenCalledTimes(2);
182 it('should have custom attribute "collectedStates"', () => {
183 expectAttributeOnEveryOsd('collectedStates');
184 expect(component.osds[0].collectedStates).toEqual(['in', 'up']);
187 it('should have "destroyed" state in "collectedStates"', () => {
188 osds[0].state.push('destroyed');
190 component.getOsdList();
192 expectAttributeOnEveryOsd('collectedStates');
193 expect(component.osds[0].collectedStates).toEqual(['in', 'destroyed']);
196 it('should have custom attribute "stats_history.out_bytes"', () => {
197 expectAttributeOnEveryOsd('stats_history.out_bytes');
198 expect(component.osds[0].stats_history.out_bytes).toEqual([1, 2]);
201 it('should have custom attribute "stats_history.in_bytes"', () => {
202 expectAttributeOnEveryOsd('stats_history.in_bytes');
203 expect(component.osds[0].stats_history.in_bytes).toEqual([3, 4]);
206 it('should have custom attribute "stats.usage"', () => {
207 expectAttributeOnEveryOsd('stats.usage');
208 expect(component.osds[0].stats.usage).toBe(1);
209 expect(component.osds[1].stats.usage).toBe(0.5);
210 expect(component.osds[2].stats.usage).toBe(3 / 9);
213 it('should have custom attribute "cdIsBinary" to be true', () => {
214 expectAttributeOnEveryOsd('cdIsBinary');
215 expect(component.osds[0].cdIsBinary).toBe(true);
218 it('should return valid individual flags only', () => {
219 const osd1 = createOsd(1);
220 const osd2 = createOsd(2);
221 osd1.state = ['noup', 'exists', 'up'];
222 osd2.state = ['noup', 'exists', 'up', 'noin'];
224 component.getOsdList();
226 expect(component.osds[0].cdIndivFlags).toStrictEqual(['noup']);
227 expect(component.osds[1].cdIndivFlags).toStrictEqual(['noup', 'noin']);
230 it('should not fail on empty individual flags list', () => {
231 expect(component.osds[0].cdIndivFlags).toStrictEqual([]);
234 it('should not return disabled cluster-wide flags', () => {
235 flagsSpy.and.callFake(() => of(['noout', 'nodown', 'sortbitwise']));
236 component.getOsdList();
237 expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
239 flagsSpy.and.callFake(() => of(['noout', 'purged_snapdirs', 'nodown']));
240 component.getOsdList();
241 expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
243 flagsSpy.and.callFake(() => of(['recovery_deletes', 'noout', 'pglog_hardlimit', 'nodown']));
244 component.getOsdList();
245 expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
248 it('should not fail on empty cluster-wide flags list', () => {
249 flagsSpy.and.callFake(() => of([]));
250 component.getOsdList();
251 expect(component.osds[0].cdClusterFlags).toStrictEqual([]);
255 describe('show osd actions as defined', () => {
256 const getOsdActions = () => {
257 fixture.detectChanges();
258 return fixture.debugElement.query(By.css('#cluster-wide-actions')).componentInstance
262 it('shows osd actions after osd-actions', () => {
263 fixture.detectChanges();
264 expect(fixture.debugElement.query(By.css('#cluster-wide-actions'))).toBe(
265 fixture.debugElement.queryAll(By.directive(TableActionsComponent))[1]
269 it('shows both osd actions', () => {
270 const osdActions = getOsdActions();
271 expect(osdActions).toEqual(component.clusterWideActions);
272 expect(osdActions.length).toBe(3);
275 it('shows only "Flags" action', () => {
276 component.permissions.configOpt.read = false;
277 const osdActions = getOsdActions();
278 expect(osdActions[0].name).toBe('Flags');
279 expect(osdActions.length).toBe(1);
282 it('shows only "Recovery Priority" action', () => {
283 component.permissions.osd.read = false;
284 const osdActions = getOsdActions();
285 expect(osdActions[0].name).toBe('Recovery Priority');
286 expect(osdActions[1].name).toBe('PG scrub');
287 expect(osdActions.length).toBe(2);
290 it('shows no osd actions', () => {
291 component.permissions.configOpt.read = false;
292 component.permissions.osd.read = false;
293 const osdActions = getOsdActions();
294 expect(osdActions).toEqual([]);
298 it('should test all TableActions combinations', () => {
299 const permissionHelper: PermissionHelper = new PermissionHelper(component.permissions.osd);
300 const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
301 component.tableActions
304 expect(tableActions).toEqual({
305 'create,update,delete': {
321 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
335 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
338 actions: ['Create', 'Mark Lost', 'Purge', 'Destroy', 'Delete'],
341 executing: 'Mark Lost',
348 primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
365 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
378 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
381 actions: ['Mark Lost', 'Purge', 'Destroy', 'Delete'],
383 multiple: 'Mark Lost',
384 executing: 'Mark Lost',
391 primary: { multiple: '', executing: '', single: '', no: '' }
396 describe('test table actions in submenu', () => {
398 fixture.detectChanges();
401 beforeEach(fakeAsync(() => {
402 // The menu needs a click to render the dropdown!
403 const dropDownToggle = fixture.debugElement.query(By.css('.dropdown-toggle'));
404 dropDownToggle.triggerEventHandler('click', null);
406 fixture.detectChanges();
409 it('has all menu entries disabled except create', () => {
410 const tableActionElement = fixture.debugElement.query(By.directive(TableActionsComponent));
411 const toClassName = TestBed.get(TableActionsComponent).toClassName;
412 const getActionClasses = (action: CdTableAction) =>
413 tableActionElement.query(By.css(`.${toClassName(action.name)} .dropdown-item`)).classes;
415 component.tableActions.forEach((action) => {
416 if (action.name === 'Create') {
419 expect(getActionClasses(action).disabled).toBe(true);
424 describe('tests if all modals are opened correctly', () => {
426 * Helper function to check if a function opens a modal
428 * @param modalClass - The expected class of the modal
430 const expectOpensModal = (actionName: string, modalClass: any): void => {
431 openActionModal(actionName);
433 // @TODO: check why tsc is complaining when passing 'expectationFailOutput' param.
434 expect(modalServiceShowSpy.calls.any()).toBeTruthy();
435 expect(modalServiceShowSpy.calls.first().args[0]).toBe(modalClass);
437 modalServiceShowSpy.calls.reset();
440 it('opens the reweight modal', () => {
441 expectOpensModal('Reweight', OsdReweightModalComponent);
444 it('opens the form modal', () => {
445 expectOpensModal('Edit', FormModalComponent);
448 it('opens all confirmation modals', () => {
449 const modalClass = ConfirmationModalComponent;
450 expectOpensModal('Mark Out', modalClass);
451 expectOpensModal('Mark In', modalClass);
452 expectOpensModal('Mark Down', modalClass);
455 it('opens all critical confirmation modals', () => {
456 const modalClass = CriticalConfirmationModalComponent;
458 expectOpensModal('Mark Lost', modalClass);
459 expectOpensModal('Purge', modalClass);
460 expectOpensModal('Destroy', modalClass);
461 mockOrchestratorStatus();
463 expectOpensModal('Delete', modalClass);
467 describe('tests if the correct methods are called on confirmation', () => {
468 const expectOsdServiceMethodCalled = (
470 osdServiceMethodName:
479 const osdServiceSpy = spyOn(osdService, osdServiceMethodName).and.callFake(() => EMPTY);
480 openActionModal(actionName);
481 const initialState = modalServiceShowSpy.calls.first().args[1].initialState;
482 const submit = initialState.onSubmit || initialState.submitAction;
483 submit.call(component);
485 expect(osdServiceSpy.calls.count()).toBe(1);
486 expect(osdServiceSpy.calls.first().args[0]).toBe(1);
488 // Reset spies to be able to recreate them
489 osdServiceSpy.calls.reset();
490 modalServiceShowSpy.calls.reset();
493 it('calls the corresponding service methods in confirmation modals', () => {
494 expectOsdServiceMethodCalled('Mark Out', 'markOut');
495 expectOsdServiceMethodCalled('Mark In', 'markIn');
496 expectOsdServiceMethodCalled('Mark Down', 'markDown');
499 it('calls the corresponding service methods in critical confirmation modals', () => {
501 expectOsdServiceMethodCalled('Mark Lost', 'markLost');
502 expectOsdServiceMethodCalled('Purge', 'purge');
503 expectOsdServiceMethodCalled('Destroy', 'destroy');
504 mockOrchestratorStatus();
506 expectOsdServiceMethodCalled('Delete', 'delete');