]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts
02cf636ac89e67cf9dadeaaf0bcc0718b6ce38a0
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / block / rbd-list / rbd-list.component.spec.ts
1 import { HttpClientTestingModule } from '@angular/common/http/testing';
2 import { ComponentFixture, TestBed } from '@angular/core/testing';
3 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
4 import { RouterTestingModule } from '@angular/router/testing';
5
6 import { NgbNavModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
7 import { ToastrModule } from 'ngx-toastr';
8 import { BehaviorSubject, of } from 'rxjs';
9
10 import { RbdService } from '~/app/shared/api/rbd.service';
11 import { TableStatusViewCache } from '~/app/shared/classes/table-status-view-cache';
12 import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
13 import { ViewCacheStatus } from '~/app/shared/enum/view-cache-status.enum';
14 import { ExecutingTask } from '~/app/shared/models/executing-task';
15 import { SummaryService } from '~/app/shared/services/summary.service';
16 import { TaskListService } from '~/app/shared/services/task-list.service';
17 import { SharedModule } from '~/app/shared/shared.module';
18 import { configureTestBed, expectItemTasks, PermissionHelper } from '~/testing/unit-test-helper';
19 import { RbdConfigurationListComponent } from '../rbd-configuration-list/rbd-configuration-list.component';
20 import { RbdDetailsComponent } from '../rbd-details/rbd-details.component';
21 import { RbdSnapshotListComponent } from '../rbd-snapshot-list/rbd-snapshot-list.component';
22 import { RbdTabsComponent } from '../rbd-tabs/rbd-tabs.component';
23 import { RbdListComponent } from './rbd-list.component';
24 import { RbdModel } from './rbd-model';
25
26 describe('RbdListComponent', () => {
27 let fixture: ComponentFixture<RbdListComponent>;
28 let component: RbdListComponent;
29 let summaryService: SummaryService;
30 let rbdService: RbdService;
31
32 const refresh = (data: any) => {
33 summaryService['summaryDataSource'].next(data);
34 };
35
36 configureTestBed({
37 imports: [
38 BrowserAnimationsModule,
39 SharedModule,
40 NgbNavModule,
41 NgbTooltipModule,
42 ToastrModule.forRoot(),
43 RouterTestingModule,
44 HttpClientTestingModule
45 ],
46 declarations: [
47 RbdListComponent,
48 RbdDetailsComponent,
49 RbdSnapshotListComponent,
50 RbdConfigurationListComponent,
51 RbdTabsComponent
52 ],
53 providers: [TaskListService]
54 });
55
56 beforeEach(() => {
57 fixture = TestBed.createComponent(RbdListComponent);
58 component = fixture.componentInstance;
59 summaryService = TestBed.inject(SummaryService);
60 rbdService = TestBed.inject(RbdService);
61
62 // this is needed because summaryService isn't being reset after each test.
63 summaryService['summaryDataSource'] = new BehaviorSubject(null);
64 summaryService['summaryData$'] = summaryService['summaryDataSource'].asObservable();
65 });
66
67 it('should create', () => {
68 expect(component).toBeTruthy();
69 });
70
71 describe('after ngOnInit', () => {
72 beforeEach(() => {
73 fixture.detectChanges();
74 spyOn(rbdService, 'list').and.callThrough();
75 });
76
77 it('should load images on init', () => {
78 refresh({});
79 expect(rbdService.list).toHaveBeenCalled();
80 });
81
82 it('should not load images on init because no data', () => {
83 refresh(undefined);
84 expect(rbdService.list).not.toHaveBeenCalled();
85 });
86
87 it('should call error function on init when summary service fails', () => {
88 spyOn(component.table, 'reset');
89 summaryService['summaryDataSource'].error(undefined);
90 expect(component.table.reset).toHaveBeenCalled();
91 expect(component.tableStatus).toEqual(
92 new TableStatusViewCache(ViewCacheStatus.ValueException)
93 );
94 });
95 });
96
97 describe('handling of provisioned columns', () => {
98 let rbdServiceListSpy: jasmine.Spy;
99
100 const images = [
101 {
102 name: 'img1',
103 pool_name: 'rbd',
104 features_name: ['layering', 'exclusive-lock'],
105 disk_usage: null,
106 total_disk_usage: null
107 },
108 {
109 name: 'img2',
110 pool_name: 'rbd',
111 features_name: ['layering', 'exclusive-lock', 'object-map', 'fast-diff'],
112 disk_usage: 1024,
113 total_disk_usage: 1024
114 }
115 ];
116
117 beforeEach(() => {
118 component.images = images;
119 refresh({ executing_tasks: [], finished_tasks: [] });
120 rbdServiceListSpy = spyOn(rbdService, 'list');
121 });
122
123 it('should display N/A for Provisioned & Total Provisioned columns if disk usage is null', () => {
124 rbdServiceListSpy.and.callFake(() => of([{ pool_name: 'rbd', status: 1, value: images }]));
125 fixture.detectChanges();
126 const spanWithoutFastDiff = fixture.debugElement.nativeElement.querySelectorAll(
127 '.datatable-body-cell-label span'
128 );
129 // check image with disk usage = null & fast-diff disabled
130 expect(spanWithoutFastDiff[6].textContent).toBe('N/A');
131
132 images[0]['features_name'] = ['layering', 'exclusive-lock', 'object-map', 'fast-diff'];
133 component.images = images;
134 refresh({ executing_tasks: [], finished_tasks: [] });
135
136 rbdServiceListSpy.and.callFake(() => of([{ pool_name: 'rbd', status: 1, value: images }]));
137 fixture.detectChanges();
138
139 const spanWithFastDiff = fixture.debugElement.nativeElement.querySelectorAll(
140 '.datatable-body-cell-label span'
141 );
142 // check image with disk usage = null & fast-diff changed to enabled
143 expect(spanWithFastDiff[6].textContent).toBe('-');
144 });
145 });
146
147 describe('handling of deletion', () => {
148 beforeEach(() => {
149 fixture.detectChanges();
150 });
151
152 it('should check if there are no snapshots', () => {
153 component.selection.add({
154 id: '-1',
155 name: 'rbd1',
156 pool_name: 'rbd'
157 });
158 expect(component.hasSnapshots()).toBeFalsy();
159 });
160
161 it('should check if there are snapshots', () => {
162 component.selection.add({
163 id: '-1',
164 name: 'rbd1',
165 pool_name: 'rbd',
166 snapshots: [{}, {}]
167 });
168 expect(component.hasSnapshots()).toBeTruthy();
169 });
170
171 it('should get delete disable description', () => {
172 component.selection.add({
173 id: '-1',
174 name: 'rbd1',
175 pool_name: 'rbd',
176 snapshots: [
177 {
178 children: [{}]
179 }
180 ]
181 });
182 expect(component.getDeleteDisableDesc(component.selection)).toBe(
183 'This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.'
184 );
185 });
186
187 it('should list all protected snapshots', () => {
188 component.selection.add({
189 id: '-1',
190 name: 'rbd1',
191 pool_name: 'rbd',
192 snapshots: [
193 {
194 name: 'snap1',
195 is_protected: false
196 },
197 {
198 name: 'snap2',
199 is_protected: true
200 }
201 ]
202 });
203
204 expect(component.listProtectedSnapshots()).toEqual(['snap2']);
205 });
206 });
207
208 describe('handling of executing tasks', () => {
209 let images: RbdModel[];
210
211 const addImage = (name: string) => {
212 const model = new RbdModel();
213 model.id = '-1';
214 model.name = name;
215 model.pool_name = 'rbd';
216 images.push(model);
217 };
218
219 const addTask = (name: string, image_name: string) => {
220 const task = new ExecutingTask();
221 task.name = name;
222 switch (task.name) {
223 case 'rbd/copy':
224 task.metadata = {
225 dest_pool_name: 'rbd',
226 dest_namespace: null,
227 dest_image_name: 'd'
228 };
229 break;
230 case 'rbd/clone':
231 task.metadata = {
232 child_pool_name: 'rbd',
233 child_namespace: null,
234 child_image_name: 'd'
235 };
236 break;
237 case 'rbd/create':
238 task.metadata = {
239 pool_name: 'rbd',
240 namespace: null,
241 image_name: image_name
242 };
243 break;
244 default:
245 task.metadata = {
246 image_spec: `rbd/${image_name}`
247 };
248 break;
249 }
250 summaryService.addRunningTask(task);
251 };
252
253 beforeEach(() => {
254 images = [];
255 addImage('a');
256 addImage('b');
257 addImage('c');
258 component.images = images;
259 refresh({ executing_tasks: [], finished_tasks: [] });
260 spyOn(rbdService, 'list').and.callFake(() =>
261 of([{ pool_name: 'rbd', status: 1, value: images }])
262 );
263 fixture.detectChanges();
264 });
265
266 it('should gets all images without tasks', () => {
267 expect(component.images.length).toBe(3);
268 expect(component.images.every((image: any) => !image.cdExecuting)).toBeTruthy();
269 });
270
271 it('should add a new image from a task', () => {
272 addTask('rbd/create', 'd');
273 expect(component.images.length).toBe(4);
274 expectItemTasks(component.images[0], undefined);
275 expectItemTasks(component.images[1], undefined);
276 expectItemTasks(component.images[2], undefined);
277 expectItemTasks(component.images[3], 'Creating');
278 });
279
280 it('should show when a image is being cloned', () => {
281 addTask('rbd/clone', 'd');
282 expect(component.images.length).toBe(4);
283 expectItemTasks(component.images[0], undefined);
284 expectItemTasks(component.images[1], undefined);
285 expectItemTasks(component.images[2], undefined);
286 expectItemTasks(component.images[3], 'Cloning');
287 });
288
289 it('should show when a image is being copied', () => {
290 addTask('rbd/copy', 'd');
291 expect(component.images.length).toBe(4);
292 expectItemTasks(component.images[0], undefined);
293 expectItemTasks(component.images[1], undefined);
294 expectItemTasks(component.images[2], undefined);
295 expectItemTasks(component.images[3], 'Copying');
296 });
297
298 it('should show when an existing image is being modified', () => {
299 addTask('rbd/edit', 'a');
300 addTask('rbd/delete', 'b');
301 addTask('rbd/flatten', 'c');
302 expect(component.images.length).toBe(3);
303 expectItemTasks(component.images[0], 'Updating');
304 expectItemTasks(component.images[1], 'Deleting');
305 expectItemTasks(component.images[2], 'Flattening');
306 });
307 });
308
309 it('should test all TableActions combinations', () => {
310 const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
311 const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
312 component.tableActions
313 );
314
315 expect(tableActions).toEqual({
316 'create,update,delete': {
317 actions: ['Create', 'Edit', 'Copy', 'Flatten', 'Delete', 'Move to Trash'],
318 primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
319 },
320 'create,update': {
321 actions: ['Create', 'Edit', 'Copy', 'Flatten'],
322 primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
323 },
324 'create,delete': {
325 actions: ['Create', 'Copy', 'Delete', 'Move to Trash'],
326 primary: { multiple: 'Create', executing: 'Copy', single: 'Copy', no: 'Create' }
327 },
328 create: {
329 actions: ['Create', 'Copy'],
330 primary: { multiple: 'Create', executing: 'Copy', single: 'Copy', no: 'Create' }
331 },
332 'update,delete': {
333 actions: ['Edit', 'Flatten', 'Delete', 'Move to Trash'],
334 primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
335 },
336 update: {
337 actions: ['Edit', 'Flatten'],
338 primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
339 },
340 delete: {
341 actions: ['Delete', 'Move to Trash'],
342 primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
343 },
344 'no-permissions': {
345 actions: [],
346 primary: { multiple: '', executing: '', single: '', no: '' }
347 }
348 });
349 });
350
351 const getActionDisable = (name: string) =>
352 component.tableActions.find((o) => o.name === name).disable;
353
354 const testActions = (selection: any, expected: { [action: string]: string | boolean }) => {
355 expect(getActionDisable('Edit')(selection)).toBe(expected.edit || false);
356 expect(getActionDisable('Delete')(selection)).toBe(expected.delete || false);
357 expect(getActionDisable('Copy')(selection)).toBe(expected.copy || false);
358 expect(getActionDisable('Flatten')(selection)).toBeTruthy();
359 expect(getActionDisable('Move to Trash')(selection)).toBe(expected.moveTrash || false);
360 };
361
362 it('should test TableActions with valid/invalid image name', () => {
363 component.selection.selected = [
364 {
365 name: 'foobar',
366 pool_name: 'rbd',
367 snapshots: []
368 }
369 ];
370 testActions(component.selection, {});
371
372 component.selection.selected = [
373 {
374 name: 'foo/bar',
375 pool_name: 'rbd',
376 snapshots: []
377 }
378 ];
379 const message = `This RBD image has an invalid name and can't be managed by ceph.`;
380 const expected = {
381 edit: message,
382 delete: message,
383 copy: message,
384 moveTrash: message
385 };
386 testActions(component.selection, expected);
387 });
388
389 it('should disable edit, copy, flatten and move action if RBD is in status `Removing`', () => {
390 component.selection.selected = [
391 {
392 name: 'foobar',
393 pool_name: 'rbd',
394 snapshots: [],
395 source: 'REMOVING'
396 }
397 ];
398
399 const message = `Action not possible for an RBD in status 'Removing'`;
400 const expected = {
401 edit: message,
402 copy: message,
403 moveTrash: message
404 };
405 testActions(component.selection, expected);
406 });
407 });