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