summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts
diff options
context:
space:
mode:
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.ts518
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();
+ });
+ });
+});