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