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