]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-form/rbd-form.component.spec.ts
update ceph source to reef 18.2.1
[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(createAction).toHaveBeenCalledTimes(0);
146 expect(editAction).toHaveBeenCalledTimes(1);
147 expect(cloneAction).toHaveBeenCalledTimes(0);
148 expect(copyAction).toHaveBeenCalledTimes(0);
149 expect(routerNavigate).toHaveBeenCalledTimes(0);
150 });
151
152 it('should not edit image if no image data is received', fakeAsync(() => {
153 setRouterUrl('edit', 'foo', 'bar');
154 rbdServiceGetSpy.and.returnValue(of(mock.rbd).pipe(delay(DELAY)));
155 component.ngOnInit();
156 component.submit();
157
158 expect(createAction).toHaveBeenCalledTimes(0);
159 expect(editAction).toHaveBeenCalledTimes(0);
160 expect(cloneAction).toHaveBeenCalledTimes(0);
161 expect(copyAction).toHaveBeenCalledTimes(0);
162 expect(routerNavigate).toHaveBeenCalledTimes(0);
163
164 tick(DELAY);
165 }));
166
167 describe('disable data pools', () => {
168 beforeEach(() => {
169 component.ngOnInit();
170 });
171
172 it('should be enabled with more than 1 pool', () => {
173 component['handleExternalData'](mock);
174 expect(component.allDataPools.length).toBe(3);
175 expect(component.rbdForm.get('useDataPool').disabled).toBe(false);
176
177 mock.pools.pop();
178 component['handleExternalData'](mock);
179 expect(component.allDataPools.length).toBe(2);
180 expect(component.rbdForm.get('useDataPool').disabled).toBe(false);
181 });
182
183 it('should be disabled with 1 pool', () => {
184 mock.pools = [mock.pools[0]];
185 component['handleExternalData'](mock);
186 expect(component.rbdForm.get('useDataPool').disabled).toBe(true);
187 });
188
189 // Reason for 2 tests - useDataPool is not re-enabled anywhere else
190 it('should be disabled without any pool', () => {
191 mock.pools = [];
192 component['handleExternalData'](mock);
193 expect(component.rbdForm.get('useDataPool').disabled).toBe(true);
194 });
195 });
196
197 it('should edit image after image data is received', () => {
198 setRouterUrl('edit', 'foo', 'bar');
199 component.ngOnInit();
200 component.submit();
201
202 expect(createAction).toHaveBeenCalledTimes(0);
203 expect(editAction).toHaveBeenCalledTimes(1);
204 expect(cloneAction).toHaveBeenCalledTimes(0);
205 expect(copyAction).toHaveBeenCalledTimes(0);
206 expect(routerNavigate).toHaveBeenCalledTimes(1);
207 });
208
209 it('should not clone image if no image data is received', fakeAsync(() => {
210 setRouterUrl('clone', 'foo', 'bar');
211 rbdServiceGetSpy.and.returnValue(of(mock.rbd).pipe(delay(DELAY)));
212 component.ngOnInit();
213 component.submit();
214
215 expect(createAction).toHaveBeenCalledTimes(0);
216 expect(editAction).toHaveBeenCalledTimes(0);
217 expect(cloneAction).toHaveBeenCalledTimes(0);
218 expect(copyAction).toHaveBeenCalledTimes(0);
219 expect(routerNavigate).toHaveBeenCalledTimes(0);
220
221 tick(DELAY);
222 }));
223
224 it('should clone image after image data is received', () => {
225 setRouterUrl('clone', 'foo', 'bar');
226 component.ngOnInit();
227 component.submit();
228
229 expect(createAction).toHaveBeenCalledTimes(0);
230 expect(editAction).toHaveBeenCalledTimes(0);
231 expect(cloneAction).toHaveBeenCalledTimes(1);
232 expect(copyAction).toHaveBeenCalledTimes(0);
233 expect(routerNavigate).toHaveBeenCalledTimes(1);
234 });
235
236 it('should not copy image if no image data is received', fakeAsync(() => {
237 setRouterUrl('copy', 'foo', 'bar');
238 rbdServiceGetSpy.and.returnValue(of(mock.rbd).pipe(delay(DELAY)));
239 component.ngOnInit();
240 component.submit();
241
242 expect(createAction).toHaveBeenCalledTimes(0);
243 expect(editAction).toHaveBeenCalledTimes(0);
244 expect(cloneAction).toHaveBeenCalledTimes(0);
245 expect(copyAction).toHaveBeenCalledTimes(0);
246 expect(routerNavigate).toHaveBeenCalledTimes(0);
247
248 tick(DELAY);
249 }));
250
251 it('should copy image after image data is received', () => {
252 setRouterUrl('copy', 'foo', 'bar');
253 component.ngOnInit();
254 component.submit();
255
256 expect(createAction).toHaveBeenCalledTimes(0);
257 expect(editAction).toHaveBeenCalledTimes(0);
258 expect(cloneAction).toHaveBeenCalledTimes(0);
259 expect(copyAction).toHaveBeenCalledTimes(1);
260 expect(routerNavigate).toHaveBeenCalledTimes(1);
261 });
262 });
263
264 describe('should test decodeURIComponent of params', () => {
265 let rbdService: RbdService;
266
267 beforeEach(() => {
268 rbdService = TestBed.inject(RbdService);
269 component.mode = RbdFormMode.editing;
270 fixture.detectChanges();
271 spyOn(rbdService, 'get').and.callThrough();
272 });
273
274 it('with namespace', () => {
275 activatedRoute.setParams({ image_spec: 'foo%2Fbar%2Fbaz' });
276
277 expect(rbdService.get).toHaveBeenCalledWith(new ImageSpec('foo', 'bar', 'baz'));
278 });
279
280 it('without snapName', () => {
281 activatedRoute.setParams({ image_spec: 'foo%2Fbar', snap: undefined });
282
283 expect(rbdService.get).toHaveBeenCalledWith(new ImageSpec('foo', null, 'bar'));
284 expect(component.snapName).toBeUndefined();
285 });
286
287 it('with snapName', () => {
288 activatedRoute.setParams({ image_spec: 'foo%2Fbar', snap: 'baz%2Fbaz' });
289
290 expect(rbdService.get).toHaveBeenCalledWith(new ImageSpec('foo', null, 'bar'));
291 expect(component.snapName).toBe('baz/baz');
292 });
293 });
294
295 describe('test image configuration component', () => {
296 it('is visible', () => {
297 fixture.detectChanges();
298 expect(
299 fixture.debugElement.query(By.css('cd-rbd-configuration-form')).nativeElement.parentElement
300 .hidden
301 ).toBe(true);
302 });
303 });
304
305 describe('tests for feature flags', () => {
306 let deepFlatten: any, layering: any, exclusiveLock: any, objectMap: any, fastDiff: any;
307 const defaultFeatures = [
308 // Supposed to be enabled by default
309 'deep-flatten',
310 'exclusive-lock',
311 'fast-diff',
312 'layering',
313 'object-map'
314 ];
315 const allFeatureNames = [
316 'deep-flatten',
317 'layering',
318 'exclusive-lock',
319 'object-map',
320 'fast-diff'
321 ];
322 const setFeatures = (features: Record<string, RbdImageFeature>) => {
323 component.features = features;
324 component.featuresList = component.objToArray(features);
325 component.createForm();
326 };
327 const getFeatureNativeElements = () => allFeatureNames.map((f) => queryNativeElement(`#${f}`));
328
329 it('should convert feature flags correctly in the constructor', () => {
330 setFeatures({
331 one: { desc: 'one', allowEnable: true, allowDisable: true },
332 two: { desc: 'two', allowEnable: true, allowDisable: true },
333 three: { desc: 'three', allowEnable: true, allowDisable: true }
334 });
335 expect(component.featuresList).toEqual([
336 { desc: 'one', key: 'one', allowDisable: true, allowEnable: true },
337 { desc: 'two', key: 'two', allowDisable: true, allowEnable: true },
338 { desc: 'three', key: 'three', allowDisable: true, allowEnable: true }
339 ]);
340 });
341
342 describe('test edit form flags', () => {
343 const prepare = (pool: string, image: string, enabledFeatures: string[]): void => {
344 const rbdService = TestBed.inject(RbdService);
345 spyOn(rbdService, 'get').and.returnValue(
346 of({
347 name: image,
348 pool_name: pool,
349 features_name: enabledFeatures
350 })
351 );
352 spyOn(rbdService, 'defaultFeatures').and.returnValue(of(defaultFeatures));
353 setRouterUrl('edit', pool, image);
354 fixture.detectChanges();
355 [deepFlatten, layering, exclusiveLock, objectMap, fastDiff] = getFeatureNativeElements();
356 };
357
358 it('should have the interlock feature for flags disabled, if one feature is not set', () => {
359 prepare('rbd', 'foobar', ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']);
360
361 expect(objectMap.disabled).toBe(false);
362 expect(fastDiff.disabled).toBe(false);
363
364 expect(objectMap.checked).toBe(true);
365 expect(fastDiff.checked).toBe(false);
366
367 fastDiff.click();
368 fastDiff.click();
369
370 expect(objectMap.checked).toBe(true); // Shall not be disabled by `fast-diff`!
371 });
372
373 it('should not disable object-map when fast-diff is unchecked', () => {
374 prepare('rbd', 'foobar', ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']);
375
376 fastDiff.click();
377 fastDiff.click();
378
379 expect(objectMap.checked).toBe(true); // Shall not be disabled by `fast-diff`!
380 });
381
382 it('should not enable fast-diff when object-map is checked', () => {
383 prepare('rbd', 'foobar', ['deep-flatten', 'exclusive-lock', 'layering', 'object-map']);
384
385 objectMap.click();
386 objectMap.click();
387
388 expect(fastDiff.checked).toBe(false); // Shall not be disabled by `fast-diff`!
389 });
390 });
391
392 describe('test create form flags', () => {
393 beforeEach(() => {
394 const rbdService = TestBed.inject(RbdService);
395 spyOn(rbdService, 'defaultFeatures').and.returnValue(of(defaultFeatures));
396 setRouterUrl('create');
397 fixture.detectChanges();
398 [deepFlatten, layering, exclusiveLock, objectMap, fastDiff] = getFeatureNativeElements();
399 });
400
401 it('should initialize the checkboxes correctly', () => {
402 expect(deepFlatten.disabled).toBe(false);
403 expect(layering.disabled).toBe(false);
404 expect(exclusiveLock.disabled).toBe(false);
405 expect(objectMap.disabled).toBe(false);
406 expect(fastDiff.disabled).toBe(false);
407
408 expect(deepFlatten.checked).toBe(true);
409 expect(layering.checked).toBe(true);
410 expect(exclusiveLock.checked).toBe(true);
411 expect(objectMap.checked).toBe(true);
412 expect(fastDiff.checked).toBe(true);
413 });
414
415 it('should disable features if their requirements are not met (exclusive-lock)', () => {
416 exclusiveLock.click(); // unchecks exclusive-lock
417 expect(objectMap.disabled).toBe(true);
418 expect(fastDiff.disabled).toBe(true);
419 });
420
421 it('should disable features if their requirements are not met (object-map)', () => {
422 objectMap.click(); // unchecks object-map
423 expect(fastDiff.disabled).toBe(true);
424 });
425 });
426
427 describe('test mirroring options', () => {
428 beforeEach(() => {
429 component.ngOnInit();
430 fixture.detectChanges();
431 const mirroring = fixture.debugElement.query(By.css('#mirroring')).nativeElement;
432 mirroring.click();
433 fixture.detectChanges();
434 });
435
436 it('should verify two mirroring options are shown', () => {
437 const journal = fixture.debugElement.query(By.css('#journal')).nativeElement;
438 const snapshot = fixture.debugElement.query(By.css('#snapshot')).nativeElement;
439 expect(journal).not.toBeNull();
440 expect(snapshot).not.toBeNull();
441 });
442
443 it('should verify only snapshot is disabled for pools that are in pool mirror mode', () => {
444 component.poolMirrorMode = 'pool';
445 fixture.detectChanges();
446 const journal = fixture.debugElement.query(By.css('#journal')).nativeElement;
447 const snapshot = fixture.debugElement.query(By.css('#snapshot')).nativeElement;
448 expect(journal.disabled).toBe(false);
449 expect(snapshot.disabled).toBe(true);
450 });
451
452 it('should set and disable exclusive-lock only for the journal mode', () => {
453 component.poolMirrorMode = 'pool';
454 component.mirroring = true;
455 const journal = fixture.debugElement.query(By.css('#journal')).nativeElement;
456 journal.click();
457 fixture.detectChanges();
458 const exclusiveLocks = fixture.debugElement.query(By.css('#exclusive-lock')).nativeElement;
459 expect(exclusiveLocks.checked).toBe(true);
460 expect(exclusiveLocks.disabled).toBe(true);
461 });
462
463 it('should have journaling feature for journaling mirror mode on createRequest', () => {
464 component.mirroring = true;
465 fixture.detectChanges();
466 const journal = fixture.debugElement.query(By.css('#journal')).nativeElement;
467 journal.click();
468 expect(journal.checked).toBe(true);
469 const request = component.createRequest();
470 expect(request.features).toContain('journaling');
471 });
472
473 it('should have journaling feature for journaling mirror mode on editRequest', () => {
474 component.mirroring = true;
475 fixture.detectChanges();
476 const journal = fixture.debugElement.query(By.css('#journal')).nativeElement;
477 journal.click();
478 expect(journal.checked).toBe(true);
479 const request = component.editRequest();
480 expect(request.features).toContain('journaling');
481 });
482 });
483 });
484 });