]> 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
import 15.2.9
[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 * 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';
13
14 import {
15 configureTestBed,
16 i18nProviders,
17 PermissionHelper
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';
34
35 describe('OsdListComponent', () => {
36 let component: OsdListComponent;
37 let fixture: ComponentFixture<OsdListComponent>;
38 let modalServiceShowSpy: jasmine.Spy;
39 let osdService: OsdService;
40
41 const fakeAuthStorageService = {
42 getPermissions: () => {
43 return new Permissions({
44 'config-opt': ['read', 'update', 'create', 'delete'],
45 osd: ['read', 'update', 'create', 'delete']
46 });
47 }
48 };
49
50 const getTableAction = (name: string) =>
51 component.tableActions.find((action) => action.name === name);
52
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' } }];
57
58 // Table data and selection
59 component.selection = new CdTableSelection();
60 component.selection.selected = selection;
61 component.osds = data;
62 component.permissions = fakeAuthStorageService.getPermissions();
63 };
64
65 const openActionModal = (actionName: string) => {
66 setFakeSelection();
67 getTableAction(actionName).click();
68 };
69
70 /**
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.
74 */
75 const mockSafeToDestroy = () => {
76 spyOn(TestBed.get(OsdService), 'safeToDestroy').and.callFake(() =>
77 of({ is_safe_to_destroy: true })
78 );
79 };
80
81 const mockSafeToDelete = () => {
82 spyOn(TestBed.get(OsdService), 'safeToDelete').and.callFake(() =>
83 of({ is_safe_to_delete: true })
84 );
85 };
86
87 const mockOrchestratorStatus = () => {
88 spyOn(TestBed.get(OrchestratorService), 'status').and.callFake(() => of({ available: true }));
89 };
90
91 configureTestBed({
92 imports: [
93 BrowserAnimationsModule,
94 HttpClientTestingModule,
95 PerformanceCounterModule,
96 TabsModule.forRoot(),
97 ToastrModule.forRoot(),
98 CephModule,
99 ReactiveFormsModule,
100 RouterTestingModule,
101 CoreModule,
102 RouterTestingModule
103 ],
104 declarations: [],
105 providers: [
106 { provide: AuthStorageService, useValue: fakeAuthStorageService },
107 TableActionsComponent,
108 BsModalService,
109 i18nProviders
110 ]
111 });
112
113 beforeEach(() => {
114 fixture = TestBed.createComponent(OsdListComponent);
115 component = fixture.componentInstance;
116 osdService = TestBed.get(OsdService);
117 modalServiceShowSpy = spyOn(TestBed.get(BsModalService), 'show').and.stub();
118 });
119
120 it('should create', () => {
121 fixture.detectChanges();
122 expect(component).toBeTruthy();
123 });
124
125 it('should have columns that are sortable', () => {
126 fixture.detectChanges();
127 expect(
128 component.columns
129 .filter((column) => !(column.prop === undefined))
130 .every((column) => Boolean(column.prop))
131 ).toBeTruthy();
132 });
133
134 describe('getOsdList', () => {
135 let osds: any[];
136 let flagsSpy: jasmine.Spy;
137
138 const createOsd = (n: number) =>
139 <Record<string, any>>{
140 in: 'in',
141 up: 'up',
142 tree: {
143 device_class: 'ssd'
144 },
145 stats_history: {
146 op_out_bytes: [
147 [n, n],
148 [n * 2, n * 2]
149 ],
150 op_in_bytes: [
151 [n * 3, n * 3],
152 [n * 4, n * 4]
153 ]
154 },
155 stats: {
156 stat_bytes_used: n * n,
157 stat_bytes: n * n * n
158 },
159 state: []
160 };
161
162 const expectAttributeOnEveryOsd = (attr: string) =>
163 expect(component.osds.every((osd) => Boolean(_.get(osd, attr)))).toBeTruthy();
164
165 beforeEach(() => {
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();
170 });
171
172 it('should replace "this.osds" with new data', () => {
173 expect(component.osds.length).toBe(3);
174 expect(osdService.getList).toHaveBeenCalledTimes(1);
175
176 osds = [createOsd(4)];
177 component.getOsdList();
178 expect(component.osds.length).toBe(1);
179 expect(osdService.getList).toHaveBeenCalledTimes(2);
180 });
181
182 it('should have custom attribute "collectedStates"', () => {
183 expectAttributeOnEveryOsd('collectedStates');
184 expect(component.osds[0].collectedStates).toEqual(['in', 'up']);
185 });
186
187 it('should have "destroyed" state in "collectedStates"', () => {
188 osds[0].state.push('destroyed');
189 osds[0].up = 0;
190 component.getOsdList();
191
192 expectAttributeOnEveryOsd('collectedStates');
193 expect(component.osds[0].collectedStates).toEqual(['in', 'destroyed']);
194 });
195
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]);
199 });
200
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]);
204 });
205
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);
211 });
212
213 it('should have custom attribute "cdIsBinary" to be true', () => {
214 expectAttributeOnEveryOsd('cdIsBinary');
215 expect(component.osds[0].cdIsBinary).toBe(true);
216 });
217
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'];
223 osds = [osd1, osd2];
224 component.getOsdList();
225
226 expect(component.osds[0].cdIndivFlags).toStrictEqual(['noup']);
227 expect(component.osds[1].cdIndivFlags).toStrictEqual(['noup', 'noin']);
228 });
229
230 it('should not fail on empty individual flags list', () => {
231 expect(component.osds[0].cdIndivFlags).toStrictEqual([]);
232 });
233
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']);
238
239 flagsSpy.and.callFake(() => of(['noout', 'purged_snapdirs', 'nodown']));
240 component.getOsdList();
241 expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
242
243 flagsSpy.and.callFake(() => of(['recovery_deletes', 'noout', 'pglog_hardlimit', 'nodown']));
244 component.getOsdList();
245 expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
246 });
247
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([]);
252 });
253 });
254
255 describe('show osd actions as defined', () => {
256 const getOsdActions = () => {
257 fixture.detectChanges();
258 return fixture.debugElement.query(By.css('#cluster-wide-actions')).componentInstance
259 .dropDownActions;
260 };
261
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]
266 );
267 });
268
269 it('shows both osd actions', () => {
270 const osdActions = getOsdActions();
271 expect(osdActions).toEqual(component.clusterWideActions);
272 expect(osdActions.length).toBe(3);
273 });
274
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);
280 });
281
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);
288 });
289
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([]);
295 });
296 });
297
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
302 );
303
304 expect(tableActions).toEqual({
305 'create,update,delete': {
306 actions: [
307 'Create',
308 'Edit',
309 'Flags',
310 'Scrub',
311 'Deep Scrub',
312 'Reweight',
313 'Mark Out',
314 'Mark In',
315 'Mark Down',
316 'Mark Lost',
317 'Purge',
318 'Destroy',
319 'Delete'
320 ],
321 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
322 },
323 'create,update': {
324 actions: [
325 'Create',
326 'Edit',
327 'Flags',
328 'Scrub',
329 'Deep Scrub',
330 'Reweight',
331 'Mark Out',
332 'Mark In',
333 'Mark Down'
334 ],
335 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
336 },
337 'create,delete': {
338 actions: ['Create', 'Mark Lost', 'Purge', 'Destroy', 'Delete'],
339 primary: {
340 multiple: 'Create',
341 executing: 'Mark Lost',
342 single: 'Mark Lost',
343 no: 'Create'
344 }
345 },
346 create: {
347 actions: ['Create'],
348 primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
349 },
350 'update,delete': {
351 actions: [
352 'Edit',
353 'Flags',
354 'Scrub',
355 'Deep Scrub',
356 'Reweight',
357 'Mark Out',
358 'Mark In',
359 'Mark Down',
360 'Mark Lost',
361 'Purge',
362 'Destroy',
363 'Delete'
364 ],
365 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
366 },
367 update: {
368 actions: [
369 'Edit',
370 'Flags',
371 'Scrub',
372 'Deep Scrub',
373 'Reweight',
374 'Mark Out',
375 'Mark In',
376 'Mark Down'
377 ],
378 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
379 },
380 delete: {
381 actions: ['Mark Lost', 'Purge', 'Destroy', 'Delete'],
382 primary: {
383 multiple: 'Mark Lost',
384 executing: 'Mark Lost',
385 single: 'Mark Lost',
386 no: 'Mark Lost'
387 }
388 },
389 'no-permissions': {
390 actions: [],
391 primary: { multiple: '', executing: '', single: '', no: '' }
392 }
393 });
394 });
395
396 describe('test table actions in submenu', () => {
397 beforeEach(() => {
398 fixture.detectChanges();
399 });
400
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);
405 tick();
406 fixture.detectChanges();
407 }));
408
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;
414
415 component.tableActions.forEach((action) => {
416 if (action.name === 'Create') {
417 return;
418 }
419 expect(getActionClasses(action).disabled).toBe(true);
420 });
421 });
422 });
423
424 describe('tests if all modals are opened correctly', () => {
425 /**
426 * Helper function to check if a function opens a modal
427 *
428 * @param modalClass - The expected class of the modal
429 */
430 const expectOpensModal = (actionName: string, modalClass: any): void => {
431 openActionModal(actionName);
432
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);
436
437 modalServiceShowSpy.calls.reset();
438 };
439
440 it('opens the reweight modal', () => {
441 expectOpensModal('Reweight', OsdReweightModalComponent);
442 });
443
444 it('opens the form modal', () => {
445 expectOpensModal('Edit', FormModalComponent);
446 });
447
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);
453 });
454
455 it('opens all critical confirmation modals', () => {
456 const modalClass = CriticalConfirmationModalComponent;
457 mockSafeToDestroy();
458 expectOpensModal('Mark Lost', modalClass);
459 expectOpensModal('Purge', modalClass);
460 expectOpensModal('Destroy', modalClass);
461 mockOrchestratorStatus();
462 mockSafeToDelete();
463 expectOpensModal('Delete', modalClass);
464 });
465 });
466
467 describe('tests if the correct methods are called on confirmation', () => {
468 const expectOsdServiceMethodCalled = (
469 actionName: string,
470 osdServiceMethodName:
471 | 'markOut'
472 | 'markIn'
473 | 'markDown'
474 | 'markLost'
475 | 'purge'
476 | 'destroy'
477 | 'delete'
478 ): void => {
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);
484
485 expect(osdServiceSpy.calls.count()).toBe(1);
486 expect(osdServiceSpy.calls.first().args[0]).toBe(1);
487
488 // Reset spies to be able to recreate them
489 osdServiceSpy.calls.reset();
490 modalServiceShowSpy.calls.reset();
491 };
492
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');
497 });
498
499 it('calls the corresponding service methods in critical confirmation modals', () => {
500 mockSafeToDestroy();
501 expectOsdServiceMethodCalled('Mark Lost', 'markLost');
502 expectOsdServiceMethodCalled('Purge', 'purge');
503 expectOsdServiceMethodCalled('Destroy', 'destroy');
504 mockOrchestratorStatus();
505 mockSafeToDelete();
506 expectOsdServiceMethodCalled('Delete', 'delete');
507 });
508 });
509 });