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