summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts')
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts1466
1 files changed, 1466 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts
new file mode 100644
index 000000000..1d58a1778
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-form/pool-form.component.spec.ts
@@ -0,0 +1,1466 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { AbstractControl } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { ActivatedRoute, Router, Routes } from '@angular/router';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import {
+ NgbActiveModal,
+ NgbModalModule,
+ NgbModalRef,
+ NgbNavModule
+} from '@ng-bootstrap/ng-bootstrap';
+import _ from 'lodash';
+import { ToastrModule } from 'ngx-toastr';
+import { of } from 'rxjs';
+
+import { DashboardNotFoundError } from '~/app/core/error/error';
+import { ErrorComponent } from '~/app/core/error/error.component';
+import { CrushRuleService } from '~/app/shared/api/crush-rule.service';
+import { ErasureCodeProfileService } from '~/app/shared/api/erasure-code-profile.service';
+import { PoolService } from '~/app/shared/api/pool.service';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { SelectBadgesComponent } from '~/app/shared/components/select-badges/select-badges.component';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { ErasureCodeProfile } from '~/app/shared/models/erasure-code-profile';
+import { Permission } from '~/app/shared/models/permissions';
+import { PoolFormInfo } from '~/app/shared/models/pool-form-info';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import {
+ configureTestBed,
+ FixtureHelper,
+ FormHelper,
+ Mocks,
+ modalServiceShow
+} from '~/testing/unit-test-helper';
+import { Pool } from '../pool';
+import { PoolModule } from '../pool.module';
+import { PoolFormComponent } from './pool-form.component';
+
+describe('PoolFormComponent', () => {
+ let OSDS = 15;
+ let formHelper: FormHelper;
+ let fixtureHelper: FixtureHelper;
+ let component: PoolFormComponent;
+ let fixture: ComponentFixture<PoolFormComponent>;
+ let poolService: PoolService;
+ let form: CdFormGroup;
+ let router: Router;
+ let ecpService: ErasureCodeProfileService;
+ let crushRuleService: CrushRuleService;
+ let poolPermissions: Permission;
+ let authStorageService: AuthStorageService;
+
+ const setPgNum = (pgs: number): AbstractControl => {
+ const control = formHelper.setValue('pgNum', pgs);
+ fixture.debugElement.query(By.css('#pgNum')).nativeElement.dispatchEvent(new Event('blur'));
+ return control;
+ };
+
+ const testPgUpdate = (pgs: number, jump: number, returnValue: number) => {
+ if (pgs) {
+ setPgNum(pgs);
+ }
+ if (jump) {
+ setPgNum(form.getValue('pgNum') + jump);
+ }
+ expect(form.getValue('pgNum')).toBe(returnValue);
+ };
+
+ const expectValidSubmit = (
+ pool: any,
+ taskName = 'pool/create',
+ poolServiceMethod: 'create' | 'update' = 'create'
+ ) => {
+ spyOn(poolService, poolServiceMethod).and.stub();
+ const taskWrapper = TestBed.inject(TaskWrapperService);
+ spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
+ component.submit();
+ expect(poolService[poolServiceMethod]).toHaveBeenCalledWith(pool);
+ expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith({
+ task: {
+ name: taskName,
+ metadata: {
+ pool_name: pool.pool
+ }
+ },
+ call: undefined // because of stub
+ });
+ };
+
+ let infoReturn: PoolFormInfo;
+ const setInfo = () => {
+ const ecp1 = new ErasureCodeProfile();
+ ecp1.name = 'ecp1';
+ infoReturn = {
+ pool_names: ['someExistingPoolName'],
+ osd_count: OSDS,
+ is_all_bluestore: true,
+ bluestore_compression_algorithm: 'snappy',
+ compression_algorithms: ['snappy'],
+ compression_modes: ['none', 'passive'],
+ crush_rules_replicated: [
+ Mocks.getCrushRule({ id: 0, min: 2, max: 4, name: 'rep1', type: 'replicated' }),
+ Mocks.getCrushRule({ id: 1, min: 3, max: 18, name: 'rep2', type: 'replicated' }),
+ Mocks.getCrushRule({ id: 2, min: 1, max: 9, name: 'used_rule', type: 'replicated' })
+ ],
+ crush_rules_erasure: [
+ Mocks.getCrushRule({ id: 3, min: 1, max: 1, name: 'ecp1', type: 'erasure' })
+ ],
+ erasure_code_profiles: [ecp1],
+ pg_autoscale_default_mode: 'off',
+ pg_autoscale_modes: ['off', 'warn', 'on'],
+ used_rules: {
+ used_rule: ['some.pool.uses.it']
+ },
+ used_profiles: {
+ ecp1: ['some.other.pool.uses.it']
+ },
+ nodes: Mocks.generateSimpleCrushMap(3, 5)
+ };
+ };
+
+ const setUpPoolComponent = () => {
+ fixture = TestBed.createComponent(PoolFormComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+
+ fixtureHelper = new FixtureHelper(fixture);
+ form = component.form;
+ formHelper = new FormHelper(form);
+ };
+
+ const routes: Routes = [{ path: '404', component: ErrorComponent }];
+
+ configureTestBed(
+ {
+ declarations: [ErrorComponent],
+ imports: [
+ BrowserAnimationsModule,
+ HttpClientTestingModule,
+ RouterTestingModule.withRoutes(routes),
+ ToastrModule.forRoot(),
+ NgbNavModule,
+ PoolModule,
+ SharedModule,
+ NgbModalModule
+ ],
+ providers: [
+ ErasureCodeProfileService,
+ NgbActiveModal,
+ SelectBadgesComponent,
+ { provide: ActivatedRoute, useValue: { params: of({ name: 'somePoolName' }) } }
+ ]
+ },
+ [CriticalConfirmationModalComponent]
+ );
+
+ let navigationSpy: jasmine.Spy;
+
+ beforeEach(() => {
+ poolService = TestBed.inject(PoolService);
+ setInfo();
+ spyOn(poolService, 'getInfo').and.callFake(() => of(infoReturn));
+
+ ecpService = TestBed.inject(ErasureCodeProfileService);
+ crushRuleService = TestBed.inject(CrushRuleService);
+
+ router = TestBed.inject(Router);
+ navigationSpy = spyOn(router, 'navigate').and.stub();
+ authStorageService = TestBed.inject(AuthStorageService);
+ spyOn(authStorageService, 'getPermissions').and.callFake(() => ({
+ pool: poolPermissions
+ }));
+ poolPermissions = new Permission(['update', 'delete', 'read', 'create']);
+ setUpPoolComponent();
+
+ component.loadingReady();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('throws error for not allowed users', () => {
+ const expectError = (redirected: boolean) => {
+ navigationSpy.calls.reset();
+ if (redirected) {
+ expect(() => component.authenticate()).toThrowError(DashboardNotFoundError);
+ } else {
+ expect(() => component.authenticate()).not.toThrowError();
+ }
+ };
+
+ beforeEach(() => {
+ poolPermissions = new Permission(['delete']);
+ });
+
+ it('navigates to Dashboard if not allowed', () => {
+ expect(() => component.authenticate()).toThrowError(DashboardNotFoundError);
+ });
+
+ it('throws error if user is not allowed', () => {
+ expectError(true);
+ poolPermissions.read = true;
+ expectError(true);
+ poolPermissions.delete = true;
+ expectError(true);
+ poolPermissions.update = true;
+ expectError(true);
+ component.editing = true;
+ poolPermissions.update = false;
+ poolPermissions.create = true;
+ expectError(true);
+ });
+
+ it('does not throw error for users with right permissions', () => {
+ poolPermissions.read = true;
+ poolPermissions.create = true;
+ expectError(false);
+ component.editing = true;
+ poolPermissions.update = true;
+ expectError(false);
+ poolPermissions.create = false;
+ expectError(false);
+ });
+ });
+
+ describe('pool form validation', () => {
+ beforeEach(() => {
+ fixture.detectChanges();
+ });
+
+ it('is invalid at the beginning all sub forms are valid', () => {
+ expect(form.valid).toBeFalsy();
+ ['name', 'poolType', 'pgNum'].forEach((name) => formHelper.expectError(name, 'required'));
+ ['size', 'crushRule', 'erasureProfile', 'ecOverwrites'].forEach((name) =>
+ formHelper.expectValid(name)
+ );
+ expect(component.form.get('compression').valid).toBeTruthy();
+ });
+
+ it('validates name', () => {
+ expect(component.editing).toBeFalsy();
+ formHelper.expectError('name', 'required');
+ formHelper.expectValidChange('name', 'some-name');
+ formHelper.expectValidChange('name', 'name/with/slash');
+ formHelper.expectErrorChange('name', 'someExistingPoolName', 'uniqueName');
+ formHelper.expectErrorChange('name', 'wrong format with spaces', 'pattern');
+ });
+
+ it('should validate with dots in pool name', () => {
+ formHelper.expectValidChange('name', 'pool.default.bar', true);
+ });
+
+ it('validates poolType', () => {
+ formHelper.expectError('poolType', 'required');
+ formHelper.expectValidChange('poolType', 'erasure');
+ formHelper.expectValidChange('poolType', 'replicated');
+ });
+
+ it('validates that pgNum is required creation mode', () => {
+ formHelper.expectError(form.get('pgNum'), 'required');
+ });
+
+ it('validates pgNum in edit mode', () => {
+ component.data.pool = new Pool('test');
+ component.data.pool.pg_num = 16;
+ component.editing = true;
+ component.ngOnInit(); // Switches form into edit mode
+ formHelper.setValue('poolType', 'erasure');
+ fixture.detectChanges();
+ formHelper.expectValid(setPgNum(8));
+ });
+
+ it('is valid if pgNum, poolType and name are valid', () => {
+ formHelper.setValue('name', 'some-name');
+ formHelper.setValue('poolType', 'erasure');
+ fixture.detectChanges();
+ setPgNum(1);
+ expect(form.valid).toBeTruthy();
+ });
+
+ it('validates crushRule with multiple crush rules', () => {
+ formHelper.expectValidChange('poolType', 'replicated');
+ form.get('crushRule').updateValueAndValidity();
+ formHelper.expectError('crushRule', 'required'); // As multiple rules exist
+ formHelper.expectErrorChange('crushRule', { min_size: 20 }, 'tooFewOsds');
+ });
+
+ it('validates crushRule with no crush rules', () => {
+ infoReturn.crush_rules_replicated = [];
+ setUpPoolComponent();
+ formHelper.expectValidChange('poolType', 'replicated');
+ formHelper.expectValid('crushRule');
+ });
+
+ it('validates size', () => {
+ component.info.nodes = Mocks.getCrushMap();
+ formHelper.setValue('poolType', 'replicated');
+ formHelper.expectValid('size');
+ formHelper.setValue('crushRule', Mocks.getCrushRule({ min: 2, max: 6 })); // 3 OSDs usable
+ formHelper.expectErrorChange('size', 1, 'min');
+ formHelper.expectErrorChange('size', 4, 'max'); // More than usable
+ formHelper.expectValidChange('size', 3);
+
+ formHelper.setValue(
+ 'crushRule',
+ Mocks.getCrushRule({ min: 1, max: 2, failureDomain: 'osd-rack' }) // 4 OSDs usable
+ );
+ formHelper.expectErrorChange('size', 4, 'max'); // More than rule allows
+ formHelper.expectValidChange('size', 2);
+ });
+
+ it('validates if warning is displayed when size is 1', () => {
+ formHelper.setValue('poolType', 'replicated');
+ formHelper.expectValid('size');
+
+ formHelper.setValue('size', 1, true);
+ expect(fixtureHelper.getElementByCss('#size ~ .text-warning-dark')).toBeTruthy();
+
+ formHelper.setValue('size', 2, true);
+ expect(fixtureHelper.getElementByCss('#size ~ .text-warning-dark')).toBeFalsy();
+ });
+
+ it('validates compression mode default value', () => {
+ expect(form.getValue('mode')).toBe('none');
+ });
+
+ it('validate quotas', () => {
+ formHelper.expectValid('max_bytes');
+ formHelper.expectValid('max_objects');
+ formHelper.expectValidChange('max_bytes', '10 Gib');
+ formHelper.expectValidChange('max_bytes', '');
+ formHelper.expectValidChange('max_objects', '');
+ formHelper.expectErrorChange('max_objects', -1, 'min');
+ });
+
+ describe('compression form', () => {
+ beforeEach(() => {
+ formHelper.setValue('poolType', 'replicated');
+ formHelper.setValue('mode', 'passive');
+ });
+
+ it('is valid', () => {
+ expect(component.form.get('compression').valid).toBeTruthy();
+ });
+
+ it('validates minBlobSize to be only valid between 0 and maxBlobSize', () => {
+ formHelper.expectErrorChange('minBlobSize', -1, 'min');
+ formHelper.expectValidChange('minBlobSize', 0);
+ formHelper.setValue('maxBlobSize', '2 KiB');
+ formHelper.expectErrorChange('minBlobSize', '3 KiB', 'maximum');
+ formHelper.expectValidChange('minBlobSize', '1.9 KiB');
+ });
+
+ it('validates minBlobSize converts numbers', () => {
+ const control = formHelper.setValue('minBlobSize', '1');
+ fixture.detectChanges();
+ formHelper.expectValid(control);
+ expect(control.value).toBe('1 KiB');
+ });
+
+ it('validates maxBlobSize to be only valid bigger than minBlobSize', () => {
+ formHelper.expectErrorChange('maxBlobSize', -1, 'min');
+ formHelper.setValue('minBlobSize', '1 KiB');
+ formHelper.expectErrorChange('maxBlobSize', '0.5 KiB', 'minimum');
+ formHelper.expectValidChange('maxBlobSize', '1.5 KiB');
+ });
+
+ it('s valid to only use one blob size', () => {
+ formHelper.expectValid(formHelper.setValue('minBlobSize', '1 KiB'));
+ formHelper.expectValid(formHelper.setValue('maxBlobSize', ''));
+ formHelper.expectValid(formHelper.setValue('minBlobSize', ''));
+ formHelper.expectValid(formHelper.setValue('maxBlobSize', '1 KiB'));
+ });
+
+ it('dismisses any size error if one of the blob sizes is changed into a valid state', () => {
+ const min = formHelper.setValue('minBlobSize', '10 KiB');
+ const max = formHelper.setValue('maxBlobSize', '1 KiB');
+ fixture.detectChanges();
+ max.setValue('');
+ formHelper.expectValid(min);
+ formHelper.expectValid(max);
+ max.setValue('1 KiB');
+ fixture.detectChanges();
+ min.setValue('0.5 KiB');
+ formHelper.expectValid(min);
+ formHelper.expectValid(max);
+ });
+
+ it('validates maxBlobSize converts numbers', () => {
+ const control = formHelper.setValue('maxBlobSize', '2');
+ fixture.detectChanges();
+ expect(control.value).toBe('2 KiB');
+ });
+
+ it('validates that odd size validator works as expected', () => {
+ const odd = (min: string, max: string) => component['oddBlobSize'](min, max);
+ expect(odd('10', '8')).toBe(true);
+ expect(odd('8', '-')).toBe(false);
+ expect(odd('8', '10')).toBe(false);
+ expect(odd(null, '8')).toBe(false);
+ expect(odd('10', '')).toBe(false);
+ expect(odd('10', null)).toBe(false);
+ expect(odd(null, null)).toBe(false);
+ });
+
+ it('validates ratio to be only valid between 0 and 1', () => {
+ formHelper.expectValid('ratio');
+ formHelper.expectErrorChange('ratio', -0.1, 'min');
+ formHelper.expectValidChange('ratio', 0);
+ formHelper.expectValidChange('ratio', 1);
+ formHelper.expectErrorChange('ratio', 1.1, 'max');
+ });
+ });
+
+ it('validates application metadata name', () => {
+ formHelper.setValue('poolType', 'replicated');
+ fixture.detectChanges();
+ const selectBadges = fixture.debugElement.query(By.directive(SelectBadgesComponent))
+ .componentInstance;
+ const control = selectBadges.cdSelect.filter;
+ formHelper.expectValid(control);
+ control.setValue('?');
+ formHelper.expectError(control, 'pattern');
+ control.setValue('Ab3_');
+ formHelper.expectValid(control);
+ control.setValue('a'.repeat(129));
+ formHelper.expectError(control, 'maxlength');
+ });
+ });
+
+ describe('pool type changes', () => {
+ beforeEach(() => {
+ component.ngOnInit();
+ });
+
+ it('should have a default replicated size of 3', () => {
+ formHelper.setValue('poolType', 'replicated');
+ expect(form.getValue('size')).toBe(3);
+ });
+
+ describe('replicatedRuleChange', () => {
+ beforeEach(() => {
+ formHelper.setValue('poolType', 'replicated');
+ formHelper.setValue('size', 99);
+ });
+
+ it('should not set size if a replicated pool is not set', () => {
+ formHelper.setValue('poolType', 'erasure');
+ expect(form.getValue('size')).toBe(99);
+ formHelper.setValue('crushRule', component.info.crush_rules_replicated[1]);
+ expect(form.getValue('size')).toBe(99);
+ });
+
+ it('should set size to maximum if size exceeds maximum', () => {
+ formHelper.setValue('crushRule', component.info.crush_rules_replicated[0]);
+ expect(form.getValue('size')).toBe(4);
+ });
+
+ it('should set size to minimum if size is lower than minimum', () => {
+ formHelper.setValue('size', -1);
+ formHelper.setValue('crushRule', component.info.crush_rules_replicated[0]);
+ expect(form.getValue('size')).toBe(2);
+ });
+ });
+
+ describe('rulesChange', () => {
+ it('has no effect if info is not there', () => {
+ delete component.info;
+ formHelper.setValue('poolType', 'replicated');
+ expect(component.current.rules).toEqual([]);
+ });
+
+ it('has no effect if pool type is not set', () => {
+ component['poolTypeChange']('');
+ expect(component.current.rules).toEqual([]);
+ });
+
+ it('shows all replicated rules when pool type is "replicated"', () => {
+ formHelper.setValue('poolType', 'replicated');
+ expect(component.current.rules).toEqual(component.info.crush_rules_replicated);
+ expect(component.current.rules.length).toBe(3);
+ });
+
+ it('shows all erasure code rules when pool type is "erasure"', () => {
+ formHelper.setValue('poolType', 'erasure');
+ expect(component.current.rules).toEqual(component.info.crush_rules_erasure);
+ expect(component.current.rules.length).toBe(1);
+ });
+
+ it('disables rule field if only one rule exists which is used in the disabled field', () => {
+ infoReturn.crush_rules_replicated = [
+ Mocks.getCrushRule({ id: 0, min: 2, max: 4, name: 'rep1', type: 'replicated' })
+ ];
+ setUpPoolComponent();
+ formHelper.setValue('poolType', 'replicated');
+ const control = form.get('crushRule');
+ expect(control.value).toEqual(component.info.crush_rules_replicated[0]);
+ expect(control.disabled).toBe(true);
+ });
+
+ it('does not select the first rule if more than one exist', () => {
+ formHelper.setValue('poolType', 'replicated');
+ const control = form.get('crushRule');
+ expect(control.value).toEqual(null);
+ expect(control.disabled).toBe(false);
+ });
+
+ it('changing between both pool types will not forget the crush rule selection', () => {
+ formHelper.setValue('poolType', 'replicated');
+ const control = form.get('crushRule');
+ const currentRule = component.info.crush_rules_replicated[0];
+ control.setValue(currentRule);
+ formHelper.setValue('poolType', 'erasure');
+ formHelper.setValue('poolType', 'replicated');
+ expect(control.value).toEqual(currentRule);
+ });
+ });
+ });
+
+ describe('getMaxSize and getMinSize', () => {
+ const setCrushRule = ({ min, max }: { min?: number; max?: number }) => {
+ formHelper.setValue('crushRule', Mocks.getCrushRule({ min, max }));
+ };
+
+ it('returns 0 if osd count is 0', () => {
+ component.info.osd_count = 0;
+ expect(component.getMinSize()).toBe(0);
+ expect(component.getMaxSize()).toBe(0);
+ });
+
+ it('returns 0 if info is not there', () => {
+ delete component.info;
+ expect(component.getMinSize()).toBe(0);
+ expect(component.getMaxSize()).toBe(0);
+ });
+
+ it('returns minimum and maximum of rule', () => {
+ setCrushRule({ min: 2, max: 6 });
+ expect(component.getMinSize()).toBe(2);
+ expect(component.getMaxSize()).toBe(6);
+ });
+
+ it('returns 1 as minimum and 3 as maximum if no crush rule is available', () => {
+ expect(component.getMinSize()).toBe(1);
+ expect(component.getMaxSize()).toBe(3);
+ });
+
+ it('returns the osd count as maximum if the rule maximum exceeds it', () => {
+ setCrushRule({ max: 100 });
+ expect(component.getMaxSize()).toBe(15);
+ });
+
+ it('should return the osd count as minimum if its lower the the rule minimum', () => {
+ setCrushRule({ min: 20 });
+ expect(component.getMinSize()).toBe(20);
+ const control = form.get('crushRule');
+ expect(control.invalid).toBe(true);
+ formHelper.expectError(control, 'tooFewOsds');
+ });
+
+ it('should get the right maximum if the device type is defined', () => {
+ formHelper.setValue(
+ 'crushRule',
+ Mocks.getCrushRule({ min: 1, max: 5, itemName: 'default~ssd' })
+ );
+ expect(form.getValue('crushRule').usable_size).toBe(5);
+ });
+ });
+
+ describe('application metadata', () => {
+ let selectBadges: SelectBadgesComponent;
+
+ const testAddApp = (app?: string, result?: string[]) => {
+ selectBadges.cdSelect.filter.setValue(app);
+ selectBadges.cdSelect.updateFilter();
+ selectBadges.cdSelect.selectOption();
+ expect(component.data.applications.selected).toEqual(result);
+ };
+
+ const testRemoveApp = (app: string, result: string[]) => {
+ selectBadges.cdSelect.removeItem(app);
+ expect(component.data.applications.selected).toEqual(result);
+ };
+
+ const setCurrentApps = (apps: string[]) => {
+ component.data.applications.selected = apps;
+ fixture.detectChanges();
+ selectBadges.cdSelect.ngOnInit();
+ return apps;
+ };
+
+ beforeEach(() => {
+ formHelper.setValue('poolType', 'replicated');
+ fixture.detectChanges();
+ selectBadges = fixture.debugElement.query(By.directive(SelectBadgesComponent))
+ .componentInstance;
+ });
+
+ it('adds all predefined and a custom applications to the application metadata array', () => {
+ testAddApp('g', ['rgw']);
+ testAddApp('b', ['rbd', 'rgw']);
+ testAddApp('c', ['cephfs', 'rbd', 'rgw']);
+ testAddApp('ownApp', ['cephfs', 'ownApp', 'rbd', 'rgw']);
+ });
+
+ it('only allows 4 apps to be added to the array', () => {
+ const apps = setCurrentApps(['d', 'c', 'b', 'a']);
+ testAddApp('e', apps);
+ });
+
+ it('can remove apps', () => {
+ setCurrentApps(['a', 'b', 'c', 'd']);
+ testRemoveApp('c', ['a', 'b', 'd']);
+ testRemoveApp('a', ['b', 'd']);
+ testRemoveApp('d', ['b']);
+ testRemoveApp('b', []);
+ });
+
+ it('does not remove any app that is not in the array', () => {
+ const apps = ['a', 'b', 'c', 'd'];
+ setCurrentApps(apps);
+ testRemoveApp('e', apps);
+ testRemoveApp('0', apps);
+ });
+ });
+
+ describe('pg number changes', () => {
+ beforeEach(() => {
+ formHelper.setValue('crushRule', {
+ min_size: 1,
+ max_size: 20
+ });
+ formHelper.setValue('poolType', 'erasure');
+ fixture.detectChanges();
+ setPgNum(256);
+ });
+
+ it('updates by value', () => {
+ testPgUpdate(10, undefined, 8);
+ testPgUpdate(22, undefined, 16);
+ testPgUpdate(26, undefined, 32);
+ testPgUpdate(200, undefined, 256);
+ testPgUpdate(300, undefined, 256);
+ testPgUpdate(350, undefined, 256);
+ });
+
+ it('updates by jump -> a magnitude of the power of 2', () => {
+ testPgUpdate(undefined, 1, 512);
+ testPgUpdate(undefined, -1, 256);
+ });
+
+ it('returns 1 as minimum for false numbers', () => {
+ testPgUpdate(-26, undefined, 1);
+ testPgUpdate(0, undefined, 1);
+ testPgUpdate(0, -1, 1);
+ testPgUpdate(undefined, -20, 1);
+ });
+
+ it('changes the value and than jumps', () => {
+ testPgUpdate(230, 1, 512);
+ testPgUpdate(3500, -1, 2048);
+ });
+
+ describe('pg power jump', () => {
+ it('should jump correctly at the beginning', () => {
+ testPgUpdate(1, -1, 1);
+ testPgUpdate(1, 1, 2);
+ testPgUpdate(2, -1, 1);
+ testPgUpdate(2, 1, 4);
+ testPgUpdate(4, -1, 2);
+ testPgUpdate(4, 1, 8);
+ testPgUpdate(4, 1, 8);
+ });
+
+ it('increments pg power if difference to the current number is 1', () => {
+ testPgUpdate(undefined, 1, 512);
+ testPgUpdate(undefined, 1, 1024);
+ testPgUpdate(undefined, 1, 2048);
+ testPgUpdate(undefined, 1, 4096);
+ });
+
+ it('decrements pg power if difference to the current number is -1', () => {
+ testPgUpdate(undefined, -1, 128);
+ testPgUpdate(undefined, -1, 64);
+ testPgUpdate(undefined, -1, 32);
+ testPgUpdate(undefined, -1, 16);
+ testPgUpdate(undefined, -1, 8);
+ });
+ });
+
+ describe('pgCalc', () => {
+ const PGS = 1;
+ OSDS = 8;
+
+ const getValidCase = () => ({
+ type: 'replicated',
+ osds: OSDS,
+ size: 4,
+ ecp: {
+ k: 2,
+ m: 2
+ },
+ expected: 256
+ });
+
+ const testPgCalc = ({ type, osds, size, ecp, expected }: Record<string, any>) => {
+ component.info.osd_count = osds;
+ formHelper.setValue('poolType', type);
+ if (type === 'replicated') {
+ formHelper.setValue('size', size);
+ } else {
+ formHelper.setValue('erasureProfile', ecp);
+ }
+ expect(form.getValue('pgNum')).toBe(expected);
+ expect(component.externalPgChange).toBe(PGS !== expected);
+ };
+
+ beforeEach(() => {
+ setPgNum(PGS);
+ });
+
+ it('does not change anything if type is not valid', () => {
+ const test = getValidCase();
+ test.type = '';
+ test.expected = PGS;
+ testPgCalc(test);
+ });
+
+ it('does not change anything if ecp is not valid', () => {
+ const test = getValidCase();
+ test.expected = PGS;
+ test.type = 'erasure';
+ test.ecp = null;
+ testPgCalc(test);
+ });
+
+ it('calculates some replicated values', () => {
+ const test = getValidCase();
+ testPgCalc(test);
+ test.osds = 16;
+ test.expected = 512;
+ testPgCalc(test);
+ test.osds = 8;
+ test.size = 8;
+ test.expected = 128;
+ testPgCalc(test);
+ });
+
+ it('calculates erasure code values even if selection is disabled', () => {
+ component['initEcp']([{ k: 2, m: 2, name: 'bla', plugin: '', technique: '' }]);
+ const test = getValidCase();
+ test.type = 'erasure';
+ testPgCalc(test);
+ expect(form.get('erasureProfile').disabled).toBeTruthy();
+ });
+
+ it('calculates some erasure code values', () => {
+ const test = getValidCase();
+ test.type = 'erasure';
+ testPgCalc(test);
+ test.osds = 16;
+ test.ecp.m = 5;
+ test.expected = 256;
+ testPgCalc(test);
+ test.ecp.k = 5;
+ test.expected = 128;
+ testPgCalc(test);
+ });
+
+ it('should not change a manual set pg number', () => {
+ form.get('pgNum').markAsDirty();
+ const test = getValidCase();
+ test.expected = PGS;
+ testPgCalc(test);
+ });
+ });
+ });
+
+ describe('crushRule', () => {
+ const selectRuleByIndex = (n: number) => {
+ formHelper.setValue('crushRule', component.info.crush_rules_replicated[n]);
+ };
+
+ beforeEach(() => {
+ formHelper.setValue('poolType', 'replicated');
+ selectRuleByIndex(0);
+ fixture.detectChanges();
+ });
+
+ it('should select the newly created rule', () => {
+ expect(form.getValue('crushRule').rule_name).toBe('rep1');
+ const name = 'awesomeRule';
+ spyOn(TestBed.inject(ModalService), 'show').and.callFake(() => {
+ return {
+ componentInstance: {
+ submitAction: of({ name })
+ }
+ };
+ });
+ infoReturn.crush_rules_replicated.push(Mocks.getCrushRule({ id: 8, name }));
+ component.addCrushRule();
+ expect(form.getValue('crushRule').rule_name).toBe(name);
+ });
+
+ it('should not show info per default', () => {
+ fixtureHelper.expectElementVisible('#crushRule', true);
+ fixtureHelper.expectElementVisible('#crush-info-block', false);
+ });
+
+ it('should show info if the info button is clicked', () => {
+ const infoButton = fixture.debugElement.query(By.css('#crush-info-button'));
+ infoButton.triggerEventHandler('click', null);
+ expect(component.data.crushInfo).toBeTruthy();
+ fixture.detectChanges();
+ expect(infoButton.classes['active']).toBeTruthy();
+ fixtureHelper.expectIdElementsVisible(['crushRule', 'crush-info-block'], true);
+ });
+
+ it('should know which rules are in use', () => {
+ selectRuleByIndex(2);
+ expect(component.crushUsage).toEqual(['some.pool.uses.it']);
+ });
+
+ describe('crush rule deletion', () => {
+ let taskWrapper: TaskWrapperService;
+ let deletion: CriticalConfirmationModalComponent;
+ let deleteSpy: jasmine.Spy;
+ let modalSpy: jasmine.Spy;
+
+ const callDeletion = () => {
+ component.deleteCrushRule();
+ deletion.submitActionObservable();
+ };
+
+ const callDeletionWithRuleByIndex = (index: number) => {
+ deleteSpy.calls.reset();
+ selectRuleByIndex(index);
+ callDeletion();
+ };
+
+ const expectSuccessfulDeletion = (name: string) => {
+ expect(crushRuleService.delete).toHaveBeenCalledWith(name);
+ expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith(
+ expect.objectContaining({
+ task: {
+ name: 'crushRule/delete',
+ metadata: {
+ name: name
+ }
+ }
+ })
+ );
+ };
+
+ beforeEach(() => {
+ modalSpy = spyOn(TestBed.inject(ModalService), 'show').and.callFake(
+ (deletionClass: any, initialState: any) => {
+ deletion = Object.assign(new deletionClass(), initialState);
+ return {
+ componentInstance: deletion
+ };
+ }
+ );
+ deleteSpy = spyOn(crushRuleService, 'delete').and.callFake((name: string) => {
+ const rules = infoReturn.crush_rules_replicated;
+ const index = _.findIndex(rules, (rule) => rule.rule_name === name);
+ rules.splice(index, 1);
+ return of(undefined);
+ });
+ taskWrapper = TestBed.inject(TaskWrapperService);
+ spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
+ });
+
+ describe('with unused rule', () => {
+ beforeEach(() => {
+ callDeletionWithRuleByIndex(0);
+ });
+
+ it('should have called delete', () => {
+ expectSuccessfulDeletion('rep1');
+ });
+
+ it('should not open the tooltip nor the crush info', () => {
+ expect(component.crushDeletionBtn.isOpen()).toBe(false);
+ expect(component.data.crushInfo).toBe(false);
+ });
+
+ it('should reload the rules after deletion', () => {
+ const expected = infoReturn.crush_rules_replicated;
+ const currentRules = component.current.rules;
+ expect(currentRules.length).toBe(expected.length);
+ expect(currentRules).toEqual(expected);
+ });
+ });
+
+ describe('rule in use', () => {
+ beforeEach(() => {
+ spyOn(global, 'setTimeout').and.callFake((fn: Function) => fn());
+ deleteSpy.calls.reset();
+ selectRuleByIndex(2);
+ component.deleteCrushRule();
+ });
+
+ it('should not have called delete and opened the tooltip', () => {
+ expect(crushRuleService.delete).not.toHaveBeenCalled();
+ expect(component.crushDeletionBtn.isOpen()).toBe(true);
+ expect(component.data.crushInfo).toBe(true);
+ });
+
+ it('should hide the tooltip when clicking on delete again', () => {
+ component.deleteCrushRule();
+ expect(component.crushDeletionBtn.isOpen()).toBe(false);
+ });
+
+ it('should hide the tooltip when clicking on add', () => {
+ modalSpy.and.callFake((): any => ({
+ componentInstance: {
+ submitAction: of('someRule')
+ }
+ }));
+ component.addCrushRule();
+ expect(component.crushDeletionBtn.isOpen()).toBe(false);
+ });
+
+ it('should hide the tooltip when changing the crush rule', () => {
+ selectRuleByIndex(0);
+ expect(component.crushDeletionBtn.isOpen()).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe('erasure code profile', () => {
+ const setSelectedEcp = (name: string) => {
+ formHelper.setValue('erasureProfile', { name: name });
+ };
+
+ beforeEach(() => {
+ formHelper.setValue('poolType', 'erasure');
+ fixture.detectChanges();
+ });
+
+ it('should not show info per default', () => {
+ fixtureHelper.expectElementVisible('#erasureProfile', true);
+ fixtureHelper.expectElementVisible('#ecp-info-block', false);
+ });
+
+ it('should show info if the info button is clicked', () => {
+ const infoButton = fixture.debugElement.query(By.css('#ecp-info-button'));
+ infoButton.triggerEventHandler('click', null);
+ expect(component.data.erasureInfo).toBeTruthy();
+ fixture.detectChanges();
+ expect(infoButton.classes['active']).toBeTruthy();
+ fixtureHelper.expectIdElementsVisible(['erasureProfile', 'ecp-info-block'], true);
+ });
+
+ it('should select the newly created profile', () => {
+ spyOn(ecpService, 'list').and.callFake(() => of(infoReturn.erasure_code_profiles));
+ expect(form.getValue('erasureProfile').name).toBe('ecp1');
+ const name = 'awesomeProfile';
+ spyOn(TestBed.inject(ModalService), 'show').and.callFake(() => {
+ return {
+ componentInstance: {
+ submitAction: of({ name })
+ }
+ };
+ });
+ const ecp2 = new ErasureCodeProfile();
+ ecp2.name = name;
+ infoReturn.erasure_code_profiles.push(ecp2);
+ component.addErasureCodeProfile();
+ expect(form.getValue('erasureProfile').name).toBe(name);
+ });
+
+ describe('ecp deletion', () => {
+ let taskWrapper: TaskWrapperService;
+ let deletion: CriticalConfirmationModalComponent;
+ let deleteSpy: jasmine.Spy;
+ let modalSpy: jasmine.Spy;
+ let modal: NgbModalRef;
+
+ const callEcpDeletion = () => {
+ component.deleteErasureCodeProfile();
+ modal.componentInstance.callSubmitAction();
+ };
+
+ const expectSuccessfulEcpDeletion = (name: string) => {
+ setSelectedEcp(name);
+ callEcpDeletion();
+ expect(ecpService.delete).toHaveBeenCalledWith(name);
+ expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith(
+ expect.objectContaining({
+ task: {
+ name: 'ecp/delete',
+ metadata: {
+ name: name
+ }
+ }
+ })
+ );
+ };
+
+ beforeEach(() => {
+ deletion = undefined;
+ modalSpy = spyOn(TestBed.inject(ModalService), 'show').and.callFake(
+ (comp: any, init: any) => {
+ modal = modalServiceShow(comp, init);
+ return modal;
+ }
+ );
+ deleteSpy = spyOn(ecpService, 'delete').and.callFake((name: string) => {
+ const profiles = infoReturn.erasure_code_profiles;
+ const index = _.findIndex(profiles, (profile) => profile.name === name);
+ profiles.splice(index, 1);
+ return of({ status: 202 });
+ });
+ taskWrapper = TestBed.inject(TaskWrapperService);
+ spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
+
+ const ecp2 = new ErasureCodeProfile();
+ ecp2.name = 'someEcpName';
+ infoReturn.erasure_code_profiles.push(ecp2);
+
+ const ecp3 = new ErasureCodeProfile();
+ ecp3.name = 'aDifferentEcpName';
+ infoReturn.erasure_code_profiles.push(ecp3);
+ });
+
+ it('should delete two different erasure code profiles', () => {
+ expectSuccessfulEcpDeletion('someEcpName');
+ expectSuccessfulEcpDeletion('aDifferentEcpName');
+ });
+
+ describe('with unused profile', () => {
+ beforeEach(() => {
+ expectSuccessfulEcpDeletion('someEcpName');
+ });
+
+ it('should not open the tooltip nor the crush info', () => {
+ expect(component.ecpDeletionBtn.isOpen()).toBe(false);
+ expect(component.data.erasureInfo).toBe(false);
+ });
+
+ it('should reload the rules after deletion', () => {
+ const expected = infoReturn.erasure_code_profiles;
+ const currentProfiles = component.info.erasure_code_profiles;
+ expect(currentProfiles.length).toBe(expected.length);
+ expect(currentProfiles).toEqual(expected);
+ });
+ });
+
+ describe('rule in use', () => {
+ beforeEach(() => {
+ spyOn(global, 'setTimeout').and.callFake((fn: Function) => fn());
+ deleteSpy.calls.reset();
+ setSelectedEcp('ecp1');
+ component.deleteErasureCodeProfile();
+ });
+
+ it('should not open the modal', () => {
+ expect(deletion).toBe(undefined);
+ });
+
+ it('should not have called delete and opened the tooltip', () => {
+ expect(ecpService.delete).not.toHaveBeenCalled();
+ expect(component.ecpDeletionBtn.isOpen()).toBe(true);
+ expect(component.data.erasureInfo).toBe(true);
+ });
+
+ it('should hide the tooltip when clicking on delete again', () => {
+ component.deleteErasureCodeProfile();
+ expect(component.ecpDeletionBtn.isOpen()).toBe(false);
+ });
+
+ it('should hide the tooltip when clicking on add', () => {
+ modalSpy.and.callFake((): any => ({
+ componentInstance: {
+ submitAction: of('someProfile')
+ }
+ }));
+ component.addErasureCodeProfile();
+ expect(component.ecpDeletionBtn.isOpen()).toBe(false);
+ });
+
+ it('should hide the tooltip when changing the crush rule', () => {
+ setSelectedEcp('someEcpName');
+ expect(component.ecpDeletionBtn.isOpen()).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe('submit - create', () => {
+ const setMultipleValues = (settings: object) => {
+ Object.keys(settings).forEach((name) => {
+ formHelper.setValue(name, settings[name]);
+ });
+ };
+
+ describe('erasure coded pool', () => {
+ const expectEcSubmit = (o: any) =>
+ expectValidSubmit(
+ Object.assign(
+ {
+ pool: 'ecPool',
+ pool_type: 'erasure',
+ pg_autoscale_mode: 'off',
+ erasure_code_profile: 'ecp1',
+ pg_num: 4
+ },
+ o
+ )
+ );
+
+ beforeEach(() => {
+ setMultipleValues({
+ name: 'ecPool',
+ poolType: 'erasure',
+ pgNum: 4
+ });
+ });
+
+ it('minimum requirements without ECP to create ec pool', () => {
+ // Mock that no ec profiles exist
+ infoReturn.erasure_code_profiles = [];
+ setUpPoolComponent();
+ setMultipleValues({
+ name: 'minECPool',
+ poolType: 'erasure',
+ pgNum: 4
+ });
+ expectValidSubmit({
+ pool: 'minECPool',
+ pool_type: 'erasure',
+ pg_autoscale_mode: 'off',
+ pg_num: 4
+ });
+ });
+
+ it('creates ec pool with erasure coded profile', () => {
+ const ecp = { name: 'ecpMinimalMock' };
+ setMultipleValues({
+ erasureProfile: ecp
+ });
+ expectEcSubmit({
+ erasure_code_profile: ecp.name
+ });
+ });
+
+ it('creates ec pool with ec_overwrite flag', () => {
+ setMultipleValues({
+ ecOverwrites: true
+ });
+ expectEcSubmit({
+ flags: ['ec_overwrites']
+ });
+ });
+
+ it('should ignore replicated set settings for ec pools', () => {
+ setMultipleValues({
+ size: 2 // will be ignored
+ });
+ expectEcSubmit({});
+ });
+
+ it('creates a pool with compression', () => {
+ setMultipleValues({
+ mode: 'passive',
+ algorithm: 'lz4',
+ minBlobSize: '4 K',
+ maxBlobSize: '4 M',
+ ratio: 0.7
+ });
+ expectEcSubmit({
+ compression_mode: 'passive',
+ compression_algorithm: 'lz4',
+ compression_min_blob_size: 4096,
+ compression_max_blob_size: 4194304,
+ compression_required_ratio: 0.7
+ });
+ });
+
+ it('creates a pool with application metadata', () => {
+ component.data.applications.selected = ['cephfs', 'rgw'];
+ expectEcSubmit({
+ application_metadata: ['cephfs', 'rgw']
+ });
+ });
+ });
+
+ describe('with replicated pool', () => {
+ const expectReplicatedSubmit = (o: any) =>
+ expectValidSubmit(
+ Object.assign(
+ {
+ pool: 'repPool',
+ pool_type: 'replicated',
+ pg_autoscale_mode: 'off',
+ pg_num: 16,
+ rule_name: 'rep1',
+ size: 3
+ },
+ o
+ )
+ );
+ beforeEach(() => {
+ setMultipleValues({
+ name: 'repPool',
+ poolType: 'replicated',
+ crushRule: infoReturn.crush_rules_replicated[0],
+ size: 3,
+ pgNum: 16
+ });
+ });
+
+ it('uses the minimum requirements for replicated pools', () => {
+ // Mock that no replicated rules exist
+ infoReturn.crush_rules_replicated = [];
+ setUpPoolComponent();
+
+ setMultipleValues({
+ name: 'minRepPool',
+ poolType: 'replicated',
+ size: 2,
+ pgNum: 32
+ });
+ expectValidSubmit({
+ pool: 'minRepPool',
+ pool_type: 'replicated',
+ pg_num: 32,
+ pg_autoscale_mode: 'off',
+ size: 2
+ });
+ });
+
+ it('ignores erasure only set settings for replicated pools', () => {
+ setMultipleValues({
+ erasureProfile: { name: 'ecpMinimalMock' }, // Will be ignored
+ ecOverwrites: true // Will be ignored
+ });
+ /**
+ * As pgCalc is triggered through profile changes, which is normally not possible,
+ * if type `replicated` is set, pgNum will be set to 256 with the current rule for
+ * a replicated pool.
+ */
+ expectReplicatedSubmit({
+ pg_num: 256
+ });
+ });
+
+ it('creates a pool with quotas', () => {
+ setMultipleValues({
+ max_bytes: 1024 * 1024,
+ max_objects: 3000
+ });
+ expectReplicatedSubmit({
+ quota_max_bytes: 1024 * 1024,
+ quota_max_objects: 3000
+ });
+ });
+
+ it('creates a pool with rbd qos settings', () => {
+ component.currentConfigurationValues = {
+ rbd_qos_bps_limit: 55
+ };
+ expectReplicatedSubmit({
+ configuration: {
+ rbd_qos_bps_limit: 55
+ }
+ });
+ });
+ });
+ });
+
+ describe('edit mode', () => {
+ const setUrl = (url: string) => {
+ Object.defineProperty(router, 'url', { value: url });
+ setUpPoolComponent(); // Renew of component needed because the constructor has to be called
+ };
+
+ let pool: Pool;
+ beforeEach(() => {
+ pool = new Pool('somePoolName');
+ pool.type = 'replicated';
+ pool.size = 3;
+ pool.crush_rule = 'rep1';
+ pool.pg_num = 32;
+ pool.options = {};
+ pool.options.compression_mode = 'passive';
+ pool.options.compression_algorithm = 'lz4';
+ pool.options.compression_min_blob_size = 1024 * 512;
+ pool.options.compression_max_blob_size = 1024 * 1024;
+ pool.options.compression_required_ratio = 0.8;
+ pool.flags_names = 'someFlag1,someFlag2';
+ pool.application_metadata = ['rbd', 'ownApp'];
+ pool.quota_max_bytes = 1024 * 1024 * 1024;
+ pool.quota_max_objects = 3000;
+
+ Mocks.getCrushRule({ name: 'someRule' });
+ spyOn(poolService, 'get').and.callFake(() => of(pool));
+ });
+
+ it('is not in edit mode if edit is not included in url', () => {
+ setUrl('/pool/add');
+ expect(component.editing).toBeFalsy();
+ });
+
+ it('is in edit mode if edit is included in url', () => {
+ setUrl('/pool/edit/somePoolName');
+ expect(component.editing).toBeTruthy();
+ });
+
+ describe('after ngOnInit', () => {
+ beforeEach(() => {
+ setUrl('/pool/edit/somePoolName');
+ fixture.detectChanges();
+ });
+
+ it('disabled inputs', () => {
+ fixture.detectChanges();
+ const disabled = ['poolType', 'crushRule', 'size', 'erasureProfile', 'ecOverwrites'];
+ disabled.forEach((controlName) => {
+ return expect(form.get(controlName).disabled).toBeTruthy();
+ });
+ const enabled = [
+ 'name',
+ 'pgNum',
+ 'mode',
+ 'algorithm',
+ 'minBlobSize',
+ 'maxBlobSize',
+ 'ratio',
+ 'max_bytes',
+ 'max_objects'
+ ];
+ enabled.forEach((controlName) => {
+ return expect(form.get(controlName).enabled).toBeTruthy();
+ });
+ });
+
+ it('should include the custom app as valid option', () => {
+ expect(
+ component.data.applications.available.map((app: Record<string, any>) => app.name)
+ ).toEqual(['cephfs', 'ownApp', 'rbd', 'rgw']);
+ });
+
+ it('set all control values to the given pool', () => {
+ expect(form.getValue('name')).toBe(pool.pool_name);
+ expect(form.getValue('poolType')).toBe(pool.type);
+ expect(form.getValue('crushRule')).toEqual(component.info.crush_rules_replicated[0]);
+ expect(form.getValue('size')).toBe(pool.size);
+ expect(form.getValue('pgNum')).toBe(pool.pg_num);
+ expect(form.getValue('mode')).toBe(pool.options.compression_mode);
+ expect(form.getValue('algorithm')).toBe(pool.options.compression_algorithm);
+ expect(form.getValue('minBlobSize')).toBe('512 KiB');
+ expect(form.getValue('maxBlobSize')).toBe('1 MiB');
+ expect(form.getValue('ratio')).toBe(pool.options.compression_required_ratio);
+ expect(form.getValue('max_bytes')).toBe('1 GiB');
+ expect(form.getValue('max_objects')).toBe(pool.quota_max_objects);
+ });
+
+ it('updates pgs on every change', () => {
+ testPgUpdate(undefined, -1, 16);
+ testPgUpdate(undefined, -1, 8);
+ });
+
+ it('is possible to use less or more pgs than before', () => {
+ formHelper.expectValid(setPgNum(64));
+ formHelper.expectValid(setPgNum(4));
+ });
+
+ describe('submit', () => {
+ const markControlAsPreviouslySet = (controlName: string) =>
+ form.get(controlName).markAsPristine();
+
+ beforeEach(() => {
+ [
+ 'algorithm',
+ 'maxBlobSize',
+ 'minBlobSize',
+ 'mode',
+ 'pgNum',
+ 'ratio',
+ 'name'
+ ].forEach((name) => markControlAsPreviouslySet(name));
+ fixture.detectChanges();
+ });
+
+ it(`always provides the application metadata array with submit even if it's empty`, () => {
+ expect(form.get('mode').dirty).toBe(false);
+ component.data.applications.selected = [];
+ expectValidSubmit(
+ {
+ application_metadata: [],
+ pool: 'somePoolName'
+ },
+ 'pool/edit',
+ 'update'
+ );
+ });
+
+ it(`will always provide reset value for compression options`, () => {
+ formHelper.setValue('minBlobSize', '').markAsDirty();
+ formHelper.setValue('maxBlobSize', '').markAsDirty();
+ formHelper.setValue('ratio', '').markAsDirty();
+ expectValidSubmit(
+ {
+ application_metadata: ['ownApp', 'rbd'],
+ compression_max_blob_size: 0,
+ compression_min_blob_size: 0,
+ compression_required_ratio: 0,
+ pool: 'somePoolName'
+ },
+ 'pool/edit',
+ 'update'
+ );
+ });
+
+ it(`will unset mode not used anymore`, () => {
+ formHelper.setValue('mode', 'none').markAsDirty();
+ expectValidSubmit(
+ {
+ application_metadata: ['ownApp', 'rbd'],
+ compression_mode: 'unset',
+ pool: 'somePoolName'
+ },
+ 'pool/edit',
+ 'update'
+ );
+ });
+ });
+ });
+ });
+
+ describe('test pool configuration component', () => {
+ it('is visible for replicated pools with rbd application', () => {
+ const poolType = component.form.get('poolType');
+ poolType.markAsDirty();
+ poolType.setValue('replicated');
+ component.data.applications.selected = ['rbd'];
+ fixture.detectChanges();
+ expect(
+ fixture.debugElement.query(By.css('cd-rbd-configuration-form')).nativeElement.parentElement
+ .hidden
+ ).toBe(false);
+ });
+
+ it('is invisible for erasure coded pools', () => {
+ const poolType = component.form.get('poolType');
+ poolType.markAsDirty();
+ poolType.setValue('erasure');
+ fixture.detectChanges();
+ expect(
+ fixture.debugElement.query(By.css('cd-rbd-configuration-form')).nativeElement.parentElement
+ .hidden
+ ).toBe(true);
+ });
+ });
+});