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