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