]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/pool/pool-list/pool-list.component.spec.ts
1650421002c7f7ff2c8bf8afa965bd0e46e28623
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / pool / pool-list / pool-list.component.spec.ts
1 import { HttpClientTestingModule } from '@angular/common/http/testing';
2 import { ComponentFixture, TestBed } from '@angular/core/testing';
3 import { RouterTestingModule } from '@angular/router/testing';
4
5 import * as _ from 'lodash';
6 import { BsModalService } from 'ngx-bootstrap/modal';
7 import { TabsModule } from 'ngx-bootstrap/tabs';
8 import { ToastrModule } from 'ngx-toastr';
9 import { of } from 'rxjs';
10
11 import {
12 configureTestBed,
13 expectItemTasks,
14 i18nProviders
15 } from '../../../../testing/unit-test-helper';
16 import { ConfigurationService } from '../../../shared/api/configuration.service';
17 import { PoolService } from '../../../shared/api/pool.service';
18 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
19 import { ExecutingTask } from '../../../shared/models/executing-task';
20 import { AuthStorageService } from '../../../shared/services/auth-storage.service';
21 import { SummaryService } from '../../../shared/services/summary.service';
22 import { TaskWrapperService } from '../../../shared/services/task-wrapper.service';
23 import { SharedModule } from '../../../shared/shared.module';
24 import { RbdConfigurationListComponent } from '../../block/rbd-configuration-list/rbd-configuration-list.component';
25 import { PgCategoryService } from '../../shared/pg-category.service';
26 import { Pool } from '../pool';
27 import { PoolDetailsComponent } from '../pool-details/pool-details.component';
28 import { PoolListComponent } from './pool-list.component';
29
30 describe('PoolListComponent', () => {
31 let component: PoolListComponent;
32 let fixture: ComponentFixture<PoolListComponent>;
33 let poolService: PoolService;
34
35 const createPool = (name: string, id: number): Pool => {
36 return _.merge(new Pool(name), {
37 pool: id,
38 pg_num: 256,
39 pg_placement_num: 256,
40 pg_num_target: 256,
41 pg_placement_num_target: 256
42 });
43 };
44
45 const getPoolList = (): Pool[] => {
46 return [createPool('a', 0), createPool('b', 1), createPool('c', 2)];
47 };
48
49 configureTestBed({
50 declarations: [PoolListComponent, PoolDetailsComponent, RbdConfigurationListComponent],
51 imports: [
52 SharedModule,
53 ToastrModule.forRoot(),
54 RouterTestingModule,
55 TabsModule.forRoot(),
56 HttpClientTestingModule
57 ],
58 providers: [i18nProviders, PgCategoryService]
59 });
60
61 beforeEach(() => {
62 fixture = TestBed.createComponent(PoolListComponent);
63 component = fixture.componentInstance;
64 component.permissions.pool.read = true;
65 poolService = TestBed.get(PoolService);
66 spyOn(poolService, 'getList').and.callFake(() => of(getPoolList()));
67 fixture.detectChanges();
68 });
69
70 it('should create', () => {
71 expect(component).toBeTruthy();
72 });
73
74 it('should have columns that are sortable', () => {
75 expect(component.columns.every((column) => Boolean(column.prop))).toBeTruthy();
76 });
77
78 describe('monAllowPoolDelete', () => {
79 let configOptRead: boolean;
80 let configurationService: ConfigurationService;
81
82 beforeEach(() => {
83 configOptRead = true;
84 spyOn(TestBed.get(AuthStorageService), 'getPermissions').and.callFake(() => ({
85 configOpt: { read: configOptRead }
86 }));
87 configurationService = TestBed.get(ConfigurationService);
88 });
89
90 it('should set value correctly if mon_allow_pool_delete flag is set to true', () => {
91 const configOption = {
92 name: 'mon_allow_pool_delete',
93 value: [
94 {
95 section: 'mon',
96 value: 'true'
97 }
98 ]
99 };
100 spyOn(configurationService, 'get').and.returnValue(of(configOption));
101 fixture = TestBed.createComponent(PoolListComponent);
102 component = fixture.componentInstance;
103 expect(component.monAllowPoolDelete).toBe(true);
104 });
105
106 it('should set value correctly if mon_allow_pool_delete flag is set to false', () => {
107 const configOption = {
108 name: 'mon_allow_pool_delete',
109 value: [
110 {
111 section: 'mon',
112 value: 'false'
113 }
114 ]
115 };
116 spyOn(configurationService, 'get').and.returnValue(of(configOption));
117 fixture = TestBed.createComponent(PoolListComponent);
118 component = fixture.componentInstance;
119 expect(component.monAllowPoolDelete).toBe(false);
120 });
121
122 it('should set value correctly if mon_allow_pool_delete flag is not set', () => {
123 const configOption = {
124 name: 'mon_allow_pool_delete'
125 };
126 spyOn(configurationService, 'get').and.returnValue(of(configOption));
127 fixture = TestBed.createComponent(PoolListComponent);
128 component = fixture.componentInstance;
129 expect(component.monAllowPoolDelete).toBe(false);
130 });
131
132 it('should set value correctly w/o config-opt read privileges', () => {
133 configOptRead = false;
134 fixture = TestBed.createComponent(PoolListComponent);
135 component = fixture.componentInstance;
136 expect(component.monAllowPoolDelete).toBe(false);
137 });
138 });
139
140 describe('pool deletion', () => {
141 let taskWrapper: TaskWrapperService;
142
143 const setSelectedPool = (poolName: string) =>
144 (component.selection.selected = [{ pool_name: poolName }]);
145
146 const callDeletion = () => {
147 component.deletePoolModal();
148 const deletion: CriticalConfirmationModalComponent = component.modalRef.content;
149 deletion.submitActionObservable();
150 };
151
152 const testPoolDeletion = (poolName: string) => {
153 setSelectedPool(poolName);
154 callDeletion();
155 expect(poolService.delete).toHaveBeenCalledWith(poolName);
156 expect(taskWrapper.wrapTaskAroundCall).toHaveBeenCalledWith({
157 task: {
158 name: 'pool/delete',
159 metadata: {
160 pool_name: poolName
161 }
162 },
163 call: undefined // because of stub
164 });
165 };
166
167 beforeEach(() => {
168 spyOn(TestBed.get(BsModalService), 'show').and.callFake((deletionClass, config) => {
169 return {
170 content: Object.assign(new deletionClass(), config.initialState)
171 };
172 });
173 spyOn(poolService, 'delete').and.stub();
174 taskWrapper = TestBed.get(TaskWrapperService);
175 spyOn(taskWrapper, 'wrapTaskAroundCall').and.callThrough();
176 });
177
178 it('should pool deletion with two different pools', () => {
179 testPoolDeletion('somePoolName');
180 testPoolDeletion('aDifferentPoolName');
181 });
182 });
183
184 describe('handling of executing tasks', () => {
185 let summaryService: SummaryService;
186
187 const addTask = (name: string, pool: string) => {
188 const task = new ExecutingTask();
189 task.name = name;
190 task.metadata = { pool_name: pool };
191 summaryService.addRunningTask(task);
192 };
193
194 beforeEach(() => {
195 summaryService = TestBed.get(SummaryService);
196 summaryService['summaryDataSource'].next({ executing_tasks: [], finished_tasks: [] });
197 });
198
199 it('gets all pools without executing pools', () => {
200 expect(component.pools.length).toBe(3);
201 expect(component.pools.every((pool) => !pool.executingTasks)).toBeTruthy();
202 });
203
204 it('gets a pool from a task during creation', () => {
205 addTask('pool/create', 'd');
206 expect(component.pools.length).toBe(4);
207 expectItemTasks(component.pools[3], 'Creating');
208 });
209
210 it('gets all pools with one executing pools', () => {
211 addTask('pool/create', 'a');
212 expect(component.pools.length).toBe(3);
213 expectItemTasks(component.pools[0], 'Creating');
214 expect(component.pools[1].cdExecuting).toBeFalsy();
215 expect(component.pools[2].cdExecuting).toBeFalsy();
216 });
217
218 it('gets all pools with multiple executing pools', () => {
219 addTask('pool/create', 'a');
220 addTask('pool/edit', 'a');
221 addTask('pool/delete', 'a');
222 addTask('pool/edit', 'b');
223 addTask('pool/delete', 'b');
224 addTask('pool/delete', 'c');
225 expect(component.pools.length).toBe(3);
226 expectItemTasks(component.pools[0], 'Creating..., Updating..., Deleting');
227 expectItemTasks(component.pools[1], 'Updating..., Deleting');
228 expectItemTasks(component.pools[2], 'Deleting');
229 });
230
231 it('gets all pools with multiple executing tasks (not only pool tasks)', () => {
232 addTask('rbd/create', 'a');
233 addTask('rbd/edit', 'a');
234 addTask('pool/delete', 'a');
235 addTask('pool/edit', 'b');
236 addTask('rbd/delete', 'b');
237 addTask('rbd/delete', 'c');
238 expect(component.pools.length).toBe(3);
239 expectItemTasks(component.pools[0], 'Deleting');
240 expectItemTasks(component.pools[1], 'Updating');
241 expect(component.pools[2].cdExecuting).toBeFalsy();
242 });
243 });
244
245 describe('getPgStatusCellClass', () => {
246 const testMethod = (value: string, expected: string) =>
247 expect(component.getPgStatusCellClass('', '', value)).toEqual({
248 'text-right': true,
249 [expected]: true
250 });
251
252 it('pg-clean', () => {
253 testMethod('8 active+clean', 'pg-clean');
254 });
255
256 it('pg-working', () => {
257 testMethod(' 8 active+clean+scrubbing+deep, 255 active+clean ', 'pg-working');
258 });
259
260 it('pg-warning', () => {
261 testMethod('8 active+clean+scrubbing+down', 'pg-warning');
262 testMethod('8 active+clean+scrubbing+down+nonMappedState', 'pg-warning');
263 });
264
265 it('pg-unknown', () => {
266 testMethod('8 active+clean+scrubbing+nonMappedState', 'pg-unknown');
267 testMethod('8 ', 'pg-unknown');
268 testMethod('', 'pg-unknown');
269 });
270 });
271
272 describe('custom row comparators', () => {
273 const expectCorrectComparator = (statsAttribute: string) => {
274 const mockPool = (v: number) => ({ stats: { [statsAttribute]: { latest: v } } });
275 const columnDefinition = _.find(
276 component.columns,
277 (column) => column.prop === `stats.${statsAttribute}.rates`
278 );
279 expect(columnDefinition.comparator(undefined, undefined, mockPool(2), mockPool(1))).toBe(1);
280 expect(columnDefinition.comparator(undefined, undefined, mockPool(1), mockPool(2))).toBe(-1);
281 };
282
283 it('compares read bytes correctly', () => {
284 expectCorrectComparator('rd_bytes');
285 });
286
287 it('compares write bytes correctly', () => {
288 expectCorrectComparator('wr_bytes');
289 });
290 });
291
292 describe('transformPoolsData', () => {
293 let pool: Pool;
294
295 const getPoolData = (o: object) => [
296 _.merge(
297 _.merge(createPool('a', 0), {
298 cdIsBinary: true,
299 pg_status: '',
300 stats: {
301 bytes_used: { latest: 0, rate: 0, rates: [] },
302 max_avail: { latest: 0, rate: 0, rates: [] },
303 rd: { latest: 0, rate: 0, rates: [] },
304 rd_bytes: { latest: 0, rate: 0, rates: [] },
305 wr: { latest: 0, rate: 0, rates: [] },
306 wr_bytes: { latest: 0, rate: 0, rates: [] }
307 },
308 usage: 0
309 }),
310 o
311 )
312 ];
313
314 beforeEach(() => {
315 pool = createPool('a', 0);
316 });
317
318 it('transforms pools data correctly', () => {
319 pool = _.merge(pool, {
320 stats: {
321 bytes_used: { latest: 5, rate: 0, rates: [] },
322 max_avail: { latest: 15, rate: 0, rates: [] },
323 rd_bytes: {
324 latest: 6,
325 rate: 4,
326 rates: [
327 [0, 2],
328 [1, 6]
329 ]
330 }
331 },
332 pg_status: { 'active+clean': 8, down: 2 }
333 });
334 expect(component.transformPoolsData([pool])).toEqual(
335 getPoolData({
336 pg_status: '8 active+clean, 2 down',
337 stats: {
338 bytes_used: { latest: 5, rate: 0, rates: [] },
339 max_avail: { latest: 15, rate: 0, rates: [] },
340 rd_bytes: { latest: 6, rate: 4, rates: [2, 6] }
341 },
342 usage: 0.25
343 })
344 );
345 });
346
347 it('transforms pools data correctly if stats are missing', () => {
348 expect(component.transformPoolsData([pool])).toEqual(getPoolData({}));
349 });
350
351 it('transforms empty pools data correctly', () => {
352 expect(component.transformPoolsData(undefined)).toEqual(undefined);
353 expect(component.transformPoolsData([])).toEqual([]);
354 });
355
356 it('shows not marked pools in progress if pg_num does not match pg_num_target', () => {
357 const pools = [
358 _.merge(pool, {
359 pg_num: 32,
360 pg_num_target: 16,
361 pg_placement_num: 32,
362 pg_placement_num_target: 16
363 })
364 ];
365 expect(component.transformPoolsData(pools)).toEqual(
366 getPoolData({
367 cdExecuting: 'Updating',
368 pg_num: 32,
369 pg_num_target: 16,
370 pg_placement_num: 32,
371 pg_placement_num_target: 16
372 })
373 );
374 });
375
376 it('shows marked pools in progress as defined by task', () => {
377 const pools = [
378 _.merge(pool, {
379 pg_num: 32,
380 pg_num_target: 16,
381 pg_placement_num: 32,
382 pg_placement_num_target: 16,
383 cdExecuting: 'Updating... 50%'
384 })
385 ];
386 expect(component.transformPoolsData(pools)).toEqual(
387 getPoolData({
388 cdExecuting: 'Updating... 50%',
389 pg_num: 32,
390 pg_num_target: 16,
391 pg_placement_num: 32,
392 pg_placement_num_target: 16
393 })
394 );
395 });
396 });
397
398 describe('transformPgStatus', () => {
399 it('returns status groups correctly', () => {
400 const pgStatus = { 'active+clean': 8 };
401 const expected = '8 active+clean';
402
403 expect(component.transformPgStatus(pgStatus)).toEqual(expected);
404 });
405
406 it('returns separated status groups', () => {
407 const pgStatus = { 'active+clean': 8, down: 2 };
408 const expected = '8 active+clean, 2 down';
409
410 expect(component.transformPgStatus(pgStatus)).toEqual(expected);
411 });
412
413 it('returns separated statuses correctly', () => {
414 const pgStatus = { active: 8, down: 2 };
415 const expected = '8 active, 2 down';
416
417 expect(component.transformPgStatus(pgStatus)).toEqual(expected);
418 });
419
420 it('returns empty string', () => {
421 const pgStatus: any = undefined;
422 const expected = '';
423
424 expect(component.transformPgStatus(pgStatus)).toEqual(expected);
425 });
426 });
427
428 describe('getSelectionTiers', () => {
429 const setSelectionTiers = (tiers: number[]) => {
430 component.selection.selected = [{ tiers }];
431 component.getSelectionTiers();
432 };
433
434 beforeEach(() => {
435 component.pools = getPoolList();
436 });
437
438 it('should select multiple existing cache tiers', () => {
439 setSelectionTiers([0, 1, 2]);
440 expect(component.selectionCacheTiers).toEqual(getPoolList());
441 });
442
443 it('should select correct existing cache tier', () => {
444 setSelectionTiers([0]);
445 expect(component.selectionCacheTiers).toEqual([createPool('a', 0)]);
446 });
447
448 it('should not select cache tier if id is invalid', () => {
449 setSelectionTiers([-1]);
450 expect(component.selectionCacheTiers).toEqual([]);
451 });
452
453 it('should not select cache tier if empty', () => {
454 setSelectionTiers([]);
455 expect(component.selectionCacheTiers).toEqual([]);
456 });
457
458 it('should be able to selected one pool with multiple tiers, than with a single tier, than with no tiers', () => {
459 setSelectionTiers([0, 1, 2]);
460 expect(component.selectionCacheTiers).toEqual(getPoolList());
461 setSelectionTiers([0]);
462 expect(component.selectionCacheTiers).toEqual([createPool('a', 0)]);
463 setSelectionTiers([]);
464 expect(component.selectionCacheTiers).toEqual([]);
465 });
466 });
467
468 describe('getDisableDesc', () => {
469 it('should return message if mon_allow_pool_delete flag is set to false', () => {
470 component.monAllowPoolDelete = false;
471 expect(component.getDisableDesc()).toBe(
472 'Pool deletion is disabled by the mon_allow_pool_delete configuration setting.'
473 );
474 });
475
476 it('should return undefined if mon_allow_pool_delete flag is set to true', () => {
477 component.monAllowPoolDelete = true;
478 expect(component.getDisableDesc()).toBeUndefined();
479 });
480 });
481 });