]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts
bump version to 18.2.4-pve3
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / cluster / osd / osd-list / osd-list.component.spec.ts
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';
7
8 import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
9 import _ from 'lodash';
10 import { ToastrModule } from 'ngx-toastr';
11 import { EMPTY, of } from 'rxjs';
12
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';
28 import {
29 configureTestBed,
30 OrchestratorHelper,
31 PermissionHelper,
32 TableActionHelper
33 } from '~/testing/unit-test-helper';
34 import { OsdReweightModalComponent } from '../osd-reweight-modal/osd-reweight-modal.component';
35 import { OsdListComponent } from './osd-list.component';
36
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;
43
44 const fakeAuthStorageService = {
45 getPermissions: () => {
46 return new Permissions({
47 'config-opt': ['read', 'update', 'create', 'delete'],
48 osd: ['read', 'update', 'create', 'delete']
49 });
50 }
51 };
52
53 const getTableAction = (name: string) =>
54 component.tableActions.find((action) => action.name === name);
55
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' } }];
60
61 // Table data and selection
62 component.selection = new CdTableSelection();
63 component.selection.selected = selection;
64 component.osds = data;
65 component.permissions = fakeAuthStorageService.getPermissions();
66 };
67
68 const openActionModal = (actionName: string) => {
69 setFakeSelection();
70 getTableAction(actionName).click();
71 };
72
73 /**
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.
77 */
78 const mockSafeToDestroy = () => {
79 spyOn(TestBed.inject(OsdService), 'safeToDestroy').and.callFake(() =>
80 of({ is_safe_to_destroy: true })
81 );
82 };
83
84 const mockSafeToDelete = () => {
85 spyOn(TestBed.inject(OsdService), 'safeToDelete').and.callFake(() =>
86 of({ is_safe_to_delete: true })
87 );
88 };
89
90 const mockOrch = () => {
91 const features = [OrchestratorFeature.OSD_CREATE, OrchestratorFeature.OSD_DELETE];
92 OrchestratorHelper.mockStatus(true, features);
93 };
94
95 configureTestBed({
96 imports: [
97 BrowserAnimationsModule,
98 HttpClientTestingModule,
99 PerformanceCounterModule,
100 ToastrModule.forRoot(),
101 CephModule,
102 ReactiveFormsModule,
103 NgbDropdownModule,
104 RouterTestingModule,
105 CoreModule,
106 RouterTestingModule
107 ],
108 providers: [
109 { provide: AuthStorageService, useValue: fakeAuthStorageService },
110 TableActionsComponent,
111 ModalService
112 ]
113 });
114
115 beforeEach(() => {
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.
121 close: jest.fn()
122 });
123 orchService = TestBed.inject(OrchestratorService);
124 });
125
126 it('should create', () => {
127 fixture.detectChanges();
128 expect(component).toBeTruthy();
129 });
130
131 it('should have columns that are sortable', () => {
132 fixture.detectChanges();
133 expect(
134 component.columns
135 .filter((column) => !(column.prop === undefined))
136 .every((column) => Boolean(column.prop))
137 ).toBeTruthy();
138 });
139
140 describe('getOsdList', () => {
141 let osds: any[];
142 let flagsSpy: jasmine.Spy;
143
144 const createOsd = (n: number) =>
145 <Record<string, any>>{
146 in: 'in',
147 up: 'up',
148 tree: {
149 device_class: 'ssd'
150 },
151 stats_history: {
152 op_out_bytes: [
153 [n, n],
154 [n * 2, n * 2]
155 ],
156 op_in_bytes: [
157 [n * 3, n * 3],
158 [n * 4, n * 4]
159 ]
160 },
161 stats: {
162 stat_bytes_used: n * n,
163 stat_bytes: n * n * n
164 },
165 state: []
166 };
167
168 const expectAttributeOnEveryOsd = (attr: string) =>
169 expect(component.osds.every((osd) => Boolean(_.get(osd, attr)))).toBeTruthy();
170
171 beforeEach(() => {
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();
176 });
177
178 it('should replace "this.osds" with new data', () => {
179 expect(component.osds.length).toBe(3);
180 expect(osdService.getList).toHaveBeenCalledTimes(1);
181
182 osds = [createOsd(4)];
183 component.getOsdList();
184 expect(component.osds.length).toBe(1);
185 expect(osdService.getList).toHaveBeenCalledTimes(2);
186 });
187
188 it('should have custom attribute "collectedStates"', () => {
189 expectAttributeOnEveryOsd('collectedStates');
190 expect(component.osds[0].collectedStates).toEqual(['in', 'up']);
191 });
192
193 it('should have "destroyed" state in "collectedStates"', () => {
194 osds[0].state.push('destroyed');
195 osds[0].up = 0;
196 component.getOsdList();
197
198 expectAttributeOnEveryOsd('collectedStates');
199 expect(component.osds[0].collectedStates).toEqual(['in', 'destroyed']);
200 });
201
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]);
205 });
206
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]);
210 });
211
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);
217 });
218
219 it('should have custom attribute "cdIsBinary" to be true', () => {
220 expectAttributeOnEveryOsd('cdIsBinary');
221 expect(component.osds[0].cdIsBinary).toBe(true);
222 });
223
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'];
229 osds = [osd1, osd2];
230 component.getOsdList();
231
232 expect(component.osds[0].cdIndivFlags).toStrictEqual(['noup']);
233 expect(component.osds[1].cdIndivFlags).toStrictEqual(['noup', 'noin']);
234 });
235
236 it('should not fail on empty individual flags list', () => {
237 expect(component.osds[0].cdIndivFlags).toStrictEqual([]);
238 });
239
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']);
244
245 flagsSpy.and.callFake(() => of(['noout', 'purged_snapdirs', 'nodown']));
246 component.getOsdList();
247 expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
248
249 flagsSpy.and.callFake(() => of(['recovery_deletes', 'noout', 'pglog_hardlimit', 'nodown']));
250 component.getOsdList();
251 expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
252 });
253
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([]);
258 });
259
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');
267 });
268 });
269
270 describe('show osd actions as defined', () => {
271 const getOsdActions = () => {
272 fixture.detectChanges();
273 return fixture.debugElement.query(By.css('#cluster-wide-actions')).componentInstance
274 .dropDownActions;
275 };
276
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]
281 );
282 });
283
284 it('shows both osd actions', () => {
285 const osdActions = getOsdActions();
286 expect(osdActions).toEqual(component.clusterWideActions);
287 expect(osdActions.length).toBe(3);
288 });
289
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);
295 });
296
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);
303 });
304
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([]);
310 });
311 });
312
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
317 );
318
319 expect(tableActions).toEqual({
320 'create,update,delete': {
321 actions: [
322 'Create',
323 'Edit',
324 'Flags',
325 'Scrub',
326 'Deep Scrub',
327 'Reweight',
328 'Mark Out',
329 'Mark In',
330 'Mark Down',
331 'Mark Lost',
332 'Purge',
333 'Destroy',
334 'Delete'
335 ],
336 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
337 },
338 'create,update': {
339 actions: [
340 'Create',
341 'Edit',
342 'Flags',
343 'Scrub',
344 'Deep Scrub',
345 'Reweight',
346 'Mark Out',
347 'Mark In',
348 'Mark Down'
349 ],
350 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
351 },
352 'create,delete': {
353 actions: ['Create', 'Mark Lost', 'Purge', 'Destroy', 'Delete'],
354 primary: {
355 multiple: 'Create',
356 executing: 'Mark Lost',
357 single: 'Mark Lost',
358 no: 'Create'
359 }
360 },
361 create: {
362 actions: ['Create'],
363 primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
364 },
365 'update,delete': {
366 actions: [
367 'Edit',
368 'Flags',
369 'Scrub',
370 'Deep Scrub',
371 'Reweight',
372 'Mark Out',
373 'Mark In',
374 'Mark Down',
375 'Mark Lost',
376 'Purge',
377 'Destroy',
378 'Delete'
379 ],
380 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
381 },
382 update: {
383 actions: [
384 'Edit',
385 'Flags',
386 'Scrub',
387 'Deep Scrub',
388 'Reweight',
389 'Mark Out',
390 'Mark In',
391 'Mark Down'
392 ],
393 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
394 },
395 delete: {
396 actions: ['Mark Lost', 'Purge', 'Destroy', 'Delete'],
397 primary: {
398 multiple: 'Mark Lost',
399 executing: 'Mark Lost',
400 single: 'Mark Lost',
401 no: 'Mark Lost'
402 }
403 },
404 'no-permissions': {
405 actions: [],
406 primary: { multiple: '', executing: '', single: '', no: '' }
407 }
408 });
409 });
410
411 describe('test table actions in submenu', () => {
412 beforeEach(() => {
413 fixture.detectChanges();
414 });
415
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);
420 tick();
421 fixture.detectChanges();
422 }));
423
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;
429
430 component.tableActions.forEach((action) => {
431 if (action.name === 'Create') {
432 return;
433 }
434 expect(getActionClasses(action).disabled).toBe(true);
435 });
436 });
437 });
438
439 describe('tests if all modals are opened correctly', () => {
440 /**
441 * Helper function to check if a function opens a modal
442 *
443 * @param modalClass - The expected class of the modal
444 */
445 const expectOpensModal = (actionName: string, modalClass: any): void => {
446 openActionModal(actionName);
447
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);
451
452 modalServiceShowSpy.calls.reset();
453 };
454
455 it('opens the reweight modal', () => {
456 expectOpensModal('Reweight', OsdReweightModalComponent);
457 });
458
459 it('opens the form modal', () => {
460 expectOpensModal('Edit', FormModalComponent);
461 });
462
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);
468 });
469
470 it('opens all critical confirmation modals', () => {
471 const modalClass = CriticalConfirmationModalComponent;
472 mockSafeToDestroy();
473 expectOpensModal('Mark Lost', modalClass);
474 expectOpensModal('Purge', modalClass);
475 expectOpensModal('Destroy', modalClass);
476 mockOrch();
477 mockSafeToDelete();
478 expectOpensModal('Delete', modalClass);
479 });
480 });
481
482 describe('tests if the correct methods are called on confirmation', () => {
483 const expectOsdServiceMethodCalled = (
484 actionName: string,
485 osdServiceMethodName:
486 | 'markOut'
487 | 'markIn'
488 | 'markDown'
489 | 'markLost'
490 | 'purge'
491 | 'destroy'
492 | 'delete'
493 ): void => {
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);
499
500 expect(osdServiceSpy.calls.count()).toBe(1);
501 expect(osdServiceSpy.calls.first().args[0]).toBe(1);
502
503 // Reset spies to be able to recreate them
504 osdServiceSpy.calls.reset();
505 modalServiceShowSpy.calls.reset();
506 };
507
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');
512 });
513
514 it('calls the corresponding service methods in critical confirmation modals', () => {
515 mockSafeToDestroy();
516 expectOsdServiceMethodCalled('Mark Lost', 'markLost');
517 expectOsdServiceMethodCalled('Purge', 'purge');
518 expectOsdServiceMethodCalled('Destroy', 'destroy');
519 mockOrch();
520 mockSafeToDelete();
521 expectOsdServiceMethodCalled('Delete', 'delete');
522 });
523 });
524
525 describe('table actions', () => {
526 const fakeOsds = require('./fixtures/osd_list_response.json');
527
528 beforeEach(() => {
529 component.permissions = fakeAuthStorageService.getPermissions();
530 spyOn(osdService, 'getList').and.callFake(() => of(fakeOsds));
531 spyOn(osdService, 'getFlags').and.callFake(() => of([]));
532 });
533
534 const testTableActions = async (
535 orch: boolean,
536 features: OrchestratorFeature[],
537 tests: { selectRow?: number; expectResults: any }[]
538 ) => {
539 OrchestratorHelper.mockStatus(orch, features);
540 fixture.detectChanges();
541 await fixture.whenStable();
542
543 for (const test of tests) {
544 if (test.selectRow) {
545 component.selection = new CdTableSelection();
546 component.selection.selected = [test.selectRow];
547 }
548 await TableActionHelper.verifyTableActions(
549 fixture,
550 component.tableActions,
551 test.expectResults
552 );
553 }
554 };
555
556 it('should have correct states when Orchestrator is enabled', async () => {
557 const tests = [
558 {
559 expectResults: {
560 Create: { disabled: false, disableDesc: '' },
561 Delete: { disabled: true, disableDesc: '' }
562 }
563 },
564 {
565 selectRow: fakeOsds[0],
566 expectResults: {
567 Create: { disabled: false, disableDesc: '' },
568 Delete: { disabled: false, disableDesc: '' }
569 }
570 },
571 {
572 selectRow: fakeOsds[1], // Select a row that is not managed.
573 expectResults: {
574 Create: { disabled: false, disableDesc: '' },
575 Delete: { disabled: true, disableDesc: '' }
576 }
577 },
578 {
579 selectRow: fakeOsds[2], // Select a row that is being deleted.
580 expectResults: {
581 Create: { disabled: false, disableDesc: '' },
582 Delete: { disabled: true, disableDesc: '' }
583 }
584 }
585 ];
586
587 const features = [
588 OrchestratorFeature.OSD_CREATE,
589 OrchestratorFeature.OSD_DELETE,
590 OrchestratorFeature.OSD_GET_REMOVE_STATUS
591 ];
592 await testTableActions(true, features, tests);
593 });
594
595 it('should have correct states when Orchestrator is disabled', async () => {
596 const resultNoOrchestrator = {
597 disabled: true,
598 disableDesc: orchService.disableMessages.noOrchestrator
599 };
600 const tests = [
601 {
602 expectResults: {
603 Create: resultNoOrchestrator,
604 Delete: { disabled: true, disableDesc: '' }
605 }
606 },
607 {
608 selectRow: fakeOsds[0],
609 expectResults: {
610 Create: resultNoOrchestrator,
611 Delete: resultNoOrchestrator
612 }
613 }
614 ];
615 await testTableActions(false, [], tests);
616 });
617
618 it('should have correct states when Orchestrator features are missing', async () => {
619 const resultMissingFeatures = {
620 disabled: true,
621 disableDesc: orchService.disableMessages.missingFeature
622 };
623 const tests = [
624 {
625 expectResults: {
626 Create: resultMissingFeatures,
627 Delete: { disabled: true, disableDesc: '' }
628 }
629 },
630 {
631 selectRow: fakeOsds[0],
632 expectResults: {
633 Create: resultMissingFeatures,
634 Delete: resultMissingFeatures
635 }
636 }
637 ];
638 await testTableActions(true, [], tests);
639 });
640 });
641 });