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