diff options
Diffstat (limited to 'src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts')
-rw-r--r-- | src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts | 518 |
1 files changed, 518 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts new file mode 100644 index 000000000..8a8af7b73 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts @@ -0,0 +1,518 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; +import _ from 'lodash'; +import { ToastrModule } from 'ngx-toastr'; +import { of } from 'rxjs'; + +import { RbdConfigurationListComponent } from '~/app/ceph/block/rbd-configuration-list/rbd-configuration-list.component'; +import { PgCategoryService } from '~/app/ceph/shared/pg-category.service'; +import { ConfigurationService } from '~/app/shared/api/configuration.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 { ErasureCodeProfile } from '~/app/shared/models/erasure-code-profile'; +import { ExecutingTask } from '~/app/shared/models/executing-task'; +import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { ModalService } from '~/app/shared/services/modal.service'; +import { SummaryService } from '~/app/shared/services/summary.service'; +import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; +import { SharedModule } from '~/app/shared/shared.module'; +import { configureTestBed, expectItemTasks, Mocks } from '~/testing/unit-test-helper'; +import { Pool } from '../pool'; +import { PoolDetailsComponent } from '../pool-details/pool-details.component'; +import { PoolListComponent } from './pool-list.component'; + +describe('PoolListComponent', () => { + let component: PoolListComponent; + let fixture: ComponentFixture<PoolListComponent>; + let poolService: PoolService; + let getECPList: jasmine.Spy; + + const getPoolList = (): Pool[] => { + return [Mocks.getPool('a', 0), Mocks.getPool('b', 1), Mocks.getPool('c', 2)]; + }; + const getECPProfiles = (): ErasureCodeProfile[] => { + const ecpProfile = new ErasureCodeProfile(); + ecpProfile.name = 'default'; + ecpProfile.k = 2; + ecpProfile.m = 1; + + return [ecpProfile]; + }; + + configureTestBed({ + declarations: [PoolListComponent, PoolDetailsComponent, RbdConfigurationListComponent], + imports: [ + BrowserAnimationsModule, + SharedModule, + ToastrModule.forRoot(), + RouterTestingModule, + NgbNavModule, + HttpClientTestingModule + ], + providers: [PgCategoryService] + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PoolListComponent); + component = fixture.componentInstance; + component.permissions.pool.read = true; + poolService = TestBed.inject(PoolService); + spyOn(poolService, 'getList').and.callFake(() => of(getPoolList())); + getECPList = spyOn(TestBed.inject(ErasureCodeProfileService), 'list'); + getECPList.and.returnValue(of(getECPProfiles())); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have columns that are sortable', () => { + expect( + component.columns + .filter((column) => !(column.prop === undefined)) + .every((column) => Boolean(column.prop)) + ).toBeTruthy(); + }); + + describe('monAllowPoolDelete', () => { + let configOptRead: boolean; + let configurationService: ConfigurationService; + + beforeEach(() => { + configOptRead = true; + spyOn(TestBed.inject(AuthStorageService), 'getPermissions').and.callFake(() => ({ + configOpt: { read: configOptRead } + })); + configurationService = TestBed.inject(ConfigurationService); + }); + + it('should set value correctly if mon_allow_pool_delete flag is set to true', () => { + const configOption = { + name: 'mon_allow_pool_delete', + value: [ + { + section: 'mon', + value: 'true' + } + ] + }; + spyOn(configurationService, 'get').and.returnValue(of(configOption)); + fixture = TestBed.createComponent(PoolListComponent); + component = fixture.componentInstance; + expect(component.monAllowPoolDelete).toBe(true); + }); + + it('should set value correctly if mon_allow_pool_delete flag is set to false', () => { + const configOption = { + name: 'mon_allow_pool_delete', + value: [ + { + section: 'mon', + value: 'false' + } + ] + }; + spyOn(configurationService, 'get').and.returnValue(of(configOption)); + fixture = TestBed.createComponent(PoolListComponent); + component = fixture.componentInstance; + expect(component.monAllowPoolDelete).toBe(false); + }); + + it('should set value correctly if mon_allow_pool_delete flag is not set', () => { + const configOption = { + name: 'mon_allow_pool_delete' + }; + spyOn(configurationService, 'get').and.returnValue(of(configOption)); + fixture = TestBed.createComponent(PoolListComponent); + component = fixture.componentInstance; + expect(component.monAllowPoolDelete).toBe(false); + }); + + it('should set value correctly w/o config-opt read privileges', () => { + configOptRead = false; + fixture = TestBed.createComponent(PoolListComponent); + component = fixture.componentInstance; + expect(component.monAllowPoolDelete).toBe(false); + }); + }); + + describe('pool deletion', () => { + let taskWrapper: TaskWrapperService; + let modalRef: any; + + const setSelectedPool = (poolName: string) => + (component.selection.selected = [{ pool_name: poolName }]); + + const callDeletion = () => { + component.deletePoolModal(); + expect(modalRef).toBeTruthy(); + const deletion: CriticalConfirmationModalComponent = modalRef && modalRef.componentInstance; + deletion.submitActionObservable(); + }; + + const testPoolDeletion = (poolName: string) => { + setSelectedPool(poolName); + callDeletion(); + expect(poolService.delete).toHaveBeenCalledWith(poolName); + expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith({ + task: { + name: 'pool/delete', + metadata: { + pool_name: poolName + } + }, + call: undefined // because of stub + }); + }; + + beforeEach(() => { + spyOn(TestBed.inject(ModalService), 'show').and.callFake((deletionClass, initialState) => { + modalRef = { + componentInstance: Object.assign(new deletionClass(), initialState) + }; + return modalRef; + }); + spyOn(poolService, 'delete').and.stub(); + taskWrapper = TestBed.inject(TaskWrapperService); + spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough(); + }); + + it('should pool deletion with two different pools', () => { + testPoolDeletion('somePoolName'); + testPoolDeletion('aDifferentPoolName'); + }); + }); + + describe('handling of executing tasks', () => { + let summaryService: SummaryService; + + const addTask = (name: string, pool: string) => { + const task = new ExecutingTask(); + task.name = name; + task.metadata = { pool_name: pool }; + summaryService.addRunningTask(task); + }; + + beforeEach(() => { + summaryService = TestBed.inject(SummaryService); + summaryService['summaryDataSource'].next({ + executing_tasks: [], + finished_tasks: [] + }); + }); + + it('gets all pools without executing pools', () => { + expect(component.pools.length).toBe(3); + expect(component.pools.every((pool) => !pool.executingTasks)).toBeTruthy(); + }); + + it('gets a pool from a task during creation', () => { + addTask('pool/create', 'd'); + expect(component.pools.length).toBe(4); + expectItemTasks(component.pools[3], 'Creating'); + }); + + it('gets all pools with one executing pools', () => { + addTask('pool/create', 'a'); + expect(component.pools.length).toBe(3); + expectItemTasks(component.pools[0], 'Creating'); + expect(component.pools[1].cdExecuting).toBeFalsy(); + expect(component.pools[2].cdExecuting).toBeFalsy(); + }); + + it('gets all pools with multiple executing pools', () => { + addTask('pool/create', 'a'); + addTask('pool/edit', 'a'); + addTask('pool/delete', 'a'); + addTask('pool/edit', 'b'); + addTask('pool/delete', 'b'); + addTask('pool/delete', 'c'); + expect(component.pools.length).toBe(3); + expectItemTasks(component.pools[0], 'Creating..., Updating..., Deleting'); + expectItemTasks(component.pools[1], 'Updating..., Deleting'); + expectItemTasks(component.pools[2], 'Deleting'); + }); + + it('gets all pools with multiple executing tasks (not only pool tasks)', () => { + addTask('rbd/create', 'a'); + addTask('rbd/edit', 'a'); + addTask('pool/delete', 'a'); + addTask('pool/edit', 'b'); + addTask('rbd/delete', 'b'); + addTask('rbd/delete', 'c'); + expect(component.pools.length).toBe(3); + expectItemTasks(component.pools[0], 'Deleting'); + expectItemTasks(component.pools[1], 'Updating'); + expect(component.pools[2].cdExecuting).toBeFalsy(); + }); + }); + + describe('getPgStatusCellClass', () => { + const testMethod = (value: string, expected: string) => + expect(component.getPgStatusCellClass('', '', value)).toEqual({ + 'text-right': true, + [expected]: true + }); + + it('pg-clean', () => { + testMethod('8 active+clean', 'pg-clean'); + }); + + it('pg-working', () => { + testMethod(' 8 active+clean+scrubbing+deep, 255 active+clean ', 'pg-working'); + }); + + it('pg-warning', () => { + testMethod('8 active+clean+scrubbing+down', 'pg-warning'); + testMethod('8 active+clean+scrubbing+down+nonMappedState', 'pg-warning'); + }); + + it('pg-unknown', () => { + testMethod('8 active+clean+scrubbing+nonMappedState', 'pg-unknown'); + testMethod('8 ', 'pg-unknown'); + testMethod('', 'pg-unknown'); + }); + }); + + describe('custom row comparators', () => { + const expectCorrectComparator = (statsAttribute: string) => { + const mockPool = (v: number) => ({ stats: { [statsAttribute]: { latest: v } } }); + const columnDefinition = _.find( + component.columns, + (column) => column.prop === `stats.${statsAttribute}.rates` + ); + expect(columnDefinition.comparator(undefined, undefined, mockPool(2), mockPool(1))).toBe(1); + expect(columnDefinition.comparator(undefined, undefined, mockPool(1), mockPool(2))).toBe(-1); + }; + + it('compares read bytes correctly', () => { + expectCorrectComparator('rd_bytes'); + }); + + it('compares write bytes correctly', () => { + expectCorrectComparator('wr_bytes'); + }); + }); + + describe('transformPoolsData', () => { + let pool: Pool; + + const getPoolData = (o: object) => [ + _.merge( + _.merge(Mocks.getPool('a', 0), { + cdIsBinary: true, + pg_status: '', + stats: { + bytes_used: { latest: 0, rate: 0, rates: [] }, + max_avail: { latest: 0, rate: 0, rates: [] }, + avail_raw: { latest: 0, rate: 0, rates: [] }, + percent_used: { latest: 0, rate: 0, rates: [] }, + rd: { latest: 0, rate: 0, rates: [] }, + rd_bytes: { latest: 0, rate: 0, rates: [] }, + wr: { latest: 0, rate: 0, rates: [] }, + wr_bytes: { latest: 0, rate: 0, rates: [] } + }, + usage: 0, + data_protection: 'replica: ×3' + }), + o + ) + ]; + + beforeEach(() => { + pool = Mocks.getPool('a', 0); + }); + + it('transforms replicated pools data correctly', () => { + pool = _.merge(pool, { + stats: { + bytes_used: { latest: 5, rate: 0, rates: [] }, + avail_raw: { latest: 15, rate: 0, rates: [] }, + percent_used: { latest: 0.25, rate: 0, rates: [] }, + rd_bytes: { + latest: 6, + rate: 4, + rates: [ + [0, 2], + [1, 6] + ] + } + }, + pg_status: { 'active+clean': 8, down: 2 } + }); + expect(component.transformPoolsData([pool])).toEqual( + getPoolData({ + pg_status: '8 active+clean, 2 down', + stats: { + bytes_used: { latest: 5, rate: 0, rates: [] }, + avail_raw: { latest: 15, rate: 0, rates: [] }, + percent_used: { latest: 0.25, rate: 0, rates: [] }, + rd_bytes: { latest: 6, rate: 4, rates: [2, 6] } + }, + usage: 0.25, + data_protection: 'replica: ×3' + }) + ); + }); + + it('transforms erasure pools data correctly', () => { + pool.type = 'erasure'; + pool.erasure_code_profile = 'default'; + component.ecProfileList = getECPProfiles(); + + expect(component.transformPoolsData([pool])).toEqual( + getPoolData({ + type: 'erasure', + erasure_code_profile: 'default', + data_protection: 'EC: 2+1' + }) + ); + }); + + it('transforms pools data correctly if stats are missing', () => { + expect(component.transformPoolsData([pool])).toEqual(getPoolData({})); + }); + + it('transforms empty pools data correctly', () => { + expect(component.transformPoolsData(undefined)).toEqual(undefined); + expect(component.transformPoolsData([])).toEqual([]); + }); + + it('shows not marked pools in progress if pg_num does not match pg_num_target', () => { + const pools = [ + _.merge(pool, { + pg_num: 32, + pg_num_target: 16, + pg_placement_num: 32, + pg_placement_num_target: 16 + }) + ]; + expect(component.transformPoolsData(pools)).toEqual( + getPoolData({ + cdExecuting: 'Updating', + pg_num: 32, + pg_num_target: 16, + pg_placement_num: 32, + pg_placement_num_target: 16, + data_protection: 'replica: ×3' + }) + ); + }); + + it('shows marked pools in progress as defined by task', () => { + const pools = [ + _.merge(pool, { + pg_num: 32, + pg_num_target: 16, + pg_placement_num: 32, + pg_placement_num_target: 16, + cdExecuting: 'Updating... 50%' + }) + ]; + expect(component.transformPoolsData(pools)).toEqual( + getPoolData({ + cdExecuting: 'Updating... 50%', + pg_num: 32, + pg_num_target: 16, + pg_placement_num: 32, + pg_placement_num_target: 16, + data_protection: 'replica: ×3' + }) + ); + }); + }); + + describe('transformPgStatus', () => { + it('returns status groups correctly', () => { + const pgStatus = { 'active+clean': 8 }; + const expected = '8 active+clean'; + + expect(component.transformPgStatus(pgStatus)).toEqual(expected); + }); + + it('returns separated status groups', () => { + const pgStatus = { 'active+clean': 8, down: 2 }; + const expected = '8 active+clean, 2 down'; + + expect(component.transformPgStatus(pgStatus)).toEqual(expected); + }); + + it('returns separated statuses correctly', () => { + const pgStatus = { active: 8, down: 2 }; + const expected = '8 active, 2 down'; + + expect(component.transformPgStatus(pgStatus)).toEqual(expected); + }); + + it('returns empty string', () => { + const pgStatus: any = undefined; + const expected = ''; + + expect(component.transformPgStatus(pgStatus)).toEqual(expected); + }); + }); + + describe('getSelectionTiers', () => { + const setSelectionTiers = (tiers: number[]) => { + component.expandedRow = { tiers }; + component.getSelectionTiers(); + }; + + beforeEach(() => { + component.pools = getPoolList(); + }); + + it('should select multiple existing cache tiers', () => { + setSelectionTiers([0, 1, 2]); + expect(component.cacheTiers).toEqual(getPoolList()); + }); + + it('should select correct existing cache tier', () => { + setSelectionTiers([0]); + expect(component.cacheTiers).toEqual([Mocks.getPool('a', 0)]); + }); + + it('should not select cache tier if id is invalid', () => { + setSelectionTiers([-1]); + expect(component.cacheTiers).toEqual([]); + }); + + it('should not select cache tier if empty', () => { + setSelectionTiers([]); + expect(component.cacheTiers).toEqual([]); + }); + + it('should be able to selected one pool with multiple tiers, than with a single tier, than with no tiers', () => { + setSelectionTiers([0, 1, 2]); + expect(component.cacheTiers).toEqual(getPoolList()); + setSelectionTiers([0]); + expect(component.cacheTiers).toEqual([Mocks.getPool('a', 0)]); + setSelectionTiers([]); + expect(component.cacheTiers).toEqual([]); + }); + }); + + describe('getDisableDesc', () => { + beforeEach(() => { + component.selection.selected = [{ pool_name: 'foo' }]; + }); + + it('should return message if mon_allow_pool_delete flag is set to false', () => { + component.monAllowPoolDelete = false; + expect(component.getDisableDesc()).toBe( + 'Pool deletion is disabled by the mon_allow_pool_delete configuration setting.' + ); + }); + + it('should return false if mon_allow_pool_delete flag is set to true', () => { + component.monAllowPoolDelete = true; + expect(component.getDisableDesc()).toBeFalsy(); + }); + }); +}); |