]> git.proxmox.com Git - ceph.git/blame - 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
CommitLineData
11fdf7f2
TL
1import { HttpClientTestingModule } from '@angular/common/http/testing';
2import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
3import { ReactiveFormsModule } from '@angular/forms';
4import { By } from '@angular/platform-browser';
e306af50 5import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
11fdf7f2
TL
6import { RouterTestingModule } from '@angular/router/testing';
7
9f95a23c 8import * as _ from 'lodash';
11fdf7f2
TL
9import { BsModalService } from 'ngx-bootstrap/modal';
10import { TabsModule } from 'ngx-bootstrap/tabs';
9f95a23c 11import { ToastrModule } from 'ngx-toastr';
11fdf7f2
TL
12import { EMPTY, of } from 'rxjs';
13
14import {
15 configureTestBed,
16 i18nProviders,
17 PermissionHelper
18} from '../../../../../testing/unit-test-helper';
9f95a23c
TL
19import { CoreModule } from '../../../../core/core.module';
20import { OrchestratorService } from '../../../../shared/api/orchestrator.service';
11fdf7f2
TL
21import { OsdService } from '../../../../shared/api/osd.service';
22import { ConfirmationModalComponent } from '../../../../shared/components/confirmation-modal/confirmation-modal.component';
23import { CriticalConfirmationModalComponent } from '../../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
9f95a23c 24import { FormModalComponent } from '../../../../shared/components/form-modal/form-modal.component';
11fdf7f2
TL
25import { TableActionsComponent } from '../../../../shared/datatable/table-actions/table-actions.component';
26import { CdTableAction } from '../../../../shared/models/cd-table-action';
27import { CdTableSelection } from '../../../../shared/models/cd-table-selection';
28import { Permissions } from '../../../../shared/models/permissions';
29import { AuthStorageService } from '../../../../shared/services/auth-storage.service';
9f95a23c 30import { CephModule } from '../../../ceph.module';
11fdf7f2 31import { PerformanceCounterModule } from '../../../performance-counter/performance-counter.module';
11fdf7f2
TL
32import { OsdReweightModalComponent } from '../osd-reweight-modal/osd-reweight-modal.component';
33import { OsdListComponent } from './osd-list.component';
34
35describe('OsdListComponent', () => {
36 let component: OsdListComponent;
37 let fixture: ComponentFixture<OsdListComponent>;
38 let modalServiceShowSpy: jasmine.Spy;
81eedcae 39 let osdService: OsdService;
11fdf7f2
TL
40
41 const fakeAuthStorageService = {
42 getPermissions: () => {
9f95a23c
TL
43 return new Permissions({
44 'config-opt': ['read', 'update', 'create', 'delete'],
45 osd: ['read', 'update', 'create', 'delete']
46 });
11fdf7f2
TL
47 }
48 };
49
9f95a23c
TL
50 const getTableAction = (name: string) =>
51 component.tableActions.find((action) => action.name === name);
11fdf7f2
TL
52
53 const setFakeSelection = () => {
54 // Default data and selection
9f95a23c
TL
55 const selection = [{ id: 1, tree: { device_class: 'ssd' } }];
56 const data = [{ id: 1, tree: { device_class: 'ssd' } }];
11fdf7f2
TL
57
58 // Table data and selection
59 component.selection = new CdTableSelection();
60 component.selection.selected = selection;
11fdf7f2
TL
61 component.osds = data;
62 component.permissions = fakeAuthStorageService.getPermissions();
63 };
64
9f95a23c 65 const openActionModal = (actionName: string) => {
11fdf7f2
TL
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(() =>
9f95a23c
TL
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 })
11fdf7f2
TL
84 );
85 };
86
9f95a23c
TL
87 const mockOrchestratorStatus = () => {
88 spyOn(TestBed.get(OrchestratorService), 'status').and.callFake(() => of({ available: true }));
89 };
90
11fdf7f2
TL
91 configureTestBed({
92 imports: [
e306af50 93 BrowserAnimationsModule,
11fdf7f2
TL
94 HttpClientTestingModule,
95 PerformanceCounterModule,
96 TabsModule.forRoot(),
9f95a23c
TL
97 ToastrModule.forRoot(),
98 CephModule,
11fdf7f2 99 ReactiveFormsModule,
9f95a23c
TL
100 RouterTestingModule,
101 CoreModule,
11fdf7f2
TL
102 RouterTestingModule
103 ],
9f95a23c 104 declarations: [],
11fdf7f2
TL
105 providers: [
106 { provide: AuthStorageService, useValue: fakeAuthStorageService },
107 TableActionsComponent,
108 BsModalService,
109 i18nProviders
110 ]
111 });
112
113 beforeEach(() => {
114 fixture = TestBed.createComponent(OsdListComponent);
11fdf7f2 115 component = fixture.componentInstance;
81eedcae 116 osdService = TestBed.get(OsdService);
11fdf7f2
TL
117 modalServiceShowSpy = spyOn(TestBed.get(BsModalService), 'show').and.stub();
118 });
119
120 it('should create', () => {
121 fixture.detectChanges();
122 expect(component).toBeTruthy();
123 });
124
81eedcae 125 it('should have columns that are sortable', () => {
9f95a23c
TL
126 fixture.detectChanges();
127 expect(
128 component.columns
e306af50 129 .filter((column) => !(column.prop === undefined))
9f95a23c
TL
130 .every((column) => Boolean(column.prop))
131 ).toBeTruthy();
81eedcae
TL
132 });
133
134 describe('getOsdList', () => {
9f95a23c
TL
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: {
801d1391
TL
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 ]
9f95a23c
TL
153 },
154 stats: {
155 stat_bytes_used: n * n,
156 stat_bytes: n * n * n
157 },
158 state: []
159 };
81eedcae
TL
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
9f95a23c
TL
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
81eedcae
TL
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
9f95a23c
TL
217 describe('show osd actions as defined', () => {
218 const getOsdActions = () => {
11fdf7f2 219 fixture.detectChanges();
9f95a23c
TL
220 return fixture.debugElement.query(By.css('#cluster-wide-actions')).componentInstance
221 .dropDownActions;
11fdf7f2
TL
222 };
223
9f95a23c
TL
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]
11fdf7f2 228 );
11fdf7f2
TL
229 });
230
9f95a23c
TL
231 it('shows both osd actions', () => {
232 const osdActions = getOsdActions();
233 expect(osdActions).toEqual(component.clusterWideActions);
234 expect(osdActions.length).toBe(3);
235 });
11fdf7f2 236
9f95a23c
TL
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 }
11fdf7f2
TL
343 });
344 });
345
346 describe('test table actions in submenu', () => {
9f95a23c
TL
347 beforeEach(() => {
348 fixture.detectChanges();
349 });
350
11fdf7f2
TL
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
9f95a23c 359 it('has all menu entries disabled except create', () => {
11fdf7f2
TL
360 const tableActionElement = fixture.debugElement.query(By.directive(TableActionsComponent));
361 const toClassName = TestBed.get(TableActionsComponent).toClassName;
362 const getActionClasses = (action: CdTableAction) =>
9f95a23c 363 tableActionElement.query(By.css(`.${toClassName(action.name)} .dropdown-item`)).classes;
11fdf7f2
TL
364
365 component.tableActions.forEach((action) => {
9f95a23c
TL
366 if (action.name === 'Create') {
367 return;
368 }
11fdf7f2
TL
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 */
9f95a23c 380 const expectOpensModal = (actionName: string, modalClass: any): void => {
11fdf7f2
TL
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
9f95a23c
TL
394 it('opens the form modal', () => {
395 expectOpensModal('Edit', FormModalComponent);
396 });
397
11fdf7f2
TL
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);
9f95a23c
TL
411 mockOrchestratorStatus();
412 mockSafeToDelete();
413 expectOpensModal('Delete', modalClass);
11fdf7f2
TL
414 });
415 });
416
417 describe('tests if the correct methods are called on confirmation', () => {
418 const expectOsdServiceMethodCalled = (
419 actionName: string,
9f95a23c
TL
420 osdServiceMethodName:
421 | 'markOut'
422 | 'markIn'
423 | 'markDown'
424 | 'markLost'
425 | 'purge'
426 | 'destroy'
427 | 'delete'
11fdf7f2 428 ): void => {
81eedcae 429 const osdServiceSpy = spyOn(osdService, osdServiceMethodName).and.callFake(() => EMPTY);
11fdf7f2
TL
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');
9f95a23c
TL
454 mockOrchestratorStatus();
455 mockSafeToDelete();
456 expectOsdServiceMethodCalled('Delete', 'delete');
11fdf7f2
TL
457 });
458 });
459});