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