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