]> 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.4
[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
137 const createOsd = (n: number) =>
138 <Record<string, any>>{
139 in: 'in',
140 up: 'up',
141 tree: {
142 device_class: 'ssd'
143 },
144 stats_history: {
145 op_out_bytes: [
146 [n, n],
147 [n * 2, n * 2]
148 ],
149 op_in_bytes: [
150 [n * 3, n * 3],
151 [n * 4, n * 4]
152 ]
153 },
154 stats: {
155 stat_bytes_used: n * n,
156 stat_bytes: n * n * n
157 },
158 state: []
159 };
160
161 const expectAttributeOnEveryOsd = (attr: string) =>
162 expect(component.osds.every((osd) => Boolean(_.get(osd, attr)))).toBeTruthy();
163
164 beforeEach(() => {
165 spyOn(osdService, 'getList').and.callFake(() => of(osds));
166 osds = [createOsd(1), createOsd(2), createOsd(3)];
167 component.getOsdList();
168 });
169
170 it('should replace "this.osds" with new data', () => {
171 expect(component.osds.length).toBe(3);
172 expect(osdService.getList).toHaveBeenCalledTimes(1);
173
174 osds = [createOsd(4)];
175 component.getOsdList();
176 expect(component.osds.length).toBe(1);
177 expect(osdService.getList).toHaveBeenCalledTimes(2);
178 });
179
180 it('should have custom attribute "collectedStates"', () => {
181 expectAttributeOnEveryOsd('collectedStates');
182 expect(component.osds[0].collectedStates).toEqual(['in', 'up']);
183 });
184
185 it('should have "destroyed" state in "collectedStates"', () => {
186 osds[0].state.push('destroyed');
187 osds[0].up = 0;
188 component.getOsdList();
189
190 expectAttributeOnEveryOsd('collectedStates');
191 expect(component.osds[0].collectedStates).toEqual(['in', 'destroyed']);
192 });
193
194 it('should have custom attribute "stats_history.out_bytes"', () => {
195 expectAttributeOnEveryOsd('stats_history.out_bytes');
196 expect(component.osds[0].stats_history.out_bytes).toEqual([1, 2]);
197 });
198
199 it('should have custom attribute "stats_history.in_bytes"', () => {
200 expectAttributeOnEveryOsd('stats_history.in_bytes');
201 expect(component.osds[0].stats_history.in_bytes).toEqual([3, 4]);
202 });
203
204 it('should have custom attribute "stats.usage"', () => {
205 expectAttributeOnEveryOsd('stats.usage');
206 expect(component.osds[0].stats.usage).toBe(1);
207 expect(component.osds[1].stats.usage).toBe(0.5);
208 expect(component.osds[2].stats.usage).toBe(3 / 9);
209 });
210
211 it('should have custom attribute "cdIsBinary" to be true', () => {
212 expectAttributeOnEveryOsd('cdIsBinary');
213 expect(component.osds[0].cdIsBinary).toBe(true);
214 });
215 });
216
217 describe('show osd actions as defined', () => {
218 const getOsdActions = () => {
219 fixture.detectChanges();
220 return fixture.debugElement.query(By.css('#cluster-wide-actions')).componentInstance
221 .dropDownActions;
222 };
223
224 it('shows osd actions after osd-actions', () => {
225 fixture.detectChanges();
226 expect(fixture.debugElement.query(By.css('#cluster-wide-actions'))).toBe(
227 fixture.debugElement.queryAll(By.directive(TableActionsComponent))[1]
228 );
229 });
230
231 it('shows both osd actions', () => {
232 const osdActions = getOsdActions();
233 expect(osdActions).toEqual(component.clusterWideActions);
234 expect(osdActions.length).toBe(3);
235 });
236
237 it('shows only "Flags" action', () => {
238 component.permissions.configOpt.read = false;
239 const osdActions = getOsdActions();
240 expect(osdActions[0].name).toBe('Flags');
241 expect(osdActions.length).toBe(1);
242 });
243
244 it('shows only "Recovery Priority" action', () => {
245 component.permissions.osd.read = false;
246 const osdActions = getOsdActions();
247 expect(osdActions[0].name).toBe('Recovery Priority');
248 expect(osdActions[1].name).toBe('PG scrub');
249 expect(osdActions.length).toBe(2);
250 });
251
252 it('shows no osd actions', () => {
253 component.permissions.configOpt.read = false;
254 component.permissions.osd.read = false;
255 const osdActions = getOsdActions();
256 expect(osdActions).toEqual([]);
257 });
258 });
259
260 it('should test all TableActions combinations', () => {
261 const permissionHelper: PermissionHelper = new PermissionHelper(component.permissions.osd);
262 const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
263 component.tableActions
264 );
265
266 expect(tableActions).toEqual({
267 'create,update,delete': {
268 actions: [
269 'Create',
270 'Edit',
271 'Scrub',
272 'Deep Scrub',
273 'Reweight',
274 'Mark Out',
275 'Mark In',
276 'Mark Down',
277 'Mark Lost',
278 'Purge',
279 'Destroy',
280 'Delete'
281 ],
282 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
283 },
284 'create,update': {
285 actions: [
286 'Create',
287 'Edit',
288 'Scrub',
289 'Deep Scrub',
290 'Reweight',
291 'Mark Out',
292 'Mark In',
293 'Mark Down'
294 ],
295 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
296 },
297 'create,delete': {
298 actions: ['Create', 'Mark Lost', 'Purge', 'Destroy', 'Delete'],
299 primary: {
300 multiple: 'Create',
301 executing: 'Mark Lost',
302 single: 'Mark Lost',
303 no: 'Create'
304 }
305 },
306 create: {
307 actions: ['Create'],
308 primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
309 },
310 'update,delete': {
311 actions: [
312 'Edit',
313 'Scrub',
314 'Deep Scrub',
315 'Reweight',
316 'Mark Out',
317 'Mark In',
318 'Mark Down',
319 'Mark Lost',
320 'Purge',
321 'Destroy',
322 'Delete'
323 ],
324 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
325 },
326 update: {
327 actions: ['Edit', 'Scrub', 'Deep Scrub', 'Reweight', 'Mark Out', 'Mark In', 'Mark Down'],
328 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
329 },
330 delete: {
331 actions: ['Mark Lost', 'Purge', 'Destroy', 'Delete'],
332 primary: {
333 multiple: 'Mark Lost',
334 executing: 'Mark Lost',
335 single: 'Mark Lost',
336 no: 'Mark Lost'
337 }
338 },
339 'no-permissions': {
340 actions: [],
341 primary: { multiple: '', executing: '', single: '', no: '' }
342 }
343 });
344 });
345
346 describe('test table actions in submenu', () => {
347 beforeEach(() => {
348 fixture.detectChanges();
349 });
350
351 beforeEach(fakeAsync(() => {
352 // The menu needs a click to render the dropdown!
353 const dropDownToggle = fixture.debugElement.query(By.css('.dropdown-toggle'));
354 dropDownToggle.triggerEventHandler('click', null);
355 tick();
356 fixture.detectChanges();
357 }));
358
359 it('has all menu entries disabled except create', () => {
360 const tableActionElement = fixture.debugElement.query(By.directive(TableActionsComponent));
361 const toClassName = TestBed.get(TableActionsComponent).toClassName;
362 const getActionClasses = (action: CdTableAction) =>
363 tableActionElement.query(By.css(`.${toClassName(action.name)} .dropdown-item`)).classes;
364
365 component.tableActions.forEach((action) => {
366 if (action.name === 'Create') {
367 return;
368 }
369 expect(getActionClasses(action).disabled).toBe(true);
370 });
371 });
372 });
373
374 describe('tests if all modals are opened correctly', () => {
375 /**
376 * Helper function to check if a function opens a modal
377 *
378 * @param modalClass - The expected class of the modal
379 */
380 const expectOpensModal = (actionName: string, modalClass: any): void => {
381 openActionModal(actionName);
382
383 // @TODO: check why tsc is complaining when passing 'expectationFailOutput' param.
384 expect(modalServiceShowSpy.calls.any()).toBeTruthy();
385 expect(modalServiceShowSpy.calls.first().args[0]).toBe(modalClass);
386
387 modalServiceShowSpy.calls.reset();
388 };
389
390 it('opens the reweight modal', () => {
391 expectOpensModal('Reweight', OsdReweightModalComponent);
392 });
393
394 it('opens the form modal', () => {
395 expectOpensModal('Edit', FormModalComponent);
396 });
397
398 it('opens all confirmation modals', () => {
399 const modalClass = ConfirmationModalComponent;
400 expectOpensModal('Mark Out', modalClass);
401 expectOpensModal('Mark In', modalClass);
402 expectOpensModal('Mark Down', modalClass);
403 });
404
405 it('opens all critical confirmation modals', () => {
406 const modalClass = CriticalConfirmationModalComponent;
407 mockSafeToDestroy();
408 expectOpensModal('Mark Lost', modalClass);
409 expectOpensModal('Purge', modalClass);
410 expectOpensModal('Destroy', modalClass);
411 mockOrchestratorStatus();
412 mockSafeToDelete();
413 expectOpensModal('Delete', modalClass);
414 });
415 });
416
417 describe('tests if the correct methods are called on confirmation', () => {
418 const expectOsdServiceMethodCalled = (
419 actionName: string,
420 osdServiceMethodName:
421 | 'markOut'
422 | 'markIn'
423 | 'markDown'
424 | 'markLost'
425 | 'purge'
426 | 'destroy'
427 | 'delete'
428 ): void => {
429 const osdServiceSpy = spyOn(osdService, osdServiceMethodName).and.callFake(() => EMPTY);
430 openActionModal(actionName);
431 const initialState = modalServiceShowSpy.calls.first().args[1].initialState;
432 const submit = initialState.onSubmit || initialState.submitAction;
433 submit.call(component);
434
435 expect(osdServiceSpy.calls.count()).toBe(1);
436 expect(osdServiceSpy.calls.first().args[0]).toBe(1);
437
438 // Reset spies to be able to recreate them
439 osdServiceSpy.calls.reset();
440 modalServiceShowSpy.calls.reset();
441 };
442
443 it('calls the corresponding service methods in confirmation modals', () => {
444 expectOsdServiceMethodCalled('Mark Out', 'markOut');
445 expectOsdServiceMethodCalled('Mark In', 'markIn');
446 expectOsdServiceMethodCalled('Mark Down', 'markDown');
447 });
448
449 it('calls the corresponding service methods in critical confirmation modals', () => {
450 mockSafeToDestroy();
451 expectOsdServiceMethodCalled('Mark Lost', 'markLost');
452 expectOsdServiceMethodCalled('Purge', 'purge');
453 expectOsdServiceMethodCalled('Destroy', 'destroy');
454 mockOrchestratorStatus();
455 mockSafeToDelete();
456 expectOsdServiceMethodCalled('Delete', 'delete');
457 });
458 });
459 });