]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts
5df73d93fe1ee4142c022dc854f564f4fdcdbaa6
[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, layering: any, exclusiveLock: any, objectMap: any, fastDiff: any;
308 const defaultFeatures = [
309 // Supposed to be enabled by default
310 'deep-flatten',
311 'exclusive-lock',
312 'fast-diff',
313 'layering',
314 'object-map'
315 ];
316 const allFeatureNames = [
317 'deep-flatten',
318 'layering',
319 'exclusive-lock',
320 'object-map',
321 'fast-diff'
322 ];
323 const setFeatures = (features: Record<string, RbdImageFeature>) => {
324 component.features = features;
325 component.featuresList = component.objToArray(features);
326 component.createForm();
327 };
328 const getFeatureNativeElements = () => allFeatureNames.map((f) => queryNativeElement(`#${f}`));
329
330 it('should convert feature flags correctly in the constructor', () => {
331 setFeatures({
332 one: { desc: 'one', allowEnable: true, allowDisable: true },
333 two: { desc: 'two', allowEnable: true, allowDisable: true },
334 three: { desc: 'three', allowEnable: true, allowDisable: true }
335 });
336 expect(component.featuresList).toEqual([
337 { desc: 'one', key: 'one', allowDisable: true, allowEnable: true },
338 { desc: 'two', key: 'two', allowDisable: true, allowEnable: true },
339 { desc: 'three', key: 'three', allowDisable: true, allowEnable: true }
340 ]);
341 });
342
343 describe('test edit form flags', () => {
344 const prepare = (pool: string, image: string, enabledFeatures: string[]): void => {
345 const rbdService = TestBed.inject(RbdService);
346 spyOn(rbdService, 'get').and.returnValue(
347 of({
348 name: image,
349 pool_name: pool,
350 features_name: enabledFeatures
351 })
352 );
353 spyOn(rbdService, 'defaultFeatures').and.returnValue(of(defaultFeatures));
354 setRouterUrl('edit', pool, image);
355 fixture.detectChanges();
356 [deepFlatten, layering, exclusiveLock, objectMap, fastDiff] = getFeatureNativeElements();
357 };
358
359 it('should have the interlock feature for flags disabled, if one feature is not set', () => {
360 prepare('rbd', 'foobar', ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']);
361
362 expect(objectMap.disabled).toBe(false);
363 expect(fastDiff.disabled).toBe(false);
364
365 expect(objectMap.checked).toBe(true);
366 expect(fastDiff.checked).toBe(false);
367
368 fastDiff.click();
369 fastDiff.click();
370
371 expect(objectMap.checked).toBe(true); // Shall not be disabled by `fast-diff`!
372 });
373
374 it('should not disable object-map when fast-diff is unchecked', () => {
375 prepare('rbd', 'foobar', ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']);
376
377 fastDiff.click();
378 fastDiff.click();
379
380 expect(objectMap.checked).toBe(true); // Shall not be disabled by `fast-diff`!
381 });
382
383 it('should not enable fast-diff when object-map is checked', () => {
384 prepare('rbd', 'foobar', ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']);
385
386 objectMap.click();
387 objectMap.click();
388
389 expect(fastDiff.checked).toBe(false); // Shall not be disabled by `fast-diff`!
390 });
391 });
392
393 describe('test create form flags', () => {
394 beforeEach(() => {
395 const rbdService = TestBed.inject(RbdService);
396 spyOn(rbdService, 'defaultFeatures').and.returnValue(of(defaultFeatures));
397 setRouterUrl('create');
398 fixture.detectChanges();
399 [deepFlatten, layering, exclusiveLock, objectMap, fastDiff] = getFeatureNativeElements();
400 });
401
402 it('should initialize the checkboxes correctly', () => {
403 expect(deepFlatten.disabled).toBe(false);
404 expect(layering.disabled).toBe(false);
405 expect(exclusiveLock.disabled).toBe(false);
406 expect(objectMap.disabled).toBe(false);
407 expect(fastDiff.disabled).toBe(false);
408
409 expect(deepFlatten.checked).toBe(true);
410 expect(layering.checked).toBe(true);
411 expect(exclusiveLock.checked).toBe(true);
412 expect(objectMap.checked).toBe(true);
413 expect(fastDiff.checked).toBe(true);
414 });
415
416 it('should disable features if their requirements are not met (exclusive-lock)', () => {
417 exclusiveLock.click(); // unchecks exclusive-lock
418 expect(objectMap.disabled).toBe(true);
419 expect(fastDiff.disabled).toBe(true);
420 });
421
422 it('should disable features if their requirements are not met (object-map)', () => {
423 objectMap.click(); // unchecks object-map
424 expect(fastDiff.disabled).toBe(true);
425 });
426 });
427
428 describe('test mirroring options', () => {
429 beforeEach(() => {
430 component.ngOnInit();
431 fixture.detectChanges();
432 const mirroring = fixture.debugElement.query(By.css('#mirroring')).nativeElement;
433 mirroring.click();
434 fixture.detectChanges();
435 });
436
437 it('should verify two mirroring options are shown', () => {
438 const journal = fixture.debugElement.query(By.css('#journal')).nativeElement;
439 const snapshot = fixture.debugElement.query(By.css('#snapshot')).nativeElement;
440 expect(journal).not.toBeNull();
441 expect(snapshot).not.toBeNull();
442 });
443
444 it('should verify only snapshot is disabled for pools that are in pool mirror mode', () => {
445 component.poolMirrorMode = 'pool';
446 fixture.detectChanges();
447 const journal = fixture.debugElement.query(By.css('#journal')).nativeElement;
448 const snapshot = fixture.debugElement.query(By.css('#snapshot')).nativeElement;
449 expect(journal.disabled).toBe(false);
450 expect(snapshot.disabled).toBe(true);
451 });
452
453 it('should set and disable exclusive-lock only for the journal mode', () => {
454 component.poolMirrorMode = 'pool';
455 fixture.detectChanges();
456 const exclusiveLocks = fixture.debugElement.query(By.css('#exclusive-lock')).nativeElement;
457 expect(exclusiveLocks.checked).toBe(true);
458 expect(exclusiveLocks.disabled).toBe(true);
459 });
460 });
461 });
462 });