]> 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
bba92939450b3b62d096f897706a05eb6e02a66d
[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 { RouterTestingModule } from '@angular/router/testing';
6
7 import * as _ from 'lodash';
8 import { BsModalService } from 'ngx-bootstrap/modal';
9 import { TabsModule } from 'ngx-bootstrap/tabs';
10 import { ToastrModule } from 'ngx-toastr';
11 import { EMPTY, of } from 'rxjs';
12
13 import {
14 configureTestBed,
15 i18nProviders,
16 PermissionHelper
17 } from '../../../../../testing/unit-test-helper';
18 import { CoreModule } from '../../../../core/core.module';
19 import { OrchestratorService } from '../../../../shared/api/orchestrator.service';
20 import { OsdService } from '../../../../shared/api/osd.service';
21 import { ConfirmationModalComponent } from '../../../../shared/components/confirmation-modal/confirmation-modal.component';
22 import { CriticalConfirmationModalComponent } from '../../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
23 import { FormModalComponent } from '../../../../shared/components/form-modal/form-modal.component';
24 import { TableActionsComponent } from '../../../../shared/datatable/table-actions/table-actions.component';
25 import { CdTableAction } from '../../../../shared/models/cd-table-action';
26 import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
27 import { Permissions } from '../../../../shared/models/permissions';
28 import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
29 import { CephModule } from '../../../ceph.module';
30 import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module';
31 import { OsdReweightModalComponent } from '../osd-reweight-modal/osd-reweight-modal.component';
32 import { OsdListComponent } from './osd-list.component';
33
34 describe('OsdListComponent', () => {
35 let component: OsdListComponent;
36 let fixture: ComponentFixture<OsdListComponent>;
37 let modalServiceShowSpy: jasmine.Spy;
38 let osdService: OsdService;
39
40 const fakeAuthStorageService = {
41 getPermissions: () => {
42 return new Permissions({
43 'config-opt': ['read', 'update', 'create', 'delete'],
44 osd: ['read', 'update', 'create', 'delete']
45 });
46 }
47 };
48
49 const getTableAction = (name: string) =>
50 component.tableActions.find((action) => action.name === name);
51
52 const setFakeSelection = () => {
53 // Default data and selection
54 const selection = [{ id: 1, tree: { device_class: 'ssd' } }];
55 const data = [{ id: 1, tree: { device_class: 'ssd' } }];
56
57 // Table data and selection
58 component.selection = new CdTableSelection();
59 component.selection.selected = selection;
60 component.osds = data;
61 component.permissions = fakeAuthStorageService.getPermissions();
62 };
63
64 const openActionModal = (actionName: string) => {
65 setFakeSelection();
66 getTableAction(actionName).click();
67 };
68
69 /**
70 * The following modals are called after the information about their
71 * safety to destroy/remove/mark them lost has been retrieved, hence
72 * we will have to fake its request to be able to open those modals.
73 */
74 const mockSafeToDestroy = () => {
75 spyOn(TestBed.get(OsdService), 'safeToDestroy').and.callFake(() =>
76 of({ is_safe_to_destroy: true })
77 );
78 };
79
80 const mockSafeToDelete = () => {
81 spyOn(TestBed.get(OsdService), 'safeToDelete').and.callFake(() =>
82 of({ is_safe_to_delete: true })
83 );
84 };
85
86 const mockOrchestratorStatus = () => {
87 spyOn(TestBed.get(OrchestratorService), 'status').and.callFake(() => of({ available: true }));
88 };
89
90 configureTestBed({
91 imports: [
92 HttpClientTestingModule,
93 PerformanceCounterModule,
94 TabsModule.forRoot(),
95 ToastrModule.forRoot(),
96 CephModule,
97 ReactiveFormsModule,
98 RouterTestingModule,
99 CoreModule,
100 RouterTestingModule
101 ],
102 declarations: [],
103 providers: [
104 { provide: AuthStorageService, useValue: fakeAuthStorageService },
105 TableActionsComponent,
106 BsModalService,
107 i18nProviders
108 ]
109 });
110
111 beforeEach(() => {
112 fixture = TestBed.createComponent(OsdListComponent);
113 component = fixture.componentInstance;
114 osdService = TestBed.get(OsdService);
115 modalServiceShowSpy = spyOn(TestBed.get(BsModalService), 'show').and.stub();
116 });
117
118 it('should create', () => {
119 fixture.detectChanges();
120 expect(component).toBeTruthy();
121 });
122
123 it('should have columns that are sortable', () => {
124 fixture.detectChanges();
125 expect(
126 component.columns
127 .filter((column) => !column.checkboxable)
128 .every((column) => Boolean(column.prop))
129 ).toBeTruthy();
130 });
131
132 describe('getOsdList', () => {
133 let osds: any[];
134
135 const createOsd = (n: number) =>
136 <Record<string, any>>{
137 in: 'in',
138 up: 'up',
139 tree: {
140 device_class: 'ssd'
141 },
142 stats_history: {
143 op_out_bytes: [
144 [n, n],
145 [n * 2, n * 2]
146 ],
147 op_in_bytes: [
148 [n * 3, n * 3],
149 [n * 4, n * 4]
150 ]
151 },
152 stats: {
153 stat_bytes_used: n * n,
154 stat_bytes: n * n * n
155 },
156 state: []
157 };
158
159 const expectAttributeOnEveryOsd = (attr: string) =>
160 expect(component.osds.every((osd) => Boolean(_.get(osd, attr)))).toBeTruthy();
161
162 beforeEach(() => {
163 spyOn(osdService, 'getList').and.callFake(() => of(osds));
164 osds = [createOsd(1), createOsd(2), createOsd(3)];
165 component.getOsdList();
166 });
167
168 it('should replace "this.osds" with new data', () => {
169 expect(component.osds.length).toBe(3);
170 expect(osdService.getList).toHaveBeenCalledTimes(1);
171
172 osds = [createOsd(4)];
173 component.getOsdList();
174 expect(component.osds.length).toBe(1);
175 expect(osdService.getList).toHaveBeenCalledTimes(2);
176 });
177
178 it('should have custom attribute "collectedStates"', () => {
179 expectAttributeOnEveryOsd('collectedStates');
180 expect(component.osds[0].collectedStates).toEqual(['in', 'up']);
181 });
182
183 it('should have "destroyed" state in "collectedStates"', () => {
184 osds[0].state.push('destroyed');
185 osds[0].up = 0;
186 component.getOsdList();
187
188 expectAttributeOnEveryOsd('collectedStates');
189 expect(component.osds[0].collectedStates).toEqual(['in', 'destroyed']);
190 });
191
192 it('should have custom attribute "stats_history.out_bytes"', () => {
193 expectAttributeOnEveryOsd('stats_history.out_bytes');
194 expect(component.osds[0].stats_history.out_bytes).toEqual([1, 2]);
195 });
196
197 it('should have custom attribute "stats_history.in_bytes"', () => {
198 expectAttributeOnEveryOsd('stats_history.in_bytes');
199 expect(component.osds[0].stats_history.in_bytes).toEqual([3, 4]);
200 });
201
202 it('should have custom attribute "stats.usage"', () => {
203 expectAttributeOnEveryOsd('stats.usage');
204 expect(component.osds[0].stats.usage).toBe(1);
205 expect(component.osds[1].stats.usage).toBe(0.5);
206 expect(component.osds[2].stats.usage).toBe(3 / 9);
207 });
208
209 it('should have custom attribute "cdIsBinary" to be true', () => {
210 expectAttributeOnEveryOsd('cdIsBinary');
211 expect(component.osds[0].cdIsBinary).toBe(true);
212 });
213 });
214
215 describe('show osd actions as defined', () => {
216 const getOsdActions = () => {
217 fixture.detectChanges();
218 return fixture.debugElement.query(By.css('#cluster-wide-actions')).componentInstance
219 .dropDownActions;
220 };
221
222 it('shows osd actions after osd-actions', () => {
223 fixture.detectChanges();
224 expect(fixture.debugElement.query(By.css('#cluster-wide-actions'))).toBe(
225 fixture.debugElement.queryAll(By.directive(TableActionsComponent))[1]
226 );
227 });
228
229 it('shows both osd actions', () => {
230 const osdActions = getOsdActions();
231 expect(osdActions).toEqual(component.clusterWideActions);
232 expect(osdActions.length).toBe(3);
233 });
234
235 it('shows only "Flags" action', () => {
236 component.permissions.configOpt.read = false;
237 const osdActions = getOsdActions();
238 expect(osdActions[0].name).toBe('Flags');
239 expect(osdActions.length).toBe(1);
240 });
241
242 it('shows only "Recovery Priority" action', () => {
243 component.permissions.osd.read = false;
244 const osdActions = getOsdActions();
245 expect(osdActions[0].name).toBe('Recovery Priority');
246 expect(osdActions[1].name).toBe('PG scrub');
247 expect(osdActions.length).toBe(2);
248 });
249
250 it('shows no osd actions', () => {
251 component.permissions.configOpt.read = false;
252 component.permissions.osd.read = false;
253 const osdActions = getOsdActions();
254 expect(osdActions).toEqual([]);
255 });
256 });
257
258 it('should test all TableActions combinations', () => {
259 const permissionHelper: PermissionHelper = new PermissionHelper(component.permissions.osd);
260 const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
261 component.tableActions
262 );
263
264 expect(tableActions).toEqual({
265 'create,update,delete': {
266 actions: [
267 'Create',
268 'Edit',
269 'Scrub',
270 'Deep Scrub',
271 'Reweight',
272 'Mark Out',
273 'Mark In',
274 'Mark Down',
275 'Mark Lost',
276 'Purge',
277 'Destroy',
278 'Delete'
279 ],
280 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
281 },
282 'create,update': {
283 actions: [
284 'Create',
285 'Edit',
286 'Scrub',
287 'Deep Scrub',
288 'Reweight',
289 'Mark Out',
290 'Mark In',
291 'Mark Down'
292 ],
293 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
294 },
295 'create,delete': {
296 actions: ['Create', 'Mark Lost', 'Purge', 'Destroy', 'Delete'],
297 primary: {
298 multiple: 'Create',
299 executing: 'Mark Lost',
300 single: 'Mark Lost',
301 no: 'Create'
302 }
303 },
304 create: {
305 actions: ['Create'],
306 primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
307 },
308 'update,delete': {
309 actions: [
310 'Edit',
311 'Scrub',
312 'Deep Scrub',
313 'Reweight',
314 'Mark Out',
315 'Mark In',
316 'Mark Down',
317 'Mark Lost',
318 'Purge',
319 'Destroy',
320 'Delete'
321 ],
322 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
323 },
324 update: {
325 actions: ['Edit', 'Scrub', 'Deep Scrub', 'Reweight', 'Mark Out', 'Mark In', 'Mark Down'],
326 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
327 },
328 delete: {
329 actions: ['Mark Lost', 'Purge', 'Destroy', 'Delete'],
330 primary: {
331 multiple: 'Mark Lost',
332 executing: 'Mark Lost',
333 single: 'Mark Lost',
334 no: 'Mark Lost'
335 }
336 },
337 'no-permissions': {
338 actions: [],
339 primary: { multiple: '', executing: '', single: '', no: '' }
340 }
341 });
342 });
343
344 describe('test table actions in submenu', () => {
345 beforeEach(() => {
346 fixture.detectChanges();
347 });
348
349 beforeEach(fakeAsync(() => {
350 // The menu needs a click to render the dropdown!
351 const dropDownToggle = fixture.debugElement.query(By.css('.dropdown-toggle'));
352 dropDownToggle.triggerEventHandler('click', null);
353 tick();
354 fixture.detectChanges();
355 }));
356
357 it('has all menu entries disabled except create', () => {
358 const tableActionElement = fixture.debugElement.query(By.directive(TableActionsComponent));
359 const toClassName = TestBed.get(TableActionsComponent).toClassName;
360 const getActionClasses = (action: CdTableAction) =>
361 tableActionElement.query(By.css(`.${toClassName(action.name)} .dropdown-item`)).classes;
362
363 component.tableActions.forEach((action) => {
364 if (action.name === 'Create') {
365 return;
366 }
367 expect(getActionClasses(action).disabled).toBe(true);
368 });
369 });
370 });
371
372 describe('tests if all modals are opened correctly', () => {
373 /**
374 * Helper function to check if a function opens a modal
375 *
376 * @param modalClass - The expected class of the modal
377 */
378 const expectOpensModal = (actionName: string, modalClass: any): void => {
379 openActionModal(actionName);
380
381 // @TODO: check why tsc is complaining when passing 'expectationFailOutput' param.
382 expect(modalServiceShowSpy.calls.any()).toBeTruthy();
383 expect(modalServiceShowSpy.calls.first().args[0]).toBe(modalClass);
384
385 modalServiceShowSpy.calls.reset();
386 };
387
388 it('opens the reweight modal', () => {
389 expectOpensModal('Reweight', OsdReweightModalComponent);
390 });
391
392 it('opens the form modal', () => {
393 expectOpensModal('Edit', FormModalComponent);
394 });
395
396 it('opens all confirmation modals', () => {
397 const modalClass = ConfirmationModalComponent;
398 expectOpensModal('Mark Out', modalClass);
399 expectOpensModal('Mark In', modalClass);
400 expectOpensModal('Mark Down', modalClass);
401 });
402
403 it('opens all critical confirmation modals', () => {
404 const modalClass = CriticalConfirmationModalComponent;
405 mockSafeToDestroy();
406 expectOpensModal('Mark Lost', modalClass);
407 expectOpensModal('Purge', modalClass);
408 expectOpensModal('Destroy', modalClass);
409 mockOrchestratorStatus();
410 mockSafeToDelete();
411 expectOpensModal('Delete', modalClass);
412 });
413 });
414
415 describe('tests if the correct methods are called on confirmation', () => {
416 const expectOsdServiceMethodCalled = (
417 actionName: string,
418 osdServiceMethodName:
419 | 'markOut'
420 | 'markIn'
421 | 'markDown'
422 | 'markLost'
423 | 'purge'
424 | 'destroy'
425 | 'delete'
426 ): void => {
427 const osdServiceSpy = spyOn(osdService, osdServiceMethodName).and.callFake(() => EMPTY);
428 openActionModal(actionName);
429 const initialState = modalServiceShowSpy.calls.first().args[1].initialState;
430 const submit = initialState.onSubmit || initialState.submitAction;
431 submit.call(component);
432
433 expect(osdServiceSpy.calls.count()).toBe(1);
434 expect(osdServiceSpy.calls.first().args[0]).toBe(1);
435
436 // Reset spies to be able to recreate them
437 osdServiceSpy.calls.reset();
438 modalServiceShowSpy.calls.reset();
439 };
440
441 it('calls the corresponding service methods in confirmation modals', () => {
442 expectOsdServiceMethodCalled('Mark Out', 'markOut');
443 expectOsdServiceMethodCalled('Mark In', 'markIn');
444 expectOsdServiceMethodCalled('Mark Down', 'markDown');
445 });
446
447 it('calls the corresponding service methods in critical confirmation modals', () => {
448 mockSafeToDestroy();
449 expectOsdServiceMethodCalled('Mark Lost', 'markLost');
450 expectOsdServiceMethodCalled('Purge', 'purge');
451 expectOsdServiceMethodCalled('Destroy', 'destroy');
452 mockOrchestratorStatus();
453 mockSafeToDelete();
454 expectOsdServiceMethodCalled('Delete', 'delete');
455 });
456 });
457 });