]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts
import quincy beta 17.1.0
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / pool / pool-form / pool-form.component.spec.ts
CommitLineData
11fdf7f2
TL
1import { HttpClientTestingModule } from '@angular/common/http/testing';
2import { ComponentFixture, TestBed } from '@angular/core/testing';
3import { AbstractControl } from '@angular/forms';
4import { By } from '@angular/platform-browser';
f67539c2 5import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
11fdf7f2
TL
6import { ActivatedRoute, Router, Routes } from '@angular/router';
7import { RouterTestingModule } from '@angular/router/testing';
8
f67539c2
TL
9import {
10 NgbActiveModal,
11 NgbModalModule,
12 NgbModalRef,
13 NgbNavModule
14} from '@ng-bootstrap/ng-bootstrap';
15import _ from 'lodash';
494da23a 16import { ToastrModule } from 'ngx-toastr';
11fdf7f2
TL
17import { of } from 'rxjs';
18
f67539c2
TL
19import { DashboardNotFoundError } from '~/app/core/error/error';
20import { ErrorComponent } from '~/app/core/error/error.component';
21import { CrushRuleService } from '~/app/shared/api/crush-rule.service';
22import { ErasureCodeProfileService } from '~/app/shared/api/erasure-code-profile.service';
23import { PoolService } from '~/app/shared/api/pool.service';
24import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
25import { SelectBadgesComponent } from '~/app/shared/components/select-badges/select-badges.component';
26import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
27import { ErasureCodeProfile } from '~/app/shared/models/erasure-code-profile';
28import { Permission } from '~/app/shared/models/permissions';
29import { PoolFormInfo } from '~/app/shared/models/pool-form-info';
30import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
31import { ModalService } from '~/app/shared/services/modal.service';
32import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
33import { SharedModule } from '~/app/shared/shared.module';
11fdf7f2
TL
34import {
35 configureTestBed,
36 FixtureHelper,
37 FormHelper,
f6b5b4d7 38 Mocks,
e306af50 39 modalServiceShow
f67539c2 40} from '~/testing/unit-test-helper';
11fdf7f2
TL
41import { Pool } from '../pool';
42import { PoolModule } from '../pool.module';
43import { PoolFormComponent } from './pool-form.component';
44
45describe('PoolFormComponent', () => {
f6b5b4d7 46 let OSDS = 15;
11fdf7f2
TL
47 let formHelper: FormHelper;
48 let fixtureHelper: FixtureHelper;
49 let component: PoolFormComponent;
50 let fixture: ComponentFixture<PoolFormComponent>;
51 let poolService: PoolService;
52 let form: CdFormGroup;
53 let router: Router;
54 let ecpService: ErasureCodeProfileService;
9f95a23c 55 let crushRuleService: CrushRuleService;
f67539c2
TL
56 let poolPermissions: Permission;
57 let authStorageService: AuthStorageService;
11fdf7f2 58
9f95a23c 59 const setPgNum = (pgs: number): AbstractControl => {
11fdf7f2
TL
60 const control = formHelper.setValue('pgNum', pgs);
61 fixture.debugElement.query(By.css('#pgNum')).nativeElement.dispatchEvent(new Event('blur'));
62 return control;
63 };
64
9f95a23c 65 const testPgUpdate = (pgs: number, jump: number, returnValue: number) => {
eafe8130
TL
66 if (pgs) {
67 setPgNum(pgs);
68 }
69 if (jump) {
70 setPgNum(form.getValue('pgNum') + jump);
71 }
72 expect(form.getValue('pgNum')).toBe(returnValue);
73 };
74
11fdf7f2
TL
75 const expectValidSubmit = (
76 pool: any,
9f95a23c
TL
77 taskName = 'pool/create',
78 poolServiceMethod: 'create' | 'update' = 'create'
11fdf7f2
TL
79 ) => {
80 spyOn(poolService, poolServiceMethod).and.stub();
f67539c2 81 const taskWrapper = TestBed.inject(TaskWrapperService);
11fdf7f2
TL
82 spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
83 component.submit();
84 expect(poolService[poolServiceMethod]).toHaveBeenCalledWith(pool);
85 expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith({
86 task: {
87 name: taskName,
88 metadata: {
89 pool_name: pool.pool
90 }
91 },
92 call: undefined // because of stub
93 });
94 };
95
9f95a23c
TL
96 let infoReturn: PoolFormInfo;
97 const setInfo = () => {
98 const ecp1 = new ErasureCodeProfile();
99 ecp1.name = 'ecp1';
100 infoReturn = {
101 pool_names: ['someExistingPoolName'],
11fdf7f2
TL
102 osd_count: OSDS,
103 is_all_bluestore: true,
104 bluestore_compression_algorithm: 'snappy',
105 compression_algorithms: ['snappy'],
106 compression_modes: ['none', 'passive'],
9f95a23c 107 crush_rules_replicated: [
20effc67
TL
108 Mocks.getCrushRule({ id: 0, name: 'rep1', type: 'replicated' }),
109 Mocks.getCrushRule({ id: 1, name: 'rep2', type: 'replicated' }),
110 Mocks.getCrushRule({ id: 2, name: 'used_rule', type: 'replicated' })
9f95a23c 111 ],
20effc67 112 crush_rules_erasure: [Mocks.getCrushRule({ id: 3, name: 'ecp1', type: 'erasure' })],
9f95a23c
TL
113 erasure_code_profiles: [ecp1],
114 pg_autoscale_default_mode: 'off',
115 pg_autoscale_modes: ['off', 'warn', 'on'],
116 used_rules: {
117 used_rule: ['some.pool.uses.it']
e306af50
TL
118 },
119 used_profiles: {
120 ecp1: ['some.other.pool.uses.it']
f6b5b4d7
TL
121 },
122 nodes: Mocks.generateSimpleCrushMap(3, 5)
11fdf7f2 123 };
9f95a23c
TL
124 };
125
126 const setUpPoolComponent = () => {
127 fixture = TestBed.createComponent(PoolFormComponent);
128 component = fixture.componentInstance;
129 fixture.detectChanges();
130
131 fixtureHelper = new FixtureHelper(fixture);
11fdf7f2
TL
132 form = component.form;
133 formHelper = new FormHelper(form);
134 };
135
f67539c2
TL
136 const routes: Routes = [{ path: '404', component: ErrorComponent }];
137
138 configureTestBed(
139 {
140 declarations: [ErrorComponent],
141 imports: [
142 BrowserAnimationsModule,
143 HttpClientTestingModule,
144 RouterTestingModule.withRoutes(routes),
145 ToastrModule.forRoot(),
146 NgbNavModule,
147 PoolModule,
148 SharedModule,
149 NgbModalModule
150 ],
151 providers: [
152 ErasureCodeProfileService,
153 NgbActiveModal,
154 SelectBadgesComponent,
155 { provide: ActivatedRoute, useValue: { params: of({ name: 'somePoolName' }) } }
156 ]
157 },
158 [CriticalConfirmationModalComponent]
159 );
11fdf7f2 160
9f95a23c
TL
161 let navigationSpy: jasmine.Spy;
162
11fdf7f2 163 beforeEach(() => {
f67539c2 164 poolService = TestBed.inject(PoolService);
9f95a23c
TL
165 setInfo();
166 spyOn(poolService, 'getInfo').and.callFake(() => of(infoReturn));
167
f67539c2
TL
168 ecpService = TestBed.inject(ErasureCodeProfileService);
169 crushRuleService = TestBed.inject(CrushRuleService);
9f95a23c 170
f67539c2 171 router = TestBed.inject(Router);
9f95a23c 172 navigationSpy = spyOn(router, 'navigate').and.stub();
f67539c2
TL
173 authStorageService = TestBed.inject(AuthStorageService);
174 spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
175 pool: poolPermissions
176 }));
177 poolPermissions = new Permission(['update', 'delete', 'read', 'create']);
9f95a23c 178 setUpPoolComponent();
f67539c2
TL
179
180 component.loadingReady();
11fdf7f2
TL
181 });
182
183 it('should create', () => {
184 expect(component).toBeTruthy();
185 });
186
f67539c2
TL
187 describe('throws error for not allowed users', () => {
188 const expectError = (redirected: boolean) => {
9f95a23c 189 navigationSpy.calls.reset();
f67539c2
TL
190 if (redirected) {
191 expect(() => component.authenticate()).toThrowError(DashboardNotFoundError);
192 } else {
193 expect(() => component.authenticate()).not.toThrowError();
194 }
11fdf7f2
TL
195 };
196
197 beforeEach(() => {
f67539c2 198 poolPermissions = new Permission(['delete']);
11fdf7f2
TL
199 });
200
f67539c2
TL
201 it('navigates to Dashboard if not allowed', () => {
202 expect(() => component.authenticate()).toThrowError(DashboardNotFoundError);
11fdf7f2
TL
203 });
204
f67539c2
TL
205 it('throws error if user is not allowed', () => {
206 expectError(true);
11fdf7f2 207 poolPermissions.read = true;
f67539c2 208 expectError(true);
11fdf7f2 209 poolPermissions.delete = true;
f67539c2 210 expectError(true);
11fdf7f2 211 poolPermissions.update = true;
f67539c2 212 expectError(true);
11fdf7f2
TL
213 component.editing = true;
214 poolPermissions.update = false;
215 poolPermissions.create = true;
f67539c2 216 expectError(true);
11fdf7f2
TL
217 });
218
f67539c2 219 it('does not throw error for users with right permissions', () => {
11fdf7f2
TL
220 poolPermissions.read = true;
221 poolPermissions.create = true;
f67539c2 222 expectError(false);
11fdf7f2
TL
223 component.editing = true;
224 poolPermissions.update = true;
f67539c2 225 expectError(false);
11fdf7f2 226 poolPermissions.create = false;
f67539c2 227 expectError(false);
11fdf7f2
TL
228 });
229 });
230
231 describe('pool form validation', () => {
232 beforeEach(() => {
233 fixture.detectChanges();
234 });
235
236 it('is invalid at the beginning all sub forms are valid', () => {
237 expect(form.valid).toBeFalsy();
238 ['name', 'poolType', 'pgNum'].forEach((name) => formHelper.expectError(name, 'required'));
9f95a23c 239 ['size', 'crushRule', 'erasureProfile', 'ecOverwrites'].forEach((name) =>
11fdf7f2
TL
240 formHelper.expectValid(name)
241 );
242 expect(component.form.get('compression').valid).toBeTruthy();
243 });
244
245 it('validates name', () => {
246 expect(component.editing).toBeFalsy();
247 formHelper.expectError('name', 'required');
248 formHelper.expectValidChange('name', 'some-name');
249 formHelper.expectValidChange('name', 'name/with/slash');
11fdf7f2
TL
250 formHelper.expectErrorChange('name', 'someExistingPoolName', 'uniqueName');
251 formHelper.expectErrorChange('name', 'wrong format with spaces', 'pattern');
252 });
253
254 it('should validate with dots in pool name', () => {
255 formHelper.expectValidChange('name', 'pool.default.bar', true);
256 });
257
258 it('validates poolType', () => {
259 formHelper.expectError('poolType', 'required');
260 formHelper.expectValidChange('poolType', 'erasure');
261 formHelper.expectValidChange('poolType', 'replicated');
262 });
263
264 it('validates that pgNum is required creation mode', () => {
265 formHelper.expectError(form.get('pgNum'), 'required');
266 });
267
268 it('validates pgNum in edit mode', () => {
269 component.data.pool = new Pool('test');
270 component.data.pool.pg_num = 16;
271 component.editing = true;
272 component.ngOnInit(); // Switches form into edit mode
273 formHelper.setValue('poolType', 'erasure');
274 fixture.detectChanges();
9f95a23c 275 formHelper.expectValid(setPgNum(8));
11fdf7f2
TL
276 });
277
278 it('is valid if pgNum, poolType and name are valid', () => {
279 formHelper.setValue('name', 'some-name');
280 formHelper.setValue('poolType', 'erasure');
281 fixture.detectChanges();
282 setPgNum(1);
283 expect(form.valid).toBeTruthy();
284 });
285
9f95a23c
TL
286 it('validates crushRule with multiple crush rules', () => {
287 formHelper.expectValidChange('poolType', 'replicated');
288 form.get('crushRule').updateValueAndValidity();
289 formHelper.expectError('crushRule', 'required'); // As multiple rules exist
11fdf7f2
TL
290 });
291
9f95a23c
TL
292 it('validates crushRule with no crush rules', () => {
293 infoReturn.crush_rules_replicated = [];
294 setUpPoolComponent();
295 formHelper.expectValidChange('poolType', 'replicated');
296 formHelper.expectValid('crushRule');
297 });
298
11fdf7f2 299 it('validates size', () => {
f6b5b4d7 300 component.info.nodes = Mocks.getCrushMap();
11fdf7f2
TL
301 formHelper.setValue('poolType', 'replicated');
302 formHelper.expectValid('size');
11fdf7f2
TL
303 });
304
f91f0fd5
TL
305 it('validates if warning is displayed when size is 1', () => {
306 formHelper.setValue('poolType', 'replicated');
307 formHelper.expectValid('size');
308
309 formHelper.setValue('size', 1, true);
310 expect(fixtureHelper.getElementByCss('#size ~ .text-warning-dark')).toBeTruthy();
311
312 formHelper.setValue('size', 2, true);
313 expect(fixtureHelper.getElementByCss('#size ~ .text-warning-dark')).toBeFalsy();
314 });
315
11fdf7f2
TL
316 it('validates compression mode default value', () => {
317 expect(form.getValue('mode')).toBe('none');
318 });
319
9f95a23c
TL
320 it('validate quotas', () => {
321 formHelper.expectValid('max_bytes');
322 formHelper.expectValid('max_objects');
323 formHelper.expectValidChange('max_bytes', '10 Gib');
324 formHelper.expectValidChange('max_bytes', '');
325 formHelper.expectValidChange('max_objects', '');
326 formHelper.expectErrorChange('max_objects', -1, 'min');
327 });
328
11fdf7f2
TL
329 describe('compression form', () => {
330 beforeEach(() => {
331 formHelper.setValue('poolType', 'replicated');
332 formHelper.setValue('mode', 'passive');
333 });
334
335 it('is valid', () => {
336 expect(component.form.get('compression').valid).toBeTruthy();
337 });
338
339 it('validates minBlobSize to be only valid between 0 and maxBlobSize', () => {
340 formHelper.expectErrorChange('minBlobSize', -1, 'min');
341 formHelper.expectValidChange('minBlobSize', 0);
342 formHelper.setValue('maxBlobSize', '2 KiB');
343 formHelper.expectErrorChange('minBlobSize', '3 KiB', 'maximum');
344 formHelper.expectValidChange('minBlobSize', '1.9 KiB');
345 });
346
347 it('validates minBlobSize converts numbers', () => {
348 const control = formHelper.setValue('minBlobSize', '1');
349 fixture.detectChanges();
350 formHelper.expectValid(control);
351 expect(control.value).toBe('1 KiB');
352 });
353
354 it('validates maxBlobSize to be only valid bigger than minBlobSize', () => {
355 formHelper.expectErrorChange('maxBlobSize', -1, 'min');
356 formHelper.setValue('minBlobSize', '1 KiB');
357 formHelper.expectErrorChange('maxBlobSize', '0.5 KiB', 'minimum');
358 formHelper.expectValidChange('maxBlobSize', '1.5 KiB');
359 });
360
361 it('s valid to only use one blob size', () => {
362 formHelper.expectValid(formHelper.setValue('minBlobSize', '1 KiB'));
363 formHelper.expectValid(formHelper.setValue('maxBlobSize', ''));
364 formHelper.expectValid(formHelper.setValue('minBlobSize', ''));
365 formHelper.expectValid(formHelper.setValue('maxBlobSize', '1 KiB'));
366 });
367
368 it('dismisses any size error if one of the blob sizes is changed into a valid state', () => {
369 const min = formHelper.setValue('minBlobSize', '10 KiB');
370 const max = formHelper.setValue('maxBlobSize', '1 KiB');
371 fixture.detectChanges();
372 max.setValue('');
373 formHelper.expectValid(min);
374 formHelper.expectValid(max);
375 max.setValue('1 KiB');
376 fixture.detectChanges();
377 min.setValue('0.5 KiB');
378 formHelper.expectValid(min);
379 formHelper.expectValid(max);
380 });
381
382 it('validates maxBlobSize converts numbers', () => {
383 const control = formHelper.setValue('maxBlobSize', '2');
384 fixture.detectChanges();
385 expect(control.value).toBe('2 KiB');
386 });
387
388 it('validates that odd size validator works as expected', () => {
9f95a23c 389 const odd = (min: string, max: string) => component['oddBlobSize'](min, max);
11fdf7f2
TL
390 expect(odd('10', '8')).toBe(true);
391 expect(odd('8', '-')).toBe(false);
392 expect(odd('8', '10')).toBe(false);
393 expect(odd(null, '8')).toBe(false);
394 expect(odd('10', '')).toBe(false);
395 expect(odd('10', null)).toBe(false);
396 expect(odd(null, null)).toBe(false);
397 });
398
399 it('validates ratio to be only valid between 0 and 1', () => {
400 formHelper.expectValid('ratio');
401 formHelper.expectErrorChange('ratio', -0.1, 'min');
402 formHelper.expectValidChange('ratio', 0);
403 formHelper.expectValidChange('ratio', 1);
404 formHelper.expectErrorChange('ratio', 1.1, 'max');
405 });
406 });
407
408 it('validates application metadata name', () => {
409 formHelper.setValue('poolType', 'replicated');
410 fixture.detectChanges();
411 const selectBadges = fixture.debugElement.query(By.directive(SelectBadgesComponent))
412 .componentInstance;
413 const control = selectBadges.cdSelect.filter;
414 formHelper.expectValid(control);
415 control.setValue('?');
416 formHelper.expectError(control, 'pattern');
417 control.setValue('Ab3_');
418 formHelper.expectValid(control);
419 control.setValue('a'.repeat(129));
420 formHelper.expectError(control, 'maxlength');
421 });
422 });
423
424 describe('pool type changes', () => {
425 beforeEach(() => {
426 component.ngOnInit();
11fdf7f2
TL
427 });
428
429 it('should have a default replicated size of 3', () => {
430 formHelper.setValue('poolType', 'replicated');
431 expect(form.getValue('size')).toBe(3);
432 });
433
434 describe('replicatedRuleChange', () => {
435 beforeEach(() => {
436 formHelper.setValue('poolType', 'replicated');
437 formHelper.setValue('size', 99);
438 });
439
440 it('should not set size if a replicated pool is not set', () => {
441 formHelper.setValue('poolType', 'erasure');
442 expect(form.getValue('size')).toBe(99);
443 formHelper.setValue('crushRule', component.info.crush_rules_replicated[1]);
444 expect(form.getValue('size')).toBe(99);
445 });
446
447 it('should set size to maximum if size exceeds maximum', () => {
448 formHelper.setValue('crushRule', component.info.crush_rules_replicated[0]);
20effc67 449 expect(form.getValue('size')).toBe(10);
11fdf7f2
TL
450 });
451
452 it('should set size to minimum if size is lower than minimum', () => {
453 formHelper.setValue('size', -1);
454 formHelper.setValue('crushRule', component.info.crush_rules_replicated[0]);
20effc67 455 expect(form.getValue('size')).toBe(1);
11fdf7f2
TL
456 });
457 });
458
459 describe('rulesChange', () => {
460 it('has no effect if info is not there', () => {
461 delete component.info;
462 formHelper.setValue('poolType', 'replicated');
463 expect(component.current.rules).toEqual([]);
464 });
465
466 it('has no effect if pool type is not set', () => {
9f95a23c 467 component['poolTypeChange']('');
11fdf7f2
TL
468 expect(component.current.rules).toEqual([]);
469 });
470
471 it('shows all replicated rules when pool type is "replicated"', () => {
472 formHelper.setValue('poolType', 'replicated');
473 expect(component.current.rules).toEqual(component.info.crush_rules_replicated);
9f95a23c 474 expect(component.current.rules.length).toBe(3);
11fdf7f2
TL
475 });
476
477 it('shows all erasure code rules when pool type is "erasure"', () => {
478 formHelper.setValue('poolType', 'erasure');
479 expect(component.current.rules).toEqual(component.info.crush_rules_erasure);
480 expect(component.current.rules.length).toBe(1);
481 });
482
483 it('disables rule field if only one rule exists which is used in the disabled field', () => {
9f95a23c 484 infoReturn.crush_rules_replicated = [
20effc67 485 Mocks.getCrushRule({ id: 0, name: 'rep1', type: 'replicated' })
9f95a23c
TL
486 ];
487 setUpPoolComponent();
488 formHelper.setValue('poolType', 'replicated');
11fdf7f2 489 const control = form.get('crushRule');
9f95a23c 490 expect(control.value).toEqual(component.info.crush_rules_replicated[0]);
11fdf7f2
TL
491 expect(control.disabled).toBe(true);
492 });
493
494 it('does not select the first rule if more than one exist', () => {
495 formHelper.setValue('poolType', 'replicated');
496 const control = form.get('crushRule');
497 expect(control.value).toEqual(null);
498 expect(control.disabled).toBe(false);
499 });
500
9f95a23c 501 it('changing between both pool types will not forget the crush rule selection', () => {
11fdf7f2
TL
502 formHelper.setValue('poolType', 'replicated');
503 const control = form.get('crushRule');
9f95a23c
TL
504 const currentRule = component.info.crush_rules_replicated[0];
505 control.setValue(currentRule);
11fdf7f2 506 formHelper.setValue('poolType', 'erasure');
9f95a23c
TL
507 formHelper.setValue('poolType', 'replicated');
508 expect(control.value).toEqual(currentRule);
11fdf7f2
TL
509 });
510 });
511 });
512
513 describe('getMaxSize and getMinSize', () => {
9f95a23c 514 it('returns 0 if osd count is 0', () => {
11fdf7f2 515 component.info.osd_count = 0;
9f95a23c
TL
516 expect(component.getMinSize()).toBe(0);
517 expect(component.getMaxSize()).toBe(0);
11fdf7f2
TL
518 });
519
9f95a23c 520 it('returns 0 if info is not there', () => {
11fdf7f2 521 delete component.info;
9f95a23c
TL
522 expect(component.getMinSize()).toBe(0);
523 expect(component.getMaxSize()).toBe(0);
11fdf7f2
TL
524 });
525
f6b5b4d7 526 it('returns 1 as minimum and 3 as maximum if no crush rule is available', () => {
11fdf7f2 527 expect(component.getMinSize()).toBe(1);
f6b5b4d7 528 expect(component.getMaxSize()).toBe(3);
11fdf7f2
TL
529 });
530
11fdf7f2 531 it('should return the osd count as minimum if its lower the the rule minimum', () => {
20effc67
TL
532 component.info.osd_count = 0;
533 formHelper.setValue('crushRule', component.info.crush_rules_replicated[0]);
11fdf7f2
TL
534 const control = form.get('crushRule');
535 expect(control.invalid).toBe(true);
536 formHelper.expectError(control, 'tooFewOsds');
537 });
f6b5b4d7
TL
538
539 it('should get the right maximum if the device type is defined', () => {
20effc67
TL
540 formHelper.setValue('crushRule', Mocks.getCrushRule({ itemName: 'default~ssd' }));
541 expect(form.getValue('crushRule').usable_size).toBe(10);
f6b5b4d7 542 });
11fdf7f2
TL
543 });
544
545 describe('application metadata', () => {
546 let selectBadges: SelectBadgesComponent;
547
548 const testAddApp = (app?: string, result?: string[]) => {
549 selectBadges.cdSelect.filter.setValue(app);
550 selectBadges.cdSelect.updateFilter();
551 selectBadges.cdSelect.selectOption();
552 expect(component.data.applications.selected).toEqual(result);
553 };
554
555 const testRemoveApp = (app: string, result: string[]) => {
556 selectBadges.cdSelect.removeItem(app);
557 expect(component.data.applications.selected).toEqual(result);
558 };
559
560 const setCurrentApps = (apps: string[]) => {
561 component.data.applications.selected = apps;
562 fixture.detectChanges();
563 selectBadges.cdSelect.ngOnInit();
564 return apps;
565 };
566
567 beforeEach(() => {
568 formHelper.setValue('poolType', 'replicated');
569 fixture.detectChanges();
570 selectBadges = fixture.debugElement.query(By.directive(SelectBadgesComponent))
571 .componentInstance;
572 });
573
574 it('adds all predefined and a custom applications to the application metadata array', () => {
575 testAddApp('g', ['rgw']);
576 testAddApp('b', ['rbd', 'rgw']);
577 testAddApp('c', ['cephfs', 'rbd', 'rgw']);
9f95a23c 578 testAddApp('ownApp', ['cephfs', 'ownApp', 'rbd', 'rgw']);
11fdf7f2
TL
579 });
580
581 it('only allows 4 apps to be added to the array', () => {
582 const apps = setCurrentApps(['d', 'c', 'b', 'a']);
583 testAddApp('e', apps);
584 });
585
586 it('can remove apps', () => {
587 setCurrentApps(['a', 'b', 'c', 'd']);
588 testRemoveApp('c', ['a', 'b', 'd']);
589 testRemoveApp('a', ['b', 'd']);
590 testRemoveApp('d', ['b']);
591 testRemoveApp('b', []);
592 });
593
594 it('does not remove any app that is not in the array', () => {
595 const apps = ['a', 'b', 'c', 'd'];
596 setCurrentApps(apps);
597 testRemoveApp('e', apps);
598 testRemoveApp('0', apps);
599 });
600 });
601
602 describe('pg number changes', () => {
11fdf7f2
TL
603 beforeEach(() => {
604 formHelper.setValue('crushRule', {
605 min_size: 1,
606 max_size: 20
607 });
608 formHelper.setValue('poolType', 'erasure');
609 fixture.detectChanges();
610 setPgNum(256);
611 });
612
613 it('updates by value', () => {
614 testPgUpdate(10, undefined, 8);
615 testPgUpdate(22, undefined, 16);
616 testPgUpdate(26, undefined, 32);
617 testPgUpdate(200, undefined, 256);
618 testPgUpdate(300, undefined, 256);
619 testPgUpdate(350, undefined, 256);
620 });
621
622 it('updates by jump -> a magnitude of the power of 2', () => {
623 testPgUpdate(undefined, 1, 512);
624 testPgUpdate(undefined, -1, 256);
625 });
626
627 it('returns 1 as minimum for false numbers', () => {
628 testPgUpdate(-26, undefined, 1);
629 testPgUpdate(0, undefined, 1);
630 testPgUpdate(0, -1, 1);
631 testPgUpdate(undefined, -20, 1);
632 });
633
634 it('changes the value and than jumps', () => {
635 testPgUpdate(230, 1, 512);
636 testPgUpdate(3500, -1, 2048);
637 });
638
639 describe('pg power jump', () => {
640 it('should jump correctly at the beginning', () => {
641 testPgUpdate(1, -1, 1);
642 testPgUpdate(1, 1, 2);
643 testPgUpdate(2, -1, 1);
644 testPgUpdate(2, 1, 4);
645 testPgUpdate(4, -1, 2);
646 testPgUpdate(4, 1, 8);
647 testPgUpdate(4, 1, 8);
648 });
649
650 it('increments pg power if difference to the current number is 1', () => {
651 testPgUpdate(undefined, 1, 512);
652 testPgUpdate(undefined, 1, 1024);
653 testPgUpdate(undefined, 1, 2048);
654 testPgUpdate(undefined, 1, 4096);
655 });
656
657 it('decrements pg power if difference to the current number is -1', () => {
658 testPgUpdate(undefined, -1, 128);
659 testPgUpdate(undefined, -1, 64);
660 testPgUpdate(undefined, -1, 32);
661 testPgUpdate(undefined, -1, 16);
662 testPgUpdate(undefined, -1, 8);
663 });
664 });
665
666 describe('pgCalc', () => {
667 const PGS = 1;
f6b5b4d7 668 OSDS = 8;
11fdf7f2
TL
669
670 const getValidCase = () => ({
671 type: 'replicated',
672 osds: OSDS,
673 size: 4,
674 ecp: {
675 k: 2,
676 m: 2
677 },
678 expected: 256
679 });
680
9f95a23c 681 const testPgCalc = ({ type, osds, size, ecp, expected }: Record<string, any>) => {
11fdf7f2
TL
682 component.info.osd_count = osds;
683 formHelper.setValue('poolType', type);
684 if (type === 'replicated') {
685 formHelper.setValue('size', size);
686 } else {
687 formHelper.setValue('erasureProfile', ecp);
688 }
689 expect(form.getValue('pgNum')).toBe(expected);
690 expect(component.externalPgChange).toBe(PGS !== expected);
691 };
692
693 beforeEach(() => {
694 setPgNum(PGS);
695 });
696
697 it('does not change anything if type is not valid', () => {
698 const test = getValidCase();
699 test.type = '';
700 test.expected = PGS;
701 testPgCalc(test);
702 });
703
704 it('does not change anything if ecp is not valid', () => {
705 const test = getValidCase();
706 test.expected = PGS;
707 test.type = 'erasure';
708 test.ecp = null;
709 testPgCalc(test);
710 });
711
712 it('calculates some replicated values', () => {
713 const test = getValidCase();
714 testPgCalc(test);
715 test.osds = 16;
716 test.expected = 512;
717 testPgCalc(test);
718 test.osds = 8;
719 test.size = 8;
720 test.expected = 128;
721 testPgCalc(test);
722 });
723
724 it('calculates erasure code values even if selection is disabled', () => {
725 component['initEcp']([{ k: 2, m: 2, name: 'bla', plugin: '', technique: '' }]);
726 const test = getValidCase();
727 test.type = 'erasure';
728 testPgCalc(test);
729 expect(form.get('erasureProfile').disabled).toBeTruthy();
730 });
731
732 it('calculates some erasure code values', () => {
733 const test = getValidCase();
734 test.type = 'erasure';
735 testPgCalc(test);
736 test.osds = 16;
737 test.ecp.m = 5;
738 test.expected = 256;
739 testPgCalc(test);
740 test.ecp.k = 5;
741 test.expected = 128;
742 testPgCalc(test);
743 });
744
745 it('should not change a manual set pg number', () => {
746 form.get('pgNum').markAsDirty();
747 const test = getValidCase();
748 test.expected = PGS;
749 testPgCalc(test);
750 });
751 });
752 });
753
754 describe('crushRule', () => {
9f95a23c
TL
755 const selectRuleByIndex = (n: number) => {
756 formHelper.setValue('crushRule', component.info.crush_rules_replicated[n]);
757 };
758
11fdf7f2 759 beforeEach(() => {
11fdf7f2 760 formHelper.setValue('poolType', 'replicated');
9f95a23c 761 selectRuleByIndex(0);
11fdf7f2
TL
762 fixture.detectChanges();
763 });
764
e306af50
TL
765 it('should select the newly created rule', () => {
766 expect(form.getValue('crushRule').rule_name).toBe('rep1');
767 const name = 'awesomeRule';
f67539c2 768 spyOn(TestBed.inject(ModalService), 'show').and.callFake(() => {
e306af50 769 return {
f67539c2 770 componentInstance: {
e306af50
TL
771 submitAction: of({ name })
772 }
773 };
774 });
f6b5b4d7 775 infoReturn.crush_rules_replicated.push(Mocks.getCrushRule({ id: 8, name }));
e306af50
TL
776 component.addCrushRule();
777 expect(form.getValue('crushRule').rule_name).toBe(name);
778 });
779
11fdf7f2
TL
780 it('should not show info per default', () => {
781 fixtureHelper.expectElementVisible('#crushRule', true);
782 fixtureHelper.expectElementVisible('#crush-info-block', false);
783 });
784
785 it('should show info if the info button is clicked', () => {
11fdf7f2
TL
786 const infoButton = fixture.debugElement.query(By.css('#crush-info-button'));
787 infoButton.triggerEventHandler('click', null);
788 expect(component.data.crushInfo).toBeTruthy();
789 fixture.detectChanges();
790 expect(infoButton.classes['active']).toBeTruthy();
791 fixtureHelper.expectIdElementsVisible(['crushRule', 'crush-info-block'], true);
792 });
9f95a23c
TL
793
794 it('should know which rules are in use', () => {
795 selectRuleByIndex(2);
796 expect(component.crushUsage).toEqual(['some.pool.uses.it']);
797 });
798
799 describe('crush rule deletion', () => {
800 let taskWrapper: TaskWrapperService;
801 let deletion: CriticalConfirmationModalComponent;
802 let deleteSpy: jasmine.Spy;
803 let modalSpy: jasmine.Spy;
804
805 const callDeletion = () => {
806 component.deleteCrushRule();
807 deletion.submitActionObservable();
808 };
809
810 const callDeletionWithRuleByIndex = (index: number) => {
811 deleteSpy.calls.reset();
812 selectRuleByIndex(index);
813 callDeletion();
814 };
815
816 const expectSuccessfulDeletion = (name: string) => {
817 expect(crushRuleService.delete).toHaveBeenCalledWith(name);
e306af50
TL
818 expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith(
819 expect.objectContaining({
820 task: {
821 name: 'crushRule/delete',
822 metadata: {
823 name: name
824 }
9f95a23c 825 }
e306af50
TL
826 })
827 );
9f95a23c
TL
828 };
829
830 beforeEach(() => {
f67539c2
TL
831 modalSpy = spyOn(TestBed.inject(ModalService), 'show').and.callFake(
832 (deletionClass: any, initialState: any) => {
833 deletion = Object.assign(new deletionClass(), initialState);
9f95a23c 834 return {
f67539c2 835 componentInstance: deletion
9f95a23c
TL
836 };
837 }
838 );
e306af50 839 deleteSpy = spyOn(crushRuleService, 'delete').and.callFake((name: string) => {
9f95a23c
TL
840 const rules = infoReturn.crush_rules_replicated;
841 const index = _.findIndex(rules, (rule) => rule.rule_name === name);
842 rules.splice(index, 1);
e306af50 843 return of(undefined);
9f95a23c 844 });
f67539c2 845 taskWrapper = TestBed.inject(TaskWrapperService);
9f95a23c
TL
846 spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
847 });
848
849 describe('with unused rule', () => {
850 beforeEach(() => {
851 callDeletionWithRuleByIndex(0);
852 });
853
854 it('should have called delete', () => {
855 expectSuccessfulDeletion('rep1');
856 });
857
858 it('should not open the tooltip nor the crush info', () => {
f67539c2 859 expect(component.crushDeletionBtn.isOpen()).toBe(false);
9f95a23c
TL
860 expect(component.data.crushInfo).toBe(false);
861 });
862
863 it('should reload the rules after deletion', () => {
864 const expected = infoReturn.crush_rules_replicated;
865 const currentRules = component.current.rules;
866 expect(currentRules.length).toBe(expected.length);
867 expect(currentRules).toEqual(expected);
868 });
869 });
870
871 describe('rule in use', () => {
872 beforeEach(() => {
873 spyOn(global, 'setTimeout').and.callFake((fn: Function) => fn());
9f95a23c
TL
874 deleteSpy.calls.reset();
875 selectRuleByIndex(2);
876 component.deleteCrushRule();
877 });
878
879 it('should not have called delete and opened the tooltip', () => {
880 expect(crushRuleService.delete).not.toHaveBeenCalled();
f67539c2 881 expect(component.crushDeletionBtn.isOpen()).toBe(true);
9f95a23c
TL
882 expect(component.data.crushInfo).toBe(true);
883 });
884
9f95a23c
TL
885 it('should hide the tooltip when clicking on delete again', () => {
886 component.deleteCrushRule();
f67539c2 887 expect(component.crushDeletionBtn.isOpen()).toBe(false);
9f95a23c
TL
888 });
889
890 it('should hide the tooltip when clicking on add', () => {
891 modalSpy.and.callFake((): any => ({
f67539c2 892 componentInstance: {
9f95a23c
TL
893 submitAction: of('someRule')
894 }
895 }));
896 component.addCrushRule();
f67539c2 897 expect(component.crushDeletionBtn.isOpen()).toBe(false);
9f95a23c
TL
898 });
899
900 it('should hide the tooltip when changing the crush rule', () => {
901 selectRuleByIndex(0);
f67539c2 902 expect(component.crushDeletionBtn.isOpen()).toBe(false);
9f95a23c
TL
903 });
904 });
905 });
11fdf7f2
TL
906 });
907
908 describe('erasure code profile', () => {
909 const setSelectedEcp = (name: string) => {
910 formHelper.setValue('erasureProfile', { name: name });
911 };
912
913 beforeEach(() => {
914 formHelper.setValue('poolType', 'erasure');
915 fixture.detectChanges();
916 });
917
918 it('should not show info per default', () => {
919 fixtureHelper.expectElementVisible('#erasureProfile', true);
920 fixtureHelper.expectElementVisible('#ecp-info-block', false);
921 });
922
923 it('should show info if the info button is clicked', () => {
924 const infoButton = fixture.debugElement.query(By.css('#ecp-info-button'));
925 infoButton.triggerEventHandler('click', null);
926 expect(component.data.erasureInfo).toBeTruthy();
927 fixture.detectChanges();
928 expect(infoButton.classes['active']).toBeTruthy();
929 fixtureHelper.expectIdElementsVisible(['erasureProfile', 'ecp-info-block'], true);
930 });
931
e306af50
TL
932 it('should select the newly created profile', () => {
933 spyOn(ecpService, 'list').and.callFake(() => of(infoReturn.erasure_code_profiles));
934 expect(form.getValue('erasureProfile').name).toBe('ecp1');
935 const name = 'awesomeProfile';
f67539c2 936 spyOn(TestBed.inject(ModalService), 'show').and.callFake(() => {
e306af50 937 return {
f67539c2 938 componentInstance: {
e306af50
TL
939 submitAction: of({ name })
940 }
941 };
942 });
943 const ecp2 = new ErasureCodeProfile();
944 ecp2.name = name;
945 infoReturn.erasure_code_profiles.push(ecp2);
946 component.addErasureCodeProfile();
947 expect(form.getValue('erasureProfile').name).toBe(name);
948 });
949
11fdf7f2
TL
950 describe('ecp deletion', () => {
951 let taskWrapper: TaskWrapperService;
952 let deletion: CriticalConfirmationModalComponent;
e306af50
TL
953 let deleteSpy: jasmine.Spy;
954 let modalSpy: jasmine.Spy;
f67539c2 955 let modal: NgbModalRef;
11fdf7f2 956
e306af50 957 const callEcpDeletion = () => {
11fdf7f2 958 component.deleteErasureCodeProfile();
f67539c2 959 modal.componentInstance.callSubmitAction();
11fdf7f2
TL
960 };
961
e306af50 962 const expectSuccessfulEcpDeletion = (name: string) => {
11fdf7f2 963 setSelectedEcp(name);
e306af50 964 callEcpDeletion();
11fdf7f2 965 expect(ecpService.delete).toHaveBeenCalledWith(name);
e306af50
TL
966 expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith(
967 expect.objectContaining({
968 task: {
969 name: 'ecp/delete',
970 metadata: {
971 name: name
972 }
11fdf7f2 973 }
e306af50
TL
974 })
975 );
11fdf7f2
TL
976 };
977
978 beforeEach(() => {
e306af50 979 deletion = undefined;
f67539c2 980 modalSpy = spyOn(TestBed.inject(ModalService), 'show').and.callFake(
e306af50
TL
981 (comp: any, init: any) => {
982 modal = modalServiceShow(comp, init);
f67539c2 983 return modal;
e306af50
TL
984 }
985 );
986 deleteSpy = spyOn(ecpService, 'delete').and.callFake((name: string) => {
987 const profiles = infoReturn.erasure_code_profiles;
988 const index = _.findIndex(profiles, (profile) => profile.name === name);
989 profiles.splice(index, 1);
990 return of({ status: 202 });
11fdf7f2 991 });
f67539c2 992 taskWrapper = TestBed.inject(TaskWrapperService);
11fdf7f2 993 spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
e306af50
TL
994
995 const ecp2 = new ErasureCodeProfile();
996 ecp2.name = 'someEcpName';
997 infoReturn.erasure_code_profiles.push(ecp2);
998
999 const ecp3 = new ErasureCodeProfile();
1000 ecp3.name = 'aDifferentEcpName';
1001 infoReturn.erasure_code_profiles.push(ecp3);
11fdf7f2
TL
1002 });
1003
1004 it('should delete two different erasure code profiles', () => {
e306af50
TL
1005 expectSuccessfulEcpDeletion('someEcpName');
1006 expectSuccessfulEcpDeletion('aDifferentEcpName');
1007 });
1008
1009 describe('with unused profile', () => {
1010 beforeEach(() => {
1011 expectSuccessfulEcpDeletion('someEcpName');
1012 });
1013
1014 it('should not open the tooltip nor the crush info', () => {
f67539c2 1015 expect(component.ecpDeletionBtn.isOpen()).toBe(false);
e306af50
TL
1016 expect(component.data.erasureInfo).toBe(false);
1017 });
1018
1019 it('should reload the rules after deletion', () => {
1020 const expected = infoReturn.erasure_code_profiles;
1021 const currentProfiles = component.info.erasure_code_profiles;
1022 expect(currentProfiles.length).toBe(expected.length);
1023 expect(currentProfiles).toEqual(expected);
1024 });
1025 });
1026
1027 describe('rule in use', () => {
1028 beforeEach(() => {
1029 spyOn(global, 'setTimeout').and.callFake((fn: Function) => fn());
e306af50
TL
1030 deleteSpy.calls.reset();
1031 setSelectedEcp('ecp1');
1032 component.deleteErasureCodeProfile();
1033 });
1034
1035 it('should not open the modal', () => {
1036 expect(deletion).toBe(undefined);
1037 });
1038
1039 it('should not have called delete and opened the tooltip', () => {
1040 expect(ecpService.delete).not.toHaveBeenCalled();
f67539c2 1041 expect(component.ecpDeletionBtn.isOpen()).toBe(true);
e306af50
TL
1042 expect(component.data.erasureInfo).toBe(true);
1043 });
1044
e306af50
TL
1045 it('should hide the tooltip when clicking on delete again', () => {
1046 component.deleteErasureCodeProfile();
f67539c2 1047 expect(component.ecpDeletionBtn.isOpen()).toBe(false);
e306af50
TL
1048 });
1049
1050 it('should hide the tooltip when clicking on add', () => {
1051 modalSpy.and.callFake((): any => ({
f67539c2 1052 componentInstance: {
e306af50
TL
1053 submitAction: of('someProfile')
1054 }
1055 }));
1056 component.addErasureCodeProfile();
f67539c2 1057 expect(component.ecpDeletionBtn.isOpen()).toBe(false);
e306af50
TL
1058 });
1059
1060 it('should hide the tooltip when changing the crush rule', () => {
1061 setSelectedEcp('someEcpName');
f67539c2 1062 expect(component.ecpDeletionBtn.isOpen()).toBe(false);
e306af50 1063 });
11fdf7f2
TL
1064 });
1065 });
1066 });
1067
1068 describe('submit - create', () => {
9f95a23c 1069 const setMultipleValues = (settings: object) => {
11fdf7f2
TL
1070 Object.keys(settings).forEach((name) => {
1071 formHelper.setValue(name, settings[name]);
1072 });
1073 };
11fdf7f2
TL
1074
1075 describe('erasure coded pool', () => {
9f95a23c
TL
1076 const expectEcSubmit = (o: any) =>
1077 expectValidSubmit(
1078 Object.assign(
1079 {
1080 pool: 'ecPool',
1081 pool_type: 'erasure',
1082 pg_autoscale_mode: 'off',
1083 erasure_code_profile: 'ecp1',
1084 pg_num: 4
1085 },
1086 o
1087 )
1088 );
1089
1090 beforeEach(() => {
1091 setMultipleValues({
1092 name: 'ecPool',
1093 poolType: 'erasure',
1094 pgNum: 4
1095 });
1096 });
1097
1098 it('minimum requirements without ECP to create ec pool', () => {
1099 // Mock that no ec profiles exist
1100 infoReturn.erasure_code_profiles = [];
1101 setUpPoolComponent();
11fdf7f2
TL
1102 setMultipleValues({
1103 name: 'minECPool',
1104 poolType: 'erasure',
1105 pgNum: 4
1106 });
9f95a23c 1107 expectValidSubmit({
11fdf7f2
TL
1108 pool: 'minECPool',
1109 pool_type: 'erasure',
9f95a23c 1110 pg_autoscale_mode: 'off',
11fdf7f2
TL
1111 pg_num: 4
1112 });
1113 });
1114
9f95a23c 1115 it('creates ec pool with erasure coded profile', () => {
11fdf7f2
TL
1116 const ecp = { name: 'ecpMinimalMock' };
1117 setMultipleValues({
11fdf7f2
TL
1118 erasureProfile: ecp
1119 });
9f95a23c 1120 expectEcSubmit({
11fdf7f2
TL
1121 erasure_code_profile: ecp.name
1122 });
1123 });
1124
9f95a23c 1125 it('creates ec pool with ec_overwrite flag', () => {
11fdf7f2 1126 setMultipleValues({
11fdf7f2
TL
1127 ecOverwrites: true
1128 });
9f95a23c 1129 expectEcSubmit({
11fdf7f2
TL
1130 flags: ['ec_overwrites']
1131 });
1132 });
1133
9f95a23c 1134 it('should ignore replicated set settings for ec pools', () => {
11fdf7f2 1135 setMultipleValues({
9f95a23c 1136 size: 2 // will be ignored
11fdf7f2 1137 });
9f95a23c
TL
1138 expectEcSubmit({});
1139 });
1140
1141 it('creates a pool with compression', () => {
1142 setMultipleValues({
1143 mode: 'passive',
1144 algorithm: 'lz4',
1145 minBlobSize: '4 K',
1146 maxBlobSize: '4 M',
1147 ratio: 0.7
1148 });
1149 expectEcSubmit({
1150 compression_mode: 'passive',
1151 compression_algorithm: 'lz4',
1152 compression_min_blob_size: 4096,
1153 compression_max_blob_size: 4194304,
1154 compression_required_ratio: 0.7
1155 });
1156 });
1157
1158 it('creates a pool with application metadata', () => {
1159 component.data.applications.selected = ['cephfs', 'rgw'];
1160 expectEcSubmit({
1161 application_metadata: ['cephfs', 'rgw']
11fdf7f2
TL
1162 });
1163 });
1164 });
1165
9f95a23c
TL
1166 describe('with replicated pool', () => {
1167 const expectReplicatedSubmit = (o: any) =>
1168 expectValidSubmit(
1169 Object.assign(
1170 {
1171 pool: 'repPool',
1172 pool_type: 'replicated',
1173 pg_autoscale_mode: 'off',
1174 pg_num: 16,
1175 rule_name: 'rep1',
1176 size: 3
1177 },
1178 o
1179 )
1180 );
1181 beforeEach(() => {
1182 setMultipleValues({
1183 name: 'repPool',
1184 poolType: 'replicated',
1185 crushRule: infoReturn.crush_rules_replicated[0],
1186 size: 3,
1187 pgNum: 16
1188 });
1189 });
1190
1191 it('uses the minimum requirements for replicated pools', () => {
1192 // Mock that no replicated rules exist
1193 infoReturn.crush_rules_replicated = [];
1194 setUpPoolComponent();
1195
11fdf7f2
TL
1196 setMultipleValues({
1197 name: 'minRepPool',
1198 poolType: 'replicated',
1199 size: 2,
9f95a23c 1200 pgNum: 32
11fdf7f2 1201 });
9f95a23c 1202 expectValidSubmit({
11fdf7f2
TL
1203 pool: 'minRepPool',
1204 pool_type: 'replicated',
9f95a23c
TL
1205 pg_num: 32,
1206 pg_autoscale_mode: 'off',
11fdf7f2
TL
1207 size: 2
1208 });
1209 });
11fdf7f2 1210
9f95a23c
TL
1211 it('ignores erasure only set settings for replicated pools', () => {
1212 setMultipleValues({
1213 erasureProfile: { name: 'ecpMinimalMock' }, // Will be ignored
1214 ecOverwrites: true // Will be ignored
1215 });
1216 /**
1217 * As pgCalc is triggered through profile changes, which is normally not possible,
1218 * if type `replicated` is set, pgNum will be set to 256 with the current rule for
1219 * a replicated pool.
1220 */
1221 expectReplicatedSubmit({
1222 pg_num: 256
1223 });
11fdf7f2 1224 });
11fdf7f2 1225
9f95a23c
TL
1226 it('creates a pool with quotas', () => {
1227 setMultipleValues({
1228 max_bytes: 1024 * 1024,
1229 max_objects: 3000
1230 });
1231 expectReplicatedSubmit({
1232 quota_max_bytes: 1024 * 1024,
1233 quota_max_objects: 3000
1234 });
11fdf7f2 1235 });
9f95a23c
TL
1236
1237 it('creates a pool with rbd qos settings', () => {
1238 component.currentConfigurationValues = {
1239 rbd_qos_bps_limit: 55
1240 };
1241 expectReplicatedSubmit({
1242 configuration: {
1243 rbd_qos_bps_limit: 55
1244 }
1245 });
11fdf7f2
TL
1246 });
1247 });
1248 });
1249
1250 describe('edit mode', () => {
9f95a23c 1251 const setUrl = (url: string) => {
11fdf7f2
TL
1252 Object.defineProperty(router, 'url', { value: url });
1253 setUpPoolComponent(); // Renew of component needed because the constructor has to be called
1254 };
1255
1256 let pool: Pool;
1257 beforeEach(() => {
1258 pool = new Pool('somePoolName');
1259 pool.type = 'replicated';
1260 pool.size = 3;
9f95a23c 1261 pool.crush_rule = 'rep1';
11fdf7f2
TL
1262 pool.pg_num = 32;
1263 pool.options = {};
1264 pool.options.compression_mode = 'passive';
1265 pool.options.compression_algorithm = 'lz4';
1266 pool.options.compression_min_blob_size = 1024 * 512;
1267 pool.options.compression_max_blob_size = 1024 * 1024;
1268 pool.options.compression_required_ratio = 0.8;
1269 pool.flags_names = 'someFlag1,someFlag2';
9f95a23c
TL
1270 pool.application_metadata = ['rbd', 'ownApp'];
1271 pool.quota_max_bytes = 1024 * 1024 * 1024;
1272 pool.quota_max_objects = 3000;
11fdf7f2 1273
f6b5b4d7 1274 Mocks.getCrushRule({ name: 'someRule' });
11fdf7f2
TL
1275 spyOn(poolService, 'get').and.callFake(() => of(pool));
1276 });
1277
1278 it('is not in edit mode if edit is not included in url', () => {
1279 setUrl('/pool/add');
1280 expect(component.editing).toBeFalsy();
1281 });
1282
1283 it('is in edit mode if edit is included in url', () => {
1284 setUrl('/pool/edit/somePoolName');
1285 expect(component.editing).toBeTruthy();
1286 });
1287
1288 describe('after ngOnInit', () => {
1289 beforeEach(() => {
9f95a23c 1290 setUrl('/pool/edit/somePoolName');
11fdf7f2
TL
1291 fixture.detectChanges();
1292 });
1293
1294 it('disabled inputs', () => {
9f95a23c 1295 fixture.detectChanges();
11fdf7f2
TL
1296 const disabled = ['poolType', 'crushRule', 'size', 'erasureProfile', 'ecOverwrites'];
1297 disabled.forEach((controlName) => {
1298 return expect(form.get(controlName).disabled).toBeTruthy();
1299 });
1300 const enabled = [
1301 'name',
1302 'pgNum',
1303 'mode',
1304 'algorithm',
1305 'minBlobSize',
1306 'maxBlobSize',
9f95a23c
TL
1307 'ratio',
1308 'max_bytes',
1309 'max_objects'
11fdf7f2
TL
1310 ];
1311 enabled.forEach((controlName) => {
1312 return expect(form.get(controlName).enabled).toBeTruthy();
1313 });
1314 });
1315
9f95a23c
TL
1316 it('should include the custom app as valid option', () => {
1317 expect(
1318 component.data.applications.available.map((app: Record<string, any>) => app.name)
1319 ).toEqual(['cephfs', 'ownApp', 'rbd', 'rgw']);
1320 });
1321
11fdf7f2
TL
1322 it('set all control values to the given pool', () => {
1323 expect(form.getValue('name')).toBe(pool.pool_name);
1324 expect(form.getValue('poolType')).toBe(pool.type);
1325 expect(form.getValue('crushRule')).toEqual(component.info.crush_rules_replicated[0]);
1326 expect(form.getValue('size')).toBe(pool.size);
1327 expect(form.getValue('pgNum')).toBe(pool.pg_num);
1328 expect(form.getValue('mode')).toBe(pool.options.compression_mode);
1329 expect(form.getValue('algorithm')).toBe(pool.options.compression_algorithm);
1330 expect(form.getValue('minBlobSize')).toBe('512 KiB');
1331 expect(form.getValue('maxBlobSize')).toBe('1 MiB');
1332 expect(form.getValue('ratio')).toBe(pool.options.compression_required_ratio);
9f95a23c
TL
1333 expect(form.getValue('max_bytes')).toBe('1 GiB');
1334 expect(form.getValue('max_objects')).toBe(pool.quota_max_objects);
11fdf7f2
TL
1335 });
1336
eafe8130
TL
1337 it('updates pgs on every change', () => {
1338 testPgUpdate(undefined, -1, 16);
1339 testPgUpdate(undefined, -1, 8);
1340 });
1341
1342 it('is possible to use less or more pgs than before', () => {
11fdf7f2 1343 formHelper.expectValid(setPgNum(64));
eafe8130 1344 formHelper.expectValid(setPgNum(4));
11fdf7f2
TL
1345 });
1346
1347 describe('submit', () => {
9f95a23c
TL
1348 const markControlAsPreviouslySet = (controlName: string) =>
1349 form.get(controlName).markAsPristine();
11fdf7f2
TL
1350
1351 beforeEach(() => {
801d1391
TL
1352 [
1353 'algorithm',
1354 'maxBlobSize',
1355 'minBlobSize',
1356 'mode',
1357 'pgNum',
1358 'ratio',
1359 'name'
1360 ].forEach((name) => markControlAsPreviouslySet(name));
11fdf7f2
TL
1361 fixture.detectChanges();
1362 });
1363
1364 it(`always provides the application metadata array with submit even if it's empty`, () => {
1365 expect(form.get('mode').dirty).toBe(false);
1366 component.data.applications.selected = [];
1367 expectValidSubmit(
1368 {
1369 application_metadata: [],
1370 pool: 'somePoolName'
1371 },
1372 'pool/edit',
1373 'update'
1374 );
1375 });
1376
1377 it(`will always provide reset value for compression options`, () => {
1378 formHelper.setValue('minBlobSize', '').markAsDirty();
1379 formHelper.setValue('maxBlobSize', '').markAsDirty();
1380 formHelper.setValue('ratio', '').markAsDirty();
1381 expectValidSubmit(
1382 {
9f95a23c 1383 application_metadata: ['ownApp', 'rbd'],
11fdf7f2
TL
1384 compression_max_blob_size: 0,
1385 compression_min_blob_size: 0,
1386 compression_required_ratio: 0,
1387 pool: 'somePoolName'
1388 },
1389 'pool/edit',
1390 'update'
1391 );
1392 });
1393
1394 it(`will unset mode not used anymore`, () => {
1395 formHelper.setValue('mode', 'none').markAsDirty();
1396 expectValidSubmit(
1397 {
9f95a23c 1398 application_metadata: ['ownApp', 'rbd'],
11fdf7f2
TL
1399 compression_mode: 'unset',
1400 pool: 'somePoolName'
1401 },
1402 'pool/edit',
1403 'update'
1404 );
1405 });
1406 });
1407 });
1408 });
1409
1410 describe('test pool configuration component', () => {
1411 it('is visible for replicated pools with rbd application', () => {
1412 const poolType = component.form.get('poolType');
1413 poolType.markAsDirty();
1414 poolType.setValue('replicated');
1415 component.data.applications.selected = ['rbd'];
1416 fixture.detectChanges();
1417 expect(
1418 fixture.debugElement.query(By.css('cd-rbd-configuration-form')).nativeElement.parentElement
1419 .hidden
1420 ).toBe(false);
1421 });
1422
1423 it('is invisible for erasure coded pools', () => {
1424 const poolType = component.form.get('poolType');
1425 poolType.markAsDirty();
1426 poolType.setValue('erasure');
1427 fixture.detectChanges();
1428 expect(
1429 fixture.debugElement.query(By.css('cd-rbd-configuration-form')).nativeElement.parentElement
1430 .hidden
1431 ).toBe(true);
1432 });
1433 });
1434});