]> 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
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
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
f67539c2
TL
8import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
9import _ from 'lodash';
9f95a23c 10import { ToastrModule } from 'ngx-toastr';
11fdf7f2
TL
11import { EMPTY, of } from 'rxjs';
12
f67539c2
TL
13import { CephModule } from '~/app/ceph/ceph.module';
14import { PerformanceCounterModule } from '~/app/ceph/performance-counter/performance-counter.module';
15import { CoreModule } from '~/app/core/core.module';
16import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
17import { OsdService } from '~/app/shared/api/osd.service';
18import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
19import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
20import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
21import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
22import { CdTableAction } from '~/app/shared/models/cd-table-action';
23import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
24import { OrchestratorFeature } from '~/app/shared/models/orchestrator.enum';
25import { Permissions } from '~/app/shared/models/permissions';
26import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
27import { ModalService } from '~/app/shared/services/modal.service';
11fdf7f2
TL
28import {
29 configureTestBed,
f67539c2
TL
30 OrchestratorHelper,
31 PermissionHelper,
32 TableActionHelper
33} from '~/testing/unit-test-helper';
11fdf7f2
TL
34import { OsdReweightModalComponent } from '../osd-reweight-modal/osd-reweight-modal.component';
35import { OsdListComponent } from './osd-list.component';
f38dd50b 36import { ResizeObserver as ResizeObserverPolyfill } from '@juggle/resize-observer';
11fdf7f2
TL
37
38describe('OsdListComponent', () => {
39 let component: OsdListComponent;
40 let fixture: ComponentFixture<OsdListComponent>;
41 let modalServiceShowSpy: jasmine.Spy;
81eedcae 42 let osdService: OsdService;
f67539c2 43 let orchService: OrchestratorService;
11fdf7f2
TL
44
45 const fakeAuthStorageService = {
46 getPermissions: () => {
9f95a23c
TL
47 return new Permissions({
48 'config-opt': ['read', 'update', 'create', 'delete'],
49 osd: ['read', 'update', 'create', 'delete']
50 });
11fdf7f2
TL
51 }
52 };
53
9f95a23c
TL
54 const getTableAction = (name: string) =>
55 component.tableActions.find((action) => action.name === name);
11fdf7f2
TL
56
57 const setFakeSelection = () => {
58 // Default data and selection
9f95a23c
TL
59 const selection = [{ id: 1, tree: { device_class: 'ssd' } }];
60 const data = [{ id: 1, tree: { device_class: 'ssd' } }];
11fdf7f2
TL
61
62 // Table data and selection
63 component.selection = new CdTableSelection();
64 component.selection.selected = selection;
11fdf7f2
TL
65 component.osds = data;
66 component.permissions = fakeAuthStorageService.getPermissions();
67 };
68
9f95a23c 69 const openActionModal = (actionName: string) => {
11fdf7f2
TL
70 setFakeSelection();
71 getTableAction(actionName).click();
72 };
73
74 /**
75 * The following modals are called after the information about their
76 * safety to destroy/remove/mark them lost has been retrieved, hence
77 * we will have to fake its request to be able to open those modals.
78 */
79 const mockSafeToDestroy = () => {
f67539c2 80 spyOn(TestBed.inject(OsdService), 'safeToDestroy').and.callFake(() =>
9f95a23c
TL
81 of({ is_safe_to_destroy: true })
82 );
83 };
84
85 const mockSafeToDelete = () => {
f67539c2 86 spyOn(TestBed.inject(OsdService), 'safeToDelete').and.callFake(() =>
9f95a23c 87 of({ is_safe_to_delete: true })
11fdf7f2
TL
88 );
89 };
90
f67539c2
TL
91 const mockOrch = () => {
92 const features = [OrchestratorFeature.OSD_CREATE, OrchestratorFeature.OSD_DELETE];
93 OrchestratorHelper.mockStatus(true, features);
9f95a23c
TL
94 };
95
11fdf7f2
TL
96 configureTestBed({
97 imports: [
e306af50 98 BrowserAnimationsModule,
11fdf7f2
TL
99 HttpClientTestingModule,
100 PerformanceCounterModule,
9f95a23c
TL
101 ToastrModule.forRoot(),
102 CephModule,
11fdf7f2 103 ReactiveFormsModule,
f67539c2 104 NgbDropdownModule,
9f95a23c
TL
105 RouterTestingModule,
106 CoreModule,
11fdf7f2
TL
107 RouterTestingModule
108 ],
11fdf7f2
TL
109 providers: [
110 { provide: AuthStorageService, useValue: fakeAuthStorageService },
111 TableActionsComponent,
f67539c2 112 ModalService
11fdf7f2
TL
113 ]
114 });
115
116 beforeEach(() => {
117 fixture = TestBed.createComponent(OsdListComponent);
11fdf7f2 118 component = fixture.componentInstance;
f67539c2
TL
119 osdService = TestBed.inject(OsdService);
120 modalServiceShowSpy = spyOn(TestBed.inject(ModalService), 'show').and.returnValue({
121 // mock the close function, it might be called if there are async tests.
122 close: jest.fn()
123 });
124 orchService = TestBed.inject(OrchestratorService);
f38dd50b
TL
125 if (typeof window !== 'undefined') {
126 window.ResizeObserver = window.ResizeObserver || ResizeObserverPolyfill;
127 }
11fdf7f2
TL
128 });
129
130 it('should create', () => {
131 fixture.detectChanges();
132 expect(component).toBeTruthy();
133 });
134
81eedcae 135 it('should have columns that are sortable', () => {
9f95a23c
TL
136 fixture.detectChanges();
137 expect(
138 component.columns
e306af50 139 .filter((column) => !(column.prop === undefined))
9f95a23c
TL
140 .every((column) => Boolean(column.prop))
141 ).toBeTruthy();
81eedcae
TL
142 });
143
144 describe('getOsdList', () => {
9f95a23c 145 let osds: any[];
adb31ebb 146 let flagsSpy: jasmine.Spy;
9f95a23c
TL
147
148 const createOsd = (n: number) =>
149 <Record<string, any>>{
150 in: 'in',
151 up: 'up',
152 tree: {
153 device_class: 'ssd'
154 },
155 stats_history: {
801d1391
TL
156 op_out_bytes: [
157 [n, n],
158 [n * 2, n * 2]
159 ],
160 op_in_bytes: [
161 [n * 3, n * 3],
162 [n * 4, n * 4]
163 ]
9f95a23c
TL
164 },
165 stats: {
166 stat_bytes_used: n * n,
167 stat_bytes: n * n * n
168 },
169 state: []
170 };
81eedcae
TL
171
172 const expectAttributeOnEveryOsd = (attr: string) =>
173 expect(component.osds.every((osd) => Boolean(_.get(osd, attr)))).toBeTruthy();
174
175 beforeEach(() => {
176 spyOn(osdService, 'getList').and.callFake(() => of(osds));
adb31ebb 177 flagsSpy = spyOn(osdService, 'getFlags').and.callFake(() => of([]));
81eedcae
TL
178 osds = [createOsd(1), createOsd(2), createOsd(3)];
179 component.getOsdList();
180 });
181
182 it('should replace "this.osds" with new data', () => {
183 expect(component.osds.length).toBe(3);
184 expect(osdService.getList).toHaveBeenCalledTimes(1);
185
186 osds = [createOsd(4)];
187 component.getOsdList();
188 expect(component.osds.length).toBe(1);
189 expect(osdService.getList).toHaveBeenCalledTimes(2);
190 });
191
192 it('should have custom attribute "collectedStates"', () => {
193 expectAttributeOnEveryOsd('collectedStates');
194 expect(component.osds[0].collectedStates).toEqual(['in', 'up']);
195 });
196
9f95a23c
TL
197 it('should have "destroyed" state in "collectedStates"', () => {
198 osds[0].state.push('destroyed');
199 osds[0].up = 0;
200 component.getOsdList();
201
202 expectAttributeOnEveryOsd('collectedStates');
203 expect(component.osds[0].collectedStates).toEqual(['in', 'destroyed']);
204 });
205
81eedcae
TL
206 it('should have custom attribute "stats_history.out_bytes"', () => {
207 expectAttributeOnEveryOsd('stats_history.out_bytes');
208 expect(component.osds[0].stats_history.out_bytes).toEqual([1, 2]);
209 });
210
211 it('should have custom attribute "stats_history.in_bytes"', () => {
212 expectAttributeOnEveryOsd('stats_history.in_bytes');
213 expect(component.osds[0].stats_history.in_bytes).toEqual([3, 4]);
214 });
215
216 it('should have custom attribute "stats.usage"', () => {
217 expectAttributeOnEveryOsd('stats.usage');
218 expect(component.osds[0].stats.usage).toBe(1);
219 expect(component.osds[1].stats.usage).toBe(0.5);
220 expect(component.osds[2].stats.usage).toBe(3 / 9);
221 });
222
223 it('should have custom attribute "cdIsBinary" to be true', () => {
224 expectAttributeOnEveryOsd('cdIsBinary');
225 expect(component.osds[0].cdIsBinary).toBe(true);
226 });
adb31ebb
TL
227
228 it('should return valid individual flags only', () => {
229 const osd1 = createOsd(1);
230 const osd2 = createOsd(2);
231 osd1.state = ['noup', 'exists', 'up'];
232 osd2.state = ['noup', 'exists', 'up', 'noin'];
233 osds = [osd1, osd2];
234 component.getOsdList();
235
236 expect(component.osds[0].cdIndivFlags).toStrictEqual(['noup']);
237 expect(component.osds[1].cdIndivFlags).toStrictEqual(['noup', 'noin']);
238 });
239
240 it('should not fail on empty individual flags list', () => {
241 expect(component.osds[0].cdIndivFlags).toStrictEqual([]);
242 });
243
244 it('should not return disabled cluster-wide flags', () => {
245 flagsSpy.and.callFake(() => of(['noout', 'nodown', 'sortbitwise']));
246 component.getOsdList();
247 expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
248
249 flagsSpy.and.callFake(() => of(['noout', 'purged_snapdirs', 'nodown']));
250 component.getOsdList();
251 expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
252
253 flagsSpy.and.callFake(() => of(['recovery_deletes', 'noout', 'pglog_hardlimit', 'nodown']));
254 component.getOsdList();
255 expect(component.osds[0].cdClusterFlags).toStrictEqual(['noout', 'nodown']);
256 });
257
258 it('should not fail on empty cluster-wide flags list', () => {
259 flagsSpy.and.callFake(() => of([]));
260 component.getOsdList();
261 expect(component.osds[0].cdClusterFlags).toStrictEqual([]);
262 });
f67539c2
TL
263
264 it('should have custom attribute "cdExecuting"', () => {
265 osds[1].operational_status = 'unmanaged';
266 osds[2].operational_status = 'deleting';
267 component.getOsdList();
268 expect(component.osds[0].cdExecuting).toBeUndefined();
269 expect(component.osds[1].cdExecuting).toBeUndefined();
270 expect(component.osds[2].cdExecuting).toBe('deleting');
271 });
81eedcae
TL
272 });
273
9f95a23c
TL
274 describe('show osd actions as defined', () => {
275 const getOsdActions = () => {
11fdf7f2 276 fixture.detectChanges();
9f95a23c
TL
277 return fixture.debugElement.query(By.css('#cluster-wide-actions')).componentInstance
278 .dropDownActions;
11fdf7f2
TL
279 };
280
9f95a23c
TL
281 it('shows osd actions after osd-actions', () => {
282 fixture.detectChanges();
283 expect(fixture.debugElement.query(By.css('#cluster-wide-actions'))).toBe(
284 fixture.debugElement.queryAll(By.directive(TableActionsComponent))[1]
11fdf7f2 285 );
11fdf7f2
TL
286 });
287
9f95a23c
TL
288 it('shows both osd actions', () => {
289 const osdActions = getOsdActions();
290 expect(osdActions).toEqual(component.clusterWideActions);
291 expect(osdActions.length).toBe(3);
292 });
11fdf7f2 293
9f95a23c
TL
294 it('shows only "Flags" action', () => {
295 component.permissions.configOpt.read = false;
296 const osdActions = getOsdActions();
297 expect(osdActions[0].name).toBe('Flags');
298 expect(osdActions.length).toBe(1);
299 });
300
301 it('shows only "Recovery Priority" action', () => {
302 component.permissions.osd.read = false;
303 const osdActions = getOsdActions();
304 expect(osdActions[0].name).toBe('Recovery Priority');
305 expect(osdActions[1].name).toBe('PG scrub');
306 expect(osdActions.length).toBe(2);
307 });
308
309 it('shows no osd actions', () => {
310 component.permissions.configOpt.read = false;
311 component.permissions.osd.read = false;
312 const osdActions = getOsdActions();
313 expect(osdActions).toEqual([]);
314 });
315 });
316
317 it('should test all TableActions combinations', () => {
318 const permissionHelper: PermissionHelper = new PermissionHelper(component.permissions.osd);
319 const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
320 component.tableActions
321 );
322
323 expect(tableActions).toEqual({
324 'create,update,delete': {
325 actions: [
326 'Create',
327 'Edit',
adb31ebb 328 'Flags',
9f95a23c
TL
329 'Scrub',
330 'Deep Scrub',
331 'Reweight',
332 'Mark Out',
333 'Mark In',
334 'Mark Down',
335 'Mark Lost',
336 'Purge',
337 'Destroy',
338 'Delete'
339 ],
340 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
341 },
342 'create,update': {
343 actions: [
344 'Create',
345 'Edit',
adb31ebb 346 'Flags',
9f95a23c
TL
347 'Scrub',
348 'Deep Scrub',
349 'Reweight',
350 'Mark Out',
351 'Mark In',
352 'Mark Down'
353 ],
354 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' }
355 },
356 'create,delete': {
357 actions: ['Create', 'Mark Lost', 'Purge', 'Destroy', 'Delete'],
358 primary: {
359 multiple: 'Create',
360 executing: 'Mark Lost',
361 single: 'Mark Lost',
362 no: 'Create'
363 }
364 },
365 create: {
366 actions: ['Create'],
367 primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
368 },
369 'update,delete': {
370 actions: [
371 'Edit',
adb31ebb 372 'Flags',
9f95a23c
TL
373 'Scrub',
374 'Deep Scrub',
375 'Reweight',
376 'Mark Out',
377 'Mark In',
378 'Mark Down',
379 'Mark Lost',
380 'Purge',
381 'Destroy',
382 'Delete'
383 ],
384 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
385 },
386 update: {
adb31ebb
TL
387 actions: [
388 'Edit',
389 'Flags',
390 'Scrub',
391 'Deep Scrub',
392 'Reweight',
393 'Mark Out',
394 'Mark In',
395 'Mark Down'
396 ],
9f95a23c
TL
397 primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' }
398 },
399 delete: {
400 actions: ['Mark Lost', 'Purge', 'Destroy', 'Delete'],
401 primary: {
402 multiple: 'Mark Lost',
403 executing: 'Mark Lost',
404 single: 'Mark Lost',
405 no: 'Mark Lost'
406 }
407 },
408 'no-permissions': {
409 actions: [],
410 primary: { multiple: '', executing: '', single: '', no: '' }
411 }
11fdf7f2
TL
412 });
413 });
414
415 describe('test table actions in submenu', () => {
9f95a23c
TL
416 beforeEach(() => {
417 fixture.detectChanges();
418 });
419
11fdf7f2
TL
420 beforeEach(fakeAsync(() => {
421 // The menu needs a click to render the dropdown!
422 const dropDownToggle = fixture.debugElement.query(By.css('.dropdown-toggle'));
423 dropDownToggle.triggerEventHandler('click', null);
424 tick();
425 fixture.detectChanges();
426 }));
427
9f95a23c 428 it('has all menu entries disabled except create', () => {
11fdf7f2 429 const tableActionElement = fixture.debugElement.query(By.directive(TableActionsComponent));
f67539c2 430 const toClassName = TestBed.inject(TableActionsComponent).toClassName;
11fdf7f2 431 const getActionClasses = (action: CdTableAction) =>
f67539c2 432 tableActionElement.query(By.css(`[ngbDropdownItem].${toClassName(action)}`)).classes;
11fdf7f2
TL
433
434 component.tableActions.forEach((action) => {
9f95a23c
TL
435 if (action.name === 'Create') {
436 return;
437 }
11fdf7f2
TL
438 expect(getActionClasses(action).disabled).toBe(true);
439 });
440 });
441 });
442
443 describe('tests if all modals are opened correctly', () => {
444 /**
445 * Helper function to check if a function opens a modal
446 *
447 * @param modalClass - The expected class of the modal
448 */
9f95a23c 449 const expectOpensModal = (actionName: string, modalClass: any): void => {
11fdf7f2
TL
450 openActionModal(actionName);
451
452 // @TODO: check why tsc is complaining when passing 'expectationFailOutput' param.
453 expect(modalServiceShowSpy.calls.any()).toBeTruthy();
454 expect(modalServiceShowSpy.calls.first().args[0]).toBe(modalClass);
455
456 modalServiceShowSpy.calls.reset();
457 };
458
459 it('opens the reweight modal', () => {
460 expectOpensModal('Reweight', OsdReweightModalComponent);
461 });
462
9f95a23c
TL
463 it('opens the form modal', () => {
464 expectOpensModal('Edit', FormModalComponent);
465 });
466
11fdf7f2
TL
467 it('opens all confirmation modals', () => {
468 const modalClass = ConfirmationModalComponent;
469 expectOpensModal('Mark Out', modalClass);
470 expectOpensModal('Mark In', modalClass);
471 expectOpensModal('Mark Down', modalClass);
472 });
473
474 it('opens all critical confirmation modals', () => {
475 const modalClass = CriticalConfirmationModalComponent;
476 mockSafeToDestroy();
477 expectOpensModal('Mark Lost', modalClass);
478 expectOpensModal('Purge', modalClass);
479 expectOpensModal('Destroy', modalClass);
f67539c2 480 mockOrch();
9f95a23c
TL
481 mockSafeToDelete();
482 expectOpensModal('Delete', modalClass);
11fdf7f2
TL
483 });
484 });
485
486 describe('tests if the correct methods are called on confirmation', () => {
487 const expectOsdServiceMethodCalled = (
488 actionName: string,
9f95a23c
TL
489 osdServiceMethodName:
490 | 'markOut'
491 | 'markIn'
492 | 'markDown'
493 | 'markLost'
494 | 'purge'
495 | 'destroy'
496 | 'delete'
11fdf7f2 497 ): void => {
81eedcae 498 const osdServiceSpy = spyOn(osdService, osdServiceMethodName).and.callFake(() => EMPTY);
11fdf7f2 499 openActionModal(actionName);
f67539c2 500 const initialState = modalServiceShowSpy.calls.first().args[1];
11fdf7f2
TL
501 const submit = initialState.onSubmit || initialState.submitAction;
502 submit.call(component);
503
504 expect(osdServiceSpy.calls.count()).toBe(1);
505 expect(osdServiceSpy.calls.first().args[0]).toBe(1);
506
507 // Reset spies to be able to recreate them
508 osdServiceSpy.calls.reset();
509 modalServiceShowSpy.calls.reset();
510 };
511
512 it('calls the corresponding service methods in confirmation modals', () => {
513 expectOsdServiceMethodCalled('Mark Out', 'markOut');
514 expectOsdServiceMethodCalled('Mark In', 'markIn');
515 expectOsdServiceMethodCalled('Mark Down', 'markDown');
516 });
517
518 it('calls the corresponding service methods in critical confirmation modals', () => {
519 mockSafeToDestroy();
520 expectOsdServiceMethodCalled('Mark Lost', 'markLost');
521 expectOsdServiceMethodCalled('Purge', 'purge');
522 expectOsdServiceMethodCalled('Destroy', 'destroy');
f67539c2 523 mockOrch();
9f95a23c
TL
524 mockSafeToDelete();
525 expectOsdServiceMethodCalled('Delete', 'delete');
11fdf7f2
TL
526 });
527 });
f67539c2
TL
528
529 describe('table actions', () => {
530 const fakeOsds = require('./fixtures/osd_list_response.json');
531
532 beforeEach(() => {
533 component.permissions = fakeAuthStorageService.getPermissions();
534 spyOn(osdService, 'getList').and.callFake(() => of(fakeOsds));
535 spyOn(osdService, 'getFlags').and.callFake(() => of([]));
536 });
537
538 const testTableActions = async (
539 orch: boolean,
540 features: OrchestratorFeature[],
541 tests: { selectRow?: number; expectResults: any }[]
542 ) => {
543 OrchestratorHelper.mockStatus(orch, features);
544 fixture.detectChanges();
545 await fixture.whenStable();
546
547 for (const test of tests) {
548 if (test.selectRow) {
549 component.selection = new CdTableSelection();
550 component.selection.selected = [test.selectRow];
551 }
552 await TableActionHelper.verifyTableActions(
553 fixture,
554 component.tableActions,
555 test.expectResults
556 );
557 }
558 };
559
560 it('should have correct states when Orchestrator is enabled', async () => {
561 const tests = [
562 {
563 expectResults: {
564 Create: { disabled: false, disableDesc: '' },
565 Delete: { disabled: true, disableDesc: '' }
566 }
567 },
568 {
569 selectRow: fakeOsds[0],
570 expectResults: {
571 Create: { disabled: false, disableDesc: '' },
572 Delete: { disabled: false, disableDesc: '' }
573 }
574 },
575 {
576 selectRow: fakeOsds[1], // Select a row that is not managed.
577 expectResults: {
578 Create: { disabled: false, disableDesc: '' },
579 Delete: { disabled: true, disableDesc: '' }
580 }
581 },
582 {
583 selectRow: fakeOsds[2], // Select a row that is being deleted.
584 expectResults: {
585 Create: { disabled: false, disableDesc: '' },
586 Delete: { disabled: true, disableDesc: '' }
587 }
588 }
589 ];
590
591 const features = [
592 OrchestratorFeature.OSD_CREATE,
593 OrchestratorFeature.OSD_DELETE,
594 OrchestratorFeature.OSD_GET_REMOVE_STATUS
595 ];
596 await testTableActions(true, features, tests);
597 });
598
599 it('should have correct states when Orchestrator is disabled', async () => {
600 const resultNoOrchestrator = {
601 disabled: true,
602 disableDesc: orchService.disableMessages.noOrchestrator
603 };
604 const tests = [
605 {
606 expectResults: {
607 Create: resultNoOrchestrator,
608 Delete: { disabled: true, disableDesc: '' }
609 }
610 },
611 {
612 selectRow: fakeOsds[0],
613 expectResults: {
614 Create: resultNoOrchestrator,
615 Delete: resultNoOrchestrator
616 }
617 }
618 ];
619 await testTableActions(false, [], tests);
620 });
621
622 it('should have correct states when Orchestrator features are missing', async () => {
623 const resultMissingFeatures = {
624 disabled: true,
625 disableDesc: orchService.disableMessages.missingFeature
626 };
627 const tests = [
628 {
629 expectResults: {
630 Create: resultMissingFeatures,
631 Delete: { disabled: true, disableDesc: '' }
632 }
633 },
634 {
635 selectRow: fakeOsds[0],
636 expectResults: {
637 Create: resultMissingFeatures,
638 Delete: resultMissingFeatures
639 }
640 }
641 ];
642 await testTableActions(true, [], tests);
643 });
644 });
11fdf7f2 645});