]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts
8c00c7460ab6a5fc2bccfcbca58cfebcbdc6cec9
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / block / rbd-form / rbd-form.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 { ActivatedRoute, Router } from '@angular/router';
6 import { RouterTestingModule } from '@angular/router/testing';
7
8 import { ToastrModule } from 'ngx-toastr';
9 import { NEVER, of } from 'rxjs';
10 import { delay } from 'rxjs/operators';
11
12 import { Pool } from '~/app/ceph/pool/pool';
13 import { PoolService } from '~/app/shared/api/pool.service';
14 import { RbdService } from '~/app/shared/api/rbd.service';
15 import { ImageSpec } from '~/app/shared/models/image-spec';
16 import { SharedModule } from '~/app/shared/shared.module';
17 import { ActivatedRouteStub } from '~/testing/activated-route-stub';
18 import { configureTestBed } from '~/testing/unit-test-helper';
19 import { RbdConfigurationFormComponent } from '../rbd-configuration-form/rbd-configuration-form.component';
20 import { RbdImageFeature } from './rbd-feature.interface';
21 import { RbdFormMode } from './rbd-form-mode.enum';
22 import { RbdFormResponseModel } from './rbd-form-response.model';
23 import { RbdFormComponent } from './rbd-form.component';
24
25 describe('RbdFormComponent', () => {
26 const urlPrefix = {
27 create: '/block/rbd/create',
28 edit: '/block/rbd/edit',
29 clone: '/block/rbd/clone',
30 copy: '/block/rbd/copy'
31 };
32 let component: RbdFormComponent;
33 let fixture: ComponentFixture<RbdFormComponent>;
34 let activatedRoute: ActivatedRouteStub;
35 const mock: { rbd: RbdFormResponseModel; pools: Pool[]; defaultFeatures: string[] } = {
36 rbd: {} as RbdFormResponseModel,
37 pools: [],
38 defaultFeatures: []
39 };
40
41 const setRouterUrl = (
42 action: 'create' | 'edit' | 'clone' | 'copy',
43 poolName?: string,
44 imageName?: string
45 ) => {
46 component['routerUrl'] = [urlPrefix[action], poolName, imageName].filter((x) => x).join('/');
47 };
48
49 const queryNativeElement = (cssSelector: string) =>
50 fixture.debugElement.query(By.css(cssSelector)).nativeElement;
51
52 configureTestBed({
53 imports: [
54 HttpClientTestingModule,
55 ReactiveFormsModule,
56 RouterTestingModule,
57 ToastrModule.forRoot(),
58 SharedModule
59 ],
60 declarations: [RbdFormComponent, RbdConfigurationFormComponent],
61 providers: [
62 {
63 provide: ActivatedRoute,
64 useValue: new ActivatedRouteStub({ pool: 'foo', name: 'bar', snap: undefined })
65 },
66 RbdService
67 ]
68 });
69
70 beforeEach(() => {
71 fixture = TestBed.createComponent(RbdFormComponent);
72 component = fixture.componentInstance;
73 activatedRoute = <ActivatedRouteStub>TestBed.inject(ActivatedRoute);
74
75 component.loadingReady();
76 });
77
78 it('should create', () => {
79 expect(component).toBeTruthy();
80 });
81
82 describe('create/edit/clone/copy image', () => {
83 let createAction: jasmine.Spy;
84 let editAction: jasmine.Spy;
85 let cloneAction: jasmine.Spy;
86 let copyAction: jasmine.Spy;
87 let rbdServiceGetSpy: jasmine.Spy;
88 let routerNavigate: jasmine.Spy;
89
90 const DELAY = 100;
91
92 const getPool = (
93 pool_name: string,
94 type: 'replicated' | 'erasure',
95 flags_names: string,
96 application_metadata: string[]
97 ): Pool =>
98 ({
99 pool_name,
100 flags_names,
101 application_metadata,
102 type
103 } as Pool);
104
105 beforeEach(() => {
106 createAction = spyOn(component, 'createAction').and.returnValue(of(null));
107 editAction = spyOn(component, 'editAction');
108 editAction.and.returnValue(of(null));
109 cloneAction = spyOn(component, 'cloneAction').and.returnValue(of(null));
110 copyAction = spyOn(component, 'copyAction').and.returnValue(of(null));
111 spyOn(component, 'setResponse').and.stub();
112 routerNavigate = spyOn(TestBed.inject(Router), 'navigate').and.stub();
113 mock.pools = [
114 getPool('one', 'replicated', '', []),
115 getPool('two', 'replicated', '', ['rbd']),
116 getPool('three', 'replicated', '', ['rbd']),
117 getPool('four', 'erasure', '', ['rbd']),
118 getPool('four', 'erasure', 'ec_overwrites', ['rbd'])
119 ];
120 spyOn(TestBed.inject(PoolService), 'list').and.callFake(() => of(mock.pools));
121 rbdServiceGetSpy = spyOn(TestBed.inject(RbdService), 'get');
122 mock.rbd = ({ pool_name: 'foo', pool_image: 'bar' } as any) as RbdFormResponseModel;
123 rbdServiceGetSpy.and.returnValue(of(mock.rbd));
124 component.mode = undefined;
125 });
126
127 it('should create image', () => {
128 component.ngOnInit();
129 component.submit();
130
131 expect(createAction).toHaveBeenCalledTimes(1);
132 expect(editAction).toHaveBeenCalledTimes(0);
133 expect(cloneAction).toHaveBeenCalledTimes(0);
134 expect(copyAction).toHaveBeenCalledTimes(0);
135 expect(routerNavigate).toHaveBeenCalledTimes(1);
136 });
137
138 it('should unsubscribe right after image data is received', () => {
139 setRouterUrl('edit', 'foo', 'bar');
140 rbdServiceGetSpy.and.returnValue(of(mock.rbd));
141 editAction.and.returnValue(NEVER);
142 expect(component['rbdImage'].observers.length).toEqual(0);
143 component.ngOnInit(); // Subscribes to image once during init
144 component.submit();
145 expect(component['rbdImage'].observers.length).toEqual(1);
146 expect(createAction).toHaveBeenCalledTimes(0);
147 expect(editAction).toHaveBeenCalledTimes(1);
148 expect(cloneAction).toHaveBeenCalledTimes(0);
149 expect(copyAction).toHaveBeenCalledTimes(0);
150 expect(routerNavigate).toHaveBeenCalledTimes(0);
151 });
152
153 it('should not edit image if no image data is received', fakeAsync(() => {
154 setRouterUrl('edit', 'foo', 'bar');
155 rbdServiceGetSpy.and.returnValue(of(mock.rbd).pipe(delay(DELAY)));
156 component.ngOnInit();
157 component.submit();
158
159 expect(createAction).toHaveBeenCalledTimes(0);
160 expect(editAction).toHaveBeenCalledTimes(0);
161 expect(cloneAction).toHaveBeenCalledTimes(0);
162 expect(copyAction).toHaveBeenCalledTimes(0);
163 expect(routerNavigate).toHaveBeenCalledTimes(0);
164
165 tick(DELAY);
166 }));
167
168 describe('disable data pools', () => {
169 beforeEach(() => {
170 component.ngOnInit();
171 });
172
173 it('should be enabled with more than 1 pool', () => {
174 component['handleExternalData'](mock);
175 expect(component.allDataPools.length).toBe(3);
176 expect(component.rbdForm.get('useDataPool').disabled).toBe(false);
177
178 mock.pools.pop();
179 component['handleExternalData'](mock);
180 expect(component.allDataPools.length).toBe(2);
181 expect(component.rbdForm.get('useDataPool').disabled).toBe(false);
182 });
183
184 it('should be disabled with 1 pool', () => {
185 mock.pools = [mock.pools[0]];
186 component['handleExternalData'](mock);
187 expect(component.rbdForm.get('useDataPool').disabled).toBe(true);
188 });
189
190 // Reason for 2 tests - useDataPool is not re-enabled anywhere else
191 it('should be disabled without any pool', () => {
192 mock.pools = [];
193 component['handleExternalData'](mock);
194 expect(component.rbdForm.get('useDataPool').disabled).toBe(true);
195 });
196 });
197
198 it('should edit image after image data is received', () => {
199 setRouterUrl('edit', 'foo', 'bar');
200 component.ngOnInit();
201 component.submit();
202
203 expect(createAction).toHaveBeenCalledTimes(0);
204 expect(editAction).toHaveBeenCalledTimes(1);
205 expect(cloneAction).toHaveBeenCalledTimes(0);
206 expect(copyAction).toHaveBeenCalledTimes(0);
207 expect(routerNavigate).toHaveBeenCalledTimes(1);
208 });
209
210 it('should not clone image if no image data is received', fakeAsync(() => {
211 setRouterUrl('clone', 'foo', 'bar');
212 rbdServiceGetSpy.and.returnValue(of(mock.rbd).pipe(delay(DELAY)));
213 component.ngOnInit();
214 component.submit();
215
216 expect(createAction).toHaveBeenCalledTimes(0);
217 expect(editAction).toHaveBeenCalledTimes(0);
218 expect(cloneAction).toHaveBeenCalledTimes(0);
219 expect(copyAction).toHaveBeenCalledTimes(0);
220 expect(routerNavigate).toHaveBeenCalledTimes(0);
221
222 tick(DELAY);
223 }));
224
225 it('should clone image after image data is received', () => {
226 setRouterUrl('clone', 'foo', 'bar');
227 component.ngOnInit();
228 component.submit();
229
230 expect(createAction).toHaveBeenCalledTimes(0);
231 expect(editAction).toHaveBeenCalledTimes(0);
232 expect(cloneAction).toHaveBeenCalledTimes(1);
233 expect(copyAction).toHaveBeenCalledTimes(0);
234 expect(routerNavigate).toHaveBeenCalledTimes(1);
235 });
236
237 it('should not copy image if no image data is received', fakeAsync(() => {
238 setRouterUrl('copy', 'foo', 'bar');
239 rbdServiceGetSpy.and.returnValue(of(mock.rbd).pipe(delay(DELAY)));
240 component.ngOnInit();
241 component.submit();
242
243 expect(createAction).toHaveBeenCalledTimes(0);
244 expect(editAction).toHaveBeenCalledTimes(0);
245 expect(cloneAction).toHaveBeenCalledTimes(0);
246 expect(copyAction).toHaveBeenCalledTimes(0);
247 expect(routerNavigate).toHaveBeenCalledTimes(0);
248
249 tick(DELAY);
250 }));
251
252 it('should copy image after image data is received', () => {
253 setRouterUrl('copy', 'foo', 'bar');
254 component.ngOnInit();
255 component.submit();
256
257 expect(createAction).toHaveBeenCalledTimes(0);
258 expect(editAction).toHaveBeenCalledTimes(0);
259 expect(cloneAction).toHaveBeenCalledTimes(0);
260 expect(copyAction).toHaveBeenCalledTimes(1);
261 expect(routerNavigate).toHaveBeenCalledTimes(1);
262 });
263 });
264
265 describe('should test decodeURIComponent of params', () => {
266 let rbdService: RbdService;
267
268 beforeEach(() => {
269 rbdService = TestBed.inject(RbdService);
270 component.mode = RbdFormMode.editing;
271 fixture.detectChanges();
272 spyOn(rbdService, 'get').and.callThrough();
273 });
274
275 it('with namespace', () => {
276 activatedRoute.setParams({ image_spec: 'foo%2Fbar%2Fbaz' });
277
278 expect(rbdService.get).toHaveBeenCalledWith(new ImageSpec('foo', 'bar', 'baz'));
279 });
280
281 it('without snapName', () => {
282 activatedRoute.setParams({ image_spec: 'foo%2Fbar', snap: undefined });
283
284 expect(rbdService.get).toHaveBeenCalledWith(new ImageSpec('foo', null, 'bar'));
285 expect(component.snapName).toBeUndefined();
286 });
287
288 it('with snapName', () => {
289 activatedRoute.setParams({ image_spec: 'foo%2Fbar', snap: 'baz%2Fbaz' });
290
291 expect(rbdService.get).toHaveBeenCalledWith(new ImageSpec('foo', null, 'bar'));
292 expect(component.snapName).toBe('baz/baz');
293 });
294 });
295
296 describe('test image configuration component', () => {
297 it('is visible', () => {
298 fixture.detectChanges();
299 expect(
300 fixture.debugElement.query(By.css('cd-rbd-configuration-form')).nativeElement.parentElement
301 .hidden
302 ).toBe(true);
303 });
304 });
305
306 describe('tests for feature flags', () => {
307 let deepFlatten: any,
308 layering: any,
309 exclusiveLock: any,
310 objectMap: any,
311 journaling: any,
312 fastDiff: any;
313 const defaultFeatures = [
314 // Supposed to be enabled by default
315 'deep-flatten',
316 'exclusive-lock',
317 'fast-diff',
318 'layering',
319 'object-map'
320 ];
321 const allFeatureNames = [
322 'deep-flatten',
323 'layering',
324 'exclusive-lock',
325 'object-map',
326 'journaling',
327 'fast-diff'
328 ];
329 const setFeatures = (features: Record<string, RbdImageFeature>) => {
330 component.features = features;
331 component.featuresList = component.objToArray(features);
332 component.createForm();
333 };
334 const getFeatureNativeElements = () => allFeatureNames.map((f) => queryNativeElement(`#${f}`));
335
336 it('should convert feature flags correctly in the constructor', () => {
337 setFeatures({
338 one: { desc: 'one', allowEnable: true, allowDisable: true },
339 two: { desc: 'two', allowEnable: true, allowDisable: true },
340 three: { desc: 'three', allowEnable: true, allowDisable: true }
341 });
342 expect(component.featuresList).toEqual([
343 { desc: 'one', key: 'one', allowDisable: true, allowEnable: true },
344 { desc: 'two', key: 'two', allowDisable: true, allowEnable: true },
345 { desc: 'three', key: 'three', allowDisable: true, allowEnable: true }
346 ]);
347 });
348
349 describe('test edit form flags', () => {
350 const prepare = (pool: string, image: string, enabledFeatures: string[]): void => {
351 const rbdService = TestBed.inject(RbdService);
352 spyOn(rbdService, 'get').and.returnValue(
353 of({
354 name: image,
355 pool_name: pool,
356 features_name: enabledFeatures
357 })
358 );
359 spyOn(rbdService, 'defaultFeatures').and.returnValue(of(defaultFeatures));
360 setRouterUrl('edit', pool, image);
361 fixture.detectChanges();
362 [
363 deepFlatten,
364 layering,
365 exclusiveLock,
366 objectMap,
367 journaling,
368 fastDiff
369 ] = getFeatureNativeElements();
370 };
371
372 it('should have the interlock feature for flags disabled, if one feature is not set', () => {
373 prepare('rbd', 'foobar', ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']);
374
375 expect(objectMap.disabled).toBe(false);
376 expect(fastDiff.disabled).toBe(false);
377
378 expect(objectMap.checked).toBe(true);
379 expect(fastDiff.checked).toBe(false);
380
381 fastDiff.click();
382 fastDiff.click();
383
384 expect(objectMap.checked).toBe(true); // Shall not be disabled by `fast-diff`!
385 });
386
387 it('should not disable object-map when fast-diff is unchecked', () => {
388 prepare('rbd', 'foobar', ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']);
389
390 fastDiff.click();
391 fastDiff.click();
392
393 expect(objectMap.checked).toBe(true); // Shall not be disabled by `fast-diff`!
394 });
395
396 it('should not enable fast-diff when object-map is checked', () => {
397 prepare('rbd', 'foobar', ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']);
398
399 objectMap.click();
400 objectMap.click();
401
402 expect(fastDiff.checked).toBe(false); // Shall not be disabled by `fast-diff`!
403 });
404 });
405
406 describe('test create form flags', () => {
407 beforeEach(() => {
408 const rbdService = TestBed.inject(RbdService);
409 spyOn(rbdService, 'defaultFeatures').and.returnValue(of(defaultFeatures));
410 setRouterUrl('create');
411 fixture.detectChanges();
412 [
413 deepFlatten,
414 layering,
415 exclusiveLock,
416 objectMap,
417 journaling,
418 fastDiff
419 ] = getFeatureNativeElements();
420 });
421
422 it('should initialize the checkboxes correctly', () => {
423 expect(deepFlatten.disabled).toBe(false);
424 expect(layering.disabled).toBe(false);
425 expect(exclusiveLock.disabled).toBe(false);
426 expect(objectMap.disabled).toBe(false);
427 expect(journaling.disabled).toBe(false);
428 expect(fastDiff.disabled).toBe(false);
429
430 expect(deepFlatten.checked).toBe(true);
431 expect(layering.checked).toBe(true);
432 expect(exclusiveLock.checked).toBe(true);
433 expect(objectMap.checked).toBe(true);
434 expect(journaling.checked).toBe(false);
435 expect(fastDiff.checked).toBe(true);
436 });
437
438 it('should disable features if their requirements are not met (exclusive-lock)', () => {
439 exclusiveLock.click(); // unchecks exclusive-lock
440 expect(objectMap.disabled).toBe(true);
441 expect(journaling.disabled).toBe(true);
442 expect(fastDiff.disabled).toBe(true);
443 });
444
445 it('should disable features if their requirements are not met (object-map)', () => {
446 objectMap.click(); // unchecks object-map
447 expect(fastDiff.disabled).toBe(true);
448 });
449 });
450 });
451 });