]> git.proxmox.com Git - ceph.git/blame - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.spec.ts
import 15.2.4
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / cephfs / cephfs-directories / cephfs-directories.component.spec.ts
CommitLineData
9f95a23c
TL
1import { HttpClientTestingModule } from '@angular/common/http/testing';
2import { Type } from '@angular/core';
3import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
4import { Validators } from '@angular/forms';
5import { RouterTestingModule } from '@angular/router/testing';
6
e306af50 7import { TreeComponent, TreeModule, TREE_ACTIONS } from 'angular-tree-component';
9f95a23c
TL
8import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
9import { BsModalRef, BsModalService, ModalModule } from 'ngx-bootstrap/modal';
10import { ToastrModule } from 'ngx-toastr';
11import { Observable, of } from 'rxjs';
12
13import {
14 configureTestBed,
15 i18nProviders,
16 modalServiceShow,
17 PermissionHelper
18} from '../../../../testing/unit-test-helper';
19import { CephfsService } from '../../../shared/api/cephfs.service';
20import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component';
21import { FormModalComponent } from '../../../shared/components/form-modal/form-modal.component';
22import { NotificationType } from '../../../shared/enum/notification-type.enum';
23import { CdValidators } from '../../../shared/forms/cd-validators';
24import { CdTableAction } from '../../../shared/models/cd-table-action';
25import { CdTableSelection } from '../../../shared/models/cd-table-selection';
26import {
27 CephfsDir,
28 CephfsQuotas,
29 CephfsSnapshot
30} from '../../../shared/models/cephfs-directory-models';
31import { NotificationService } from '../../../shared/services/notification.service';
32import { SharedModule } from '../../../shared/shared.module';
33import { CephfsDirectoriesComponent } from './cephfs-directories.component';
34
35describe('CephfsDirectoriesComponent', () => {
36 let component: CephfsDirectoriesComponent;
37 let fixture: ComponentFixture<CephfsDirectoriesComponent>;
38 let cephfsService: CephfsService;
39 let noAsyncUpdate: boolean;
40 let lsDirSpy: jasmine.Spy;
41 let modalShowSpy: jasmine.Spy;
42 let notificationShowSpy: jasmine.Spy;
43 let minValidator: jasmine.Spy;
44 let maxValidator: jasmine.Spy;
45 let minBinaryValidator: jasmine.Spy;
46 let maxBinaryValidator: jasmine.Spy;
47 let modal: any;
48
49 // Get's private attributes or functions
50 const get = {
51 nodeIds: (): { [path: string]: CephfsDir } => component['nodeIds'],
52 dirs: (): CephfsDir[] => component['dirs'],
53 requestedPaths: (): string[] => component['requestedPaths']
54 };
55
56 // Object contains mock data that will be reset before each test.
57 let mockData: {
58 nodes: any;
59 parent: any;
60 createdSnaps: CephfsSnapshot[] | any[];
61 deletedSnaps: CephfsSnapshot[] | any[];
62 updatedQuotas: { [path: string]: CephfsQuotas };
63 createdDirs: CephfsDir[];
64 };
65
66 // Object contains mock functions
67 const mockLib = {
68 quotas: (max_bytes: number, max_files: number): CephfsQuotas => ({ max_bytes, max_files }),
69 snapshots: (dirPath: string, howMany: number): CephfsSnapshot[] => {
70 const name = 'someSnapshot';
71 const snapshots = [];
72 const oneDay = 3600 * 24 * 1000;
73 for (let i = 0; i < howMany; i++) {
74 const snapName = `${name}${i + 1}`;
75 const path = `${dirPath}/.snap/${snapName}`;
76 const created = new Date(+new Date() - oneDay * i).toString();
77 snapshots.push({ name: snapName, path, created });
78 }
79 return snapshots;
80 },
81 dir: (parentPath: string, name: string, modifier: number): CephfsDir => {
82 const dirPath = `${parentPath === '/' ? '' : parentPath}/${name}`;
83 let snapshots = mockLib.snapshots(parentPath, modifier);
84 const extraSnapshots = mockData.createdSnaps.filter((s) => s.path === dirPath);
85 if (extraSnapshots.length > 0) {
86 snapshots = snapshots.concat(extraSnapshots);
87 }
88 const deletedSnapshots = mockData.deletedSnaps
89 .filter((s) => s.path === dirPath)
90 .map((s) => s.name);
91 if (deletedSnapshots.length > 0) {
92 snapshots = snapshots.filter((s) => !deletedSnapshots.includes(s.name));
93 }
94 return {
95 name,
96 path: dirPath,
97 parent: parentPath,
98 quotas: Object.assign(
99 mockLib.quotas(1024 * modifier, 10 * modifier),
100 mockData.updatedQuotas[dirPath] || {}
101 ),
102 snapshots: snapshots
103 };
104 },
105 // Only used inside other mocks
106 lsSingleDir: (path = ''): CephfsDir[] => {
107 const customDirs = mockData.createdDirs.filter((d) => d.parent === path);
108 const isCustomDir = mockData.createdDirs.some((d) => d.path === path);
109 if (isCustomDir || path.includes('b')) {
110 // 'b' has no sub directories
111 return customDirs;
112 }
113 return customDirs.concat([
114 // Directories are not sorted!
115 mockLib.dir(path, 'c', 3),
116 mockLib.dir(path, 'a', 1),
117 mockLib.dir(path, 'b', 2)
118 ]);
119 },
120 lsDir: (_id: number, path = ''): Observable<CephfsDir[]> => {
121 // will return 2 levels deep
122 let data = mockLib.lsSingleDir(path);
123 const paths = data.map((dir) => dir.path);
124 paths.forEach((pathL2) => {
125 data = data.concat(mockLib.lsSingleDir(pathL2));
126 });
127 if (path === '' || path === '/') {
128 // Adds root directory on ls of '/' to the directories list.
129 const root = mockLib.dir(path, '/', 1);
130 root.path = '/';
131 root.parent = undefined;
132 root.quotas = undefined;
133 data = [root].concat(data);
134 }
135 return of(data);
136 },
137 mkSnapshot: (_id: any, path: string, name: string): Observable<string> => {
138 mockData.createdSnaps.push({
139 name,
140 path,
141 created: new Date().toString()
142 });
143 return of(name);
144 },
145 rmSnapshot: (_id: any, path: string, name: string): Observable<string> => {
146 mockData.deletedSnaps.push({
147 name,
148 path,
149 created: new Date().toString()
150 });
151 return of(name);
152 },
153 updateQuota: (_id: any, path: string, updated: CephfsQuotas): Observable<string> => {
154 mockData.updatedQuotas[path] = Object.assign(mockData.updatedQuotas[path] || {}, updated);
155 return of('Response');
156 },
157 modalShow: (comp: Type<any>, init: any): any => {
158 modal = modalServiceShow(comp, init);
159 return modal.ref;
160 },
161 getNodeById: (path: string) => {
162 return mockLib.useNode(path);
163 },
164 updateNodes: (path: string) => {
165 const p: Promise<any[]> = component.treeOptions.getChildren({ id: path });
166 return noAsyncUpdate ? () => p : mockLib.asyncNodeUpdate(p);
167 },
168 asyncNodeUpdate: fakeAsync((p: Promise<any[]>) => {
169 p.then((nodes) => {
170 mockData.nodes = mockData.nodes.concat(nodes);
171 });
172 tick();
173 }),
174 changeId: (id: number) => {
175 // For some reason this spy has to be renewed after usage
176 spyOn(global, 'setTimeout').and.callFake((fn) => fn());
177 component.id = id;
178 component.ngOnChanges();
179 mockData.nodes = component.nodes.concat(mockData.nodes);
180 },
181 selectNode: (path: string) => {
182 component.treeOptions.actionMapping.mouse.click(undefined, mockLib.useNode(path), undefined);
183 },
184 // Creates TreeNode with parents until root
185 useNode: (path: string): { id: string; parent: any; data: any; loadNodeChildren: Function } => {
186 const parentPath = path.split('/');
187 parentPath.pop();
188 const parentIsRoot = parentPath.length === 1;
189 const parent = parentIsRoot ? { id: '/' } : mockLib.useNode(parentPath.join('/'));
190 return {
191 id: path,
192 parent,
193 data: {},
194 loadNodeChildren: () => mockLib.updateNodes(path)
195 };
196 },
197 treeActions: {
198 toggleActive: (_a: any, node: any, _b: any) => {
199 return mockLib.updateNodes(node.id);
200 }
201 },
202 mkDir: (path: string, name: string, maxFiles: number, maxBytes: number) => {
203 const dir = mockLib.dir(path, name, 3);
204 dir.quotas.max_bytes = maxBytes * 1024;
205 dir.quotas.max_files = maxFiles;
206 mockData.createdDirs.push(dir);
207 // Below is needed for quota tests only where 4 dirs are mocked
208 get.nodeIds()[dir.path] = dir;
209 mockData.nodes.push({ id: dir.path });
210 },
211 createSnapshotThroughModal: (name: string) => {
212 component.createSnapshot();
213 modal.component.onSubmitForm({ name });
214 },
215 deleteSnapshotsThroughModal: (snapshots: CephfsSnapshot[]) => {
216 component.snapshot.selection.selected = snapshots;
217 component.deleteSnapshotModal();
218 modal.component.callSubmitAction();
219 },
220 updateQuotaThroughModal: (attribute: string, value: number) => {
221 component.quota.selection.selected = component.settings.filter(
222 (q) => q.quotaKey === attribute
223 );
224 component.updateQuotaModal();
225 modal.component.onSubmitForm({ [attribute]: value });
226 },
227 unsetQuotaThroughModal: (attribute: string) => {
228 component.quota.selection.selected = component.settings.filter(
229 (q) => q.quotaKey === attribute
230 );
231 component.unsetQuotaModal();
232 modal.component.onSubmit();
233 },
234 setFourQuotaDirs: (quotas: number[][]) => {
235 expect(quotas.length).toBe(4); // Make sure this function is used correctly
236 let path = '';
237 quotas.forEach((quota, index) => {
238 index += 1;
239 mockLib.mkDir(path === '' ? '/' : path, index.toString(), quota[0], quota[1]);
240 path += '/' + index;
241 });
242 mockData.parent = {
243 value: '3',
244 id: '/1/2/3',
245 parent: {
246 value: '2',
247 id: '/1/2',
248 parent: {
249 value: '1',
250 id: '/1',
251 parent: { value: '/', id: '/' }
252 }
253 }
254 };
255 mockLib.selectNode('/1/2/3/4');
256 }
257 };
258
259 // Expects that are used frequently
260 const assert = {
261 dirLength: (n: number) => expect(get.dirs().length).toBe(n),
262 nodeLength: (n: number) => expect(mockData.nodes.length).toBe(n),
263 lsDirCalledTimes: (n: number) => expect(lsDirSpy).toHaveBeenCalledTimes(n),
264 lsDirHasBeenCalledWith: (id: number, paths: string[]) => {
265 paths.forEach((path) => expect(lsDirSpy).toHaveBeenCalledWith(id, path));
266 assert.lsDirCalledTimes(paths.length);
267 },
268 requestedPaths: (expected: string[]) => expect(get.requestedPaths()).toEqual(expected),
269 snapshotsByName: (snaps: string[]) =>
270 expect(component.selectedDir.snapshots.map((s) => s.name)).toEqual(snaps),
271 dirQuotas: (bytes: number, files: number) => {
272 expect(component.selectedDir.quotas).toEqual({ max_bytes: bytes, max_files: files });
273 },
274 noQuota: (key: 'bytes' | 'files') => {
275 assert.quotaRow(key, '', 0, '');
276 },
277 quotaIsNotInherited: (key: 'bytes' | 'files', shownValue: any, nextMaximum: number) => {
278 const dir = component.selectedDir;
279 const path = dir.path;
280 assert.quotaRow(key, shownValue, nextMaximum, path);
281 },
282 quotaIsInherited: (key: 'bytes' | 'files', shownValue: any, path: string) => {
283 const isBytes = key === 'bytes';
284 const nextMaximum = get.nodeIds()[path].quotas[isBytes ? 'max_bytes' : 'max_files'];
285 assert.quotaRow(key, shownValue, nextMaximum, path);
286 },
287 quotaRow: (
288 key: 'bytes' | 'files',
289 shownValue: number | string,
290 nextTreeMaximum: number,
291 originPath: string
292 ) => {
293 const isBytes = key === 'bytes';
294 expect(component.settings[isBytes ? 1 : 0]).toEqual({
295 row: {
296 name: `Max ${isBytes ? 'size' : key}`,
297 value: shownValue,
298 originPath
299 },
300 quotaKey: `max_${key}`,
301 dirValue: expect.any(Number),
302 nextTreeMaximum: {
303 value: nextTreeMaximum,
304 path: expect.any(String)
305 }
306 });
307 },
308 quotaUnsetModalTexts: (titleText: string, message: string, notificationMsg: string) => {
309 expect(modalShowSpy).toHaveBeenCalledWith(ConfirmationModalComponent, {
310 initialState: expect.objectContaining({
311 titleText,
312 description: message,
313 buttonText: 'Unset'
314 })
315 });
316 expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
317 },
318 quotaUpdateModalTexts: (titleText: string, message: string, notificationMsg: string) => {
319 expect(modalShowSpy).toHaveBeenCalledWith(FormModalComponent, {
320 initialState: expect.objectContaining({
321 titleText,
322 message,
323 submitButtonText: 'Save'
324 })
325 });
326 expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
327 },
328 quotaUpdateModalField: (
329 type: string,
330 label: string,
331 key: string,
332 value: number,
333 max: number,
334 errors?: { [key: string]: string }
335 ) => {
336 expect(modalShowSpy).toHaveBeenCalledWith(FormModalComponent, {
337 initialState: expect.objectContaining({
338 fields: [
339 {
340 type,
341 label,
342 errors,
343 name: key,
344 value,
345 validators: expect.anything(),
346 required: true
347 }
348 ]
349 })
350 });
351 if (type === 'binary') {
352 expect(minBinaryValidator).toHaveBeenCalledWith(0);
353 expect(maxBinaryValidator).toHaveBeenCalledWith(max);
354 } else {
355 expect(minValidator).toHaveBeenCalledWith(0);
356 expect(maxValidator).toHaveBeenCalledWith(max);
357 }
358 }
359 };
360
361 configureTestBed({
362 imports: [
363 HttpClientTestingModule,
364 SharedModule,
365 RouterTestingModule,
366 TreeModule.forRoot(),
367 NgBootstrapFormValidationModule.forRoot(),
368 ToastrModule.forRoot(),
369 ModalModule.forRoot()
370 ],
371 declarations: [CephfsDirectoriesComponent],
372 providers: [i18nProviders, BsModalRef]
373 });
374
375 beforeEach(() => {
376 noAsyncUpdate = false;
377 mockData = {
378 nodes: [],
379 parent: undefined,
380 createdSnaps: [],
381 deletedSnaps: [],
382 createdDirs: [],
383 updatedQuotas: {}
384 };
385
386 cephfsService = TestBed.get(CephfsService);
387 lsDirSpy = spyOn(cephfsService, 'lsDir').and.callFake(mockLib.lsDir);
388 spyOn(cephfsService, 'mkSnapshot').and.callFake(mockLib.mkSnapshot);
389 spyOn(cephfsService, 'rmSnapshot').and.callFake(mockLib.rmSnapshot);
390 spyOn(cephfsService, 'updateQuota').and.callFake(mockLib.updateQuota);
391
392 modalShowSpy = spyOn(TestBed.get(BsModalService), 'show').and.callFake(mockLib.modalShow);
393 notificationShowSpy = spyOn(TestBed.get(NotificationService), 'show').and.stub();
394
395 fixture = TestBed.createComponent(CephfsDirectoriesComponent);
396 component = fixture.componentInstance;
397 fixture.detectChanges();
398
399 spyOn(TREE_ACTIONS, 'TOGGLE_ACTIVE').and.callFake(mockLib.treeActions.toggleActive);
400
401 component.treeComponent = {
402 sizeChanged: () => null,
403 treeModel: { getNodeById: mockLib.getNodeById, update: () => null }
404 } as TreeComponent;
405 });
406
407 it('should create', () => {
408 expect(component).toBeTruthy();
409 });
410
411 describe('mock self test', () => {
412 it('tests snapshots mock', () => {
413 expect(mockLib.snapshots('/a', 1).map((s) => ({ name: s.name, path: s.path }))).toEqual([
414 {
415 name: 'someSnapshot1',
416 path: '/a/.snap/someSnapshot1'
417 }
418 ]);
419 expect(mockLib.snapshots('/a/b', 3).map((s) => ({ name: s.name, path: s.path }))).toEqual([
420 {
421 name: 'someSnapshot1',
422 path: '/a/b/.snap/someSnapshot1'
423 },
424 {
425 name: 'someSnapshot2',
426 path: '/a/b/.snap/someSnapshot2'
427 },
428 {
429 name: 'someSnapshot3',
430 path: '/a/b/.snap/someSnapshot3'
431 }
432 ]);
433 });
434
435 it('tests dir mock', () => {
436 const path = '/a/b/c';
801d1391
TL
437 mockData.createdSnaps = [
438 { path, name: 's1' },
439 { path, name: 's2' }
440 ];
441 mockData.deletedSnaps = [
442 { path, name: 'someSnapshot2' },
443 { path, name: 's2' }
444 ];
9f95a23c
TL
445 const dir = mockLib.dir('/a/b', 'c', 2);
446 expect(dir.path).toBe('/a/b/c');
447 expect(dir.parent).toBe('/a/b');
448 expect(dir.quotas).toEqual({ max_bytes: 2048, max_files: 20 });
449 expect(dir.snapshots.map((s) => s.name)).toEqual(['someSnapshot1', 's1']);
450 });
451
452 it('tests lsdir mock', () => {
453 let dirs: CephfsDir[] = [];
454 mockLib.lsDir(2, '/a').subscribe((x) => (dirs = x));
455 expect(dirs.map((d) => d.path)).toEqual([
456 '/a/c',
457 '/a/a',
458 '/a/b',
459 '/a/c/c',
460 '/a/c/a',
461 '/a/c/b',
462 '/a/a/c',
463 '/a/a/a',
464 '/a/a/b'
465 ]);
466 });
467
468 describe('test quota update mock', () => {
469 const PATH = '/a';
470 const ID = 2;
471
472 const updateQuota = (quotas: CephfsQuotas) => mockLib.updateQuota(ID, PATH, quotas);
473
474 const expectMockUpdate = (max_bytes?: number, max_files?: number) =>
475 expect(mockData.updatedQuotas[PATH]).toEqual({
476 max_bytes,
477 max_files
478 });
479
480 const expectLsUpdate = (max_bytes?: number, max_files?: number) => {
481 let dir: CephfsDir;
482 mockLib.lsDir(ID, '/').subscribe((dirs) => (dir = dirs.find((d) => d.path === PATH)));
483 expect(dir.quotas).toEqual({
484 max_bytes,
485 max_files
486 });
487 };
488
489 it('tests to set quotas', () => {
490 expectLsUpdate(1024, 10);
491
492 updateQuota({ max_bytes: 512 });
493 expectMockUpdate(512);
494 expectLsUpdate(512, 10);
495
496 updateQuota({ max_files: 100 });
497 expectMockUpdate(512, 100);
498 expectLsUpdate(512, 100);
499 });
500
501 it('tests to unset quotas', () => {
502 updateQuota({ max_files: 0 });
503 expectMockUpdate(undefined, 0);
504 expectLsUpdate(1024, 0);
505
506 updateQuota({ max_bytes: 0 });
507 expectMockUpdate(0, 0);
508 expectLsUpdate(0, 0);
509 });
510 });
511 });
512
513 it('calls lsDir only if an id exits', () => {
514 assert.lsDirCalledTimes(0);
515
516 mockLib.changeId(1);
517 assert.lsDirCalledTimes(1);
518 expect(lsDirSpy).toHaveBeenCalledWith(1, '/');
519
520 mockLib.changeId(2);
521 assert.lsDirCalledTimes(2);
522 expect(lsDirSpy).toHaveBeenCalledWith(2, '/');
523 });
524
525 describe('listing sub directories', () => {
526 beforeEach(() => {
527 mockLib.changeId(1);
528 /**
529 * Tree looks like this:
530 * v /
531 * > a
532 * * b
533 * > c
534 * */
535 });
536
537 it('expands first level', () => {
538 // Tree will only show '*' if nor 'loadChildren' or 'children' are defined
539 expect(
540 mockData.nodes.map((node: any) => ({
541 [node.id]: node.hasChildren || node.isExpanded || Boolean(node.children)
542 }))
543 ).toEqual([{ '/': true }, { '/a': true }, { '/b': false }, { '/c': true }]);
544 });
545
546 it('resets all dynamic content on id change', () => {
547 mockLib.selectNode('/a');
548 /**
549 * Tree looks like this:
550 * v /
551 * v a <- Selected
552 * > a
553 * * b
554 * > c
555 * * b
556 * > c
557 * */
558 assert.requestedPaths(['/', '/a']);
559 assert.nodeLength(7);
560 assert.dirLength(16);
561 expect(component.selectedDir).toBeDefined();
562
563 mockLib.changeId(undefined);
564 assert.dirLength(0);
565 assert.requestedPaths([]);
566 expect(component.selectedDir).not.toBeDefined();
567 });
568
569 it('should select a node and show the directory contents', () => {
570 mockLib.selectNode('/a');
571 const dir = get.dirs().find((d) => d.path === '/a');
572 expect(component.selectedDir).toEqual(dir);
573 assert.quotaIsNotInherited('files', 10, 0);
574 assert.quotaIsNotInherited('bytes', '1 KiB', 0);
575 });
576
577 it('should extend the list by subdirectories when expanding', () => {
578 mockLib.selectNode('/a');
579 mockLib.selectNode('/a/c');
580 /**
581 * Tree looks like this:
582 * v /
583 * v a
584 * > a
585 * * b
586 * v c <- Selected
587 * > a
588 * * b
589 * > c
590 * * b
591 * > c
592 * */
593 assert.lsDirCalledTimes(3);
594 assert.requestedPaths(['/', '/a', '/a/c']);
595 assert.dirLength(22);
596 assert.nodeLength(10);
597 });
598
599 it('should update the tree after each selection', () => {
600 const spy = spyOn(component.treeComponent, 'sizeChanged').and.callThrough();
601 expect(spy).toHaveBeenCalledTimes(0);
602 mockLib.selectNode('/a');
603 expect(spy).toHaveBeenCalledTimes(1);
604 mockLib.selectNode('/a/c');
605 expect(spy).toHaveBeenCalledTimes(2);
606 });
607
608 it('should select parent by path', () => {
609 mockLib.selectNode('/a');
610 mockLib.selectNode('/a/c');
611 mockLib.selectNode('/a/c/a');
612 component.selectOrigin('/a');
613 expect(component.selectedDir.path).toBe('/a');
614 });
615
616 it('should refresh directories with no sub directories as they could have some now', () => {
617 mockLib.selectNode('/b');
618 /**
619 * Tree looks like this:
620 * v /
621 * > a
622 * * b <- Selected
623 * > c
624 * */
625 assert.lsDirCalledTimes(2);
626 assert.requestedPaths(['/', '/b']);
627 assert.nodeLength(4);
628 });
629
630 describe('used quotas', () => {
631 it('should use no quota if none is set', () => {
801d1391
TL
632 mockLib.setFourQuotaDirs([
633 [0, 0],
634 [0, 0],
635 [0, 0],
636 [0, 0]
637 ]);
9f95a23c
TL
638 assert.noQuota('files');
639 assert.noQuota('bytes');
640 assert.dirQuotas(0, 0);
641 });
642
643 it('should use quota from upper parents', () => {
801d1391
TL
644 mockLib.setFourQuotaDirs([
645 [100, 0],
646 [0, 8],
647 [0, 0],
648 [0, 0]
649 ]);
9f95a23c
TL
650 assert.quotaIsInherited('files', 100, '/1');
651 assert.quotaIsInherited('bytes', '8 KiB', '/1/2');
652 assert.dirQuotas(0, 0);
653 });
654
655 it('should use quota from the parent with the lowest value (deep inheritance)', () => {
801d1391
TL
656 mockLib.setFourQuotaDirs([
657 [200, 1],
658 [100, 4],
659 [400, 3],
660 [300, 2]
661 ]);
9f95a23c
TL
662 assert.quotaIsInherited('files', 100, '/1/2');
663 assert.quotaIsInherited('bytes', '1 KiB', '/1');
664 assert.dirQuotas(2048, 300);
665 });
666
667 it('should use current value', () => {
801d1391
TL
668 mockLib.setFourQuotaDirs([
669 [200, 2],
670 [300, 4],
671 [400, 3],
672 [100, 1]
673 ]);
9f95a23c
TL
674 assert.quotaIsNotInherited('files', 100, 200);
675 assert.quotaIsNotInherited('bytes', '1 KiB', 2048);
676 assert.dirQuotas(1024, 100);
677 });
678 });
679 });
680
681 describe('snapshots', () => {
682 beforeEach(() => {
683 mockLib.changeId(1);
684 mockLib.selectNode('/a');
685 });
686
687 it('should create a snapshot', () => {
688 mockLib.createSnapshotThroughModal('newSnap');
689 expect(cephfsService.mkSnapshot).toHaveBeenCalledWith(1, '/a', 'newSnap');
690 assert.snapshotsByName(['someSnapshot1', 'newSnap']);
691 });
692
693 it('should delete a snapshot', () => {
694 mockLib.createSnapshotThroughModal('deleteMe');
695 mockLib.deleteSnapshotsThroughModal([component.selectedDir.snapshots[1]]);
696 assert.snapshotsByName(['someSnapshot1']);
697 });
698
699 it('should delete all snapshots', () => {
700 mockLib.createSnapshotThroughModal('deleteAll');
701 mockLib.deleteSnapshotsThroughModal(component.selectedDir.snapshots);
702 assert.snapshotsByName([]);
703 });
704 });
705
706 it('should test all snapshot table actions combinations', () => {
707 const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
708 const tableActions = permissionHelper.setPermissionsAndGetActions(
709 component.snapshot.tableActions
710 );
711
712 expect(tableActions).toEqual({
713 'create,update,delete': {
714 actions: ['Create', 'Delete'],
715 primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
716 },
717 'create,update': {
718 actions: ['Create'],
719 primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
720 },
721 'create,delete': {
722 actions: ['Create', 'Delete'],
723 primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
724 },
725 create: {
726 actions: ['Create'],
727 primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
728 },
729 'update,delete': {
730 actions: ['Delete'],
731 primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
732 },
733 update: {
734 actions: [],
735 primary: { multiple: '', executing: '', single: '', no: '' }
736 },
737 delete: {
738 actions: ['Delete'],
739 primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
740 },
741 'no-permissions': {
742 actions: [],
743 primary: { multiple: '', executing: '', single: '', no: '' }
744 }
745 });
746 });
747
748 describe('quotas', () => {
749 beforeEach(() => {
750 // Spies
751 minValidator = spyOn(Validators, 'min').and.callThrough();
752 maxValidator = spyOn(Validators, 'max').and.callThrough();
753 minBinaryValidator = spyOn(CdValidators, 'binaryMin').and.callThrough();
754 maxBinaryValidator = spyOn(CdValidators, 'binaryMax').and.callThrough();
755 // Select /a/c/b
756 mockLib.changeId(1);
757 mockLib.selectNode('/a');
758 mockLib.selectNode('/a/c');
759 mockLib.selectNode('/a/c/b');
760 // Quotas after selection
761 assert.quotaIsInherited('files', 10, '/a');
762 assert.quotaIsInherited('bytes', '1 KiB', '/a');
763 assert.dirQuotas(2048, 20);
764 });
765
766 describe('update modal', () => {
767 describe('max_files', () => {
768 beforeEach(() => {
769 mockLib.updateQuotaThroughModal('max_files', 5);
770 });
771
772 it('should update max_files correctly', () => {
773 expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 5 });
774 assert.quotaIsNotInherited('files', 5, 10);
775 });
776
777 it('uses the correct form field', () => {
778 assert.quotaUpdateModalField('number', 'Max files', 'max_files', 20, 10, {
779 min: 'Value has to be at least 0 or more',
780 max: 'Value has to be at most 10 or less'
781 });
782 });
783
784 it('shows the right texts', () => {
785 assert.quotaUpdateModalTexts(
786 "Update CephFS files quota for '/a/c/b'",
787 "The inherited files quota 10 from '/a' is the maximum value to be used.",
788 "Updated CephFS files quota for '/a/c/b'"
789 );
790 });
791 });
792
793 describe('max_bytes', () => {
794 beforeEach(() => {
795 mockLib.updateQuotaThroughModal('max_bytes', 512);
796 });
797
798 it('should update max_files correctly', () => {
799 expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 512 });
800 assert.quotaIsNotInherited('bytes', '512 B', 1024);
801 });
802
803 it('uses the correct form field', () => {
804 mockLib.updateQuotaThroughModal('max_bytes', 512);
805 assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 2048, 1024);
806 });
807
808 it('shows the right texts', () => {
809 assert.quotaUpdateModalTexts(
810 "Update CephFS size quota for '/a/c/b'",
811 "The inherited size quota 1 KiB from '/a' is the maximum value to be used.",
812 "Updated CephFS size quota for '/a/c/b'"
813 );
814 });
815 });
816
817 describe('action behaviour', () => {
818 it('opens with next maximum as maximum if directory holds the current maximum', () => {
819 mockLib.updateQuotaThroughModal('max_bytes', 512);
820 mockLib.updateQuotaThroughModal('max_bytes', 888);
821 assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 512, 1024);
822 });
823
824 it("uses 'Set' action instead of 'Update' if the quota is not set (0)", () => {
825 mockLib.updateQuotaThroughModal('max_bytes', 0);
826 mockLib.updateQuotaThroughModal('max_bytes', 200);
827 assert.quotaUpdateModalTexts(
828 "Set CephFS size quota for '/a/c/b'",
829 "The inherited size quota 1 KiB from '/a' is the maximum value to be used.",
830 "Set CephFS size quota for '/a/c/b'"
831 );
832 });
833 });
834 });
835
836 describe('unset modal', () => {
837 describe('max_files', () => {
838 beforeEach(() => {
839 mockLib.updateQuotaThroughModal('max_files', 5); // Sets usable quota
840 mockLib.unsetQuotaThroughModal('max_files');
841 });
842
843 it('should unset max_files correctly', () => {
844 expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 0 });
845 assert.dirQuotas(2048, 0);
846 });
847
848 it('shows the right texts', () => {
849 assert.quotaUnsetModalTexts(
850 "Unset CephFS files quota for '/a/c/b'",
851 "Unset files quota 5 from '/a/c/b' in order to inherit files quota 10 from '/a'.",
852 "Unset CephFS files quota for '/a/c/b'"
853 );
854 });
855 });
856
857 describe('max_bytes', () => {
858 beforeEach(() => {
859 mockLib.updateQuotaThroughModal('max_bytes', 512); // Sets usable quota
860 mockLib.unsetQuotaThroughModal('max_bytes');
861 });
862
863 it('should unset max_files correctly', () => {
864 expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 0 });
865 assert.dirQuotas(0, 20);
866 });
867
868 it('shows the right texts', () => {
869 assert.quotaUnsetModalTexts(
870 "Unset CephFS size quota for '/a/c/b'",
871 "Unset size quota 512 B from '/a/c/b' in order to inherit size quota 1 KiB from '/a'.",
872 "Unset CephFS size quota for '/a/c/b'"
873 );
874 });
875 });
876
877 describe('action behaviour', () => {
878 it('uses different Text if no quota is inherited', () => {
879 mockLib.selectNode('/a');
880 mockLib.unsetQuotaThroughModal('max_bytes');
881 assert.quotaUnsetModalTexts(
882 "Unset CephFS size quota for '/a'",
883 "Unset size quota 1 KiB from '/a' in order to have no quota on the directory.",
884 "Unset CephFS size quota for '/a'"
885 );
886 });
887
888 it('uses different Text if quota is already inherited', () => {
889 mockLib.unsetQuotaThroughModal('max_bytes');
890 assert.quotaUnsetModalTexts(
891 "Unset CephFS size quota for '/a/c/b'",
892 "Unset size quota 2 KiB from '/a/c/b' which isn't used because of the inheritance " +
893 "of size quota 1 KiB from '/a'.",
894 "Unset CephFS size quota for '/a/c/b'"
895 );
896 });
897 });
898 });
899 });
900
901 describe('table actions', () => {
902 let actions: CdTableAction[];
903
904 const empty = (): CdTableSelection => new CdTableSelection();
905
906 const select = (value: number): CdTableSelection => {
907 const selection = new CdTableSelection();
908 selection.selected = [{ dirValue: value }];
909 return selection;
910 };
911
912 beforeEach(() => {
913 actions = component.quota.tableActions;
914 });
915
916 it("shows 'Set' for empty and not set quotas", () => {
917 const isSetVisible = actions[0].visible;
918 expect(isSetVisible(empty())).toBe(true);
919 expect(isSetVisible(select(0))).toBe(true);
920 expect(isSetVisible(select(1))).toBe(false);
921 });
922
923 it("shows 'Update' for set quotas only", () => {
924 const isUpdateVisible = actions[1].visible;
925 expect(isUpdateVisible(empty())).toBeFalsy();
926 expect(isUpdateVisible(select(0))).toBe(false);
927 expect(isUpdateVisible(select(1))).toBe(true);
928 });
929
930 it("only enables 'Unset' for set quotas only", () => {
931 const isUnsetDisabled = actions[2].disable;
932 expect(isUnsetDisabled(empty())).toBe(true);
933 expect(isUnsetDisabled(select(0))).toBe(true);
934 expect(isUnsetDisabled(select(1))).toBe(false);
935 });
936
937 it('should test all quota table actions permission combinations', () => {
938 const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
939 const tableActions = permissionHelper.setPermissionsAndGetActions(
940 component.quota.tableActions
941 );
942
943 expect(tableActions).toEqual({
944 'create,update,delete': {
945 actions: ['Set', 'Update', 'Unset'],
946 primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
947 },
948 'create,update': {
949 actions: ['Set', 'Update', 'Unset'],
950 primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
951 },
952 'create,delete': {
953 actions: [],
954 primary: { multiple: '', executing: '', single: '', no: '' }
955 },
956 create: {
957 actions: [],
958 primary: { multiple: '', executing: '', single: '', no: '' }
959 },
960 'update,delete': {
961 actions: ['Set', 'Update', 'Unset'],
962 primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
963 },
964 update: {
965 actions: ['Set', 'Update', 'Unset'],
966 primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
967 },
968 delete: {
969 actions: [],
970 primary: { multiple: '', executing: '', single: '', no: '' }
971 },
972 'no-permissions': {
973 actions: [],
974 primary: { multiple: '', executing: '', single: '', no: '' }
975 }
976 });
977 });
978 });
979
980 describe('reload all', () => {
981 const calledPaths = ['/', '/a', '/a/c', '/a/c/a', '/a/c/a/b'];
982
983 const dirsByPath = (): string[] => get.dirs().map((d) => d.path);
984
985 beforeEach(() => {
986 mockLib.changeId(1);
987 mockLib.selectNode('/a');
988 mockLib.selectNode('/a/c');
989 mockLib.selectNode('/a/c/a');
990 mockLib.selectNode('/a/c/a/b');
991 });
992
993 it('should reload all requested paths', () => {
994 assert.lsDirHasBeenCalledWith(1, calledPaths);
995 lsDirSpy.calls.reset();
996 assert.lsDirHasBeenCalledWith(1, []);
997 component.refreshAllDirectories();
998 assert.lsDirHasBeenCalledWith(1, calledPaths);
999 });
1000
1001 it('should reload all requested paths if not selected anything', () => {
1002 lsDirSpy.calls.reset();
1003 mockLib.changeId(2);
1004 assert.lsDirHasBeenCalledWith(2, ['/']);
1005 lsDirSpy.calls.reset();
1006 component.refreshAllDirectories();
1007 assert.lsDirHasBeenCalledWith(2, ['/']);
1008 });
1009
1010 it('should add new directories', () => {
1011 // Create two new directories in preparation
1012 const dirsBeforeRefresh = dirsByPath();
1013 expect(dirsBeforeRefresh.includes('/a/c/has_dir_now')).toBe(false);
1014 mockLib.mkDir('/a/c', 'has_dir_now', 0, 0);
1015 mockLib.mkDir('/a/c/a/b', 'has_dir_now_too', 0, 0);
1016 // Now the new directories will be fetched
1017 component.refreshAllDirectories();
1018 const dirsAfterRefresh = dirsByPath();
1019 expect(dirsAfterRefresh.length - dirsBeforeRefresh.length).toBe(2);
1020 expect(dirsAfterRefresh.includes('/a/c/has_dir_now')).toBe(true);
1021 expect(dirsAfterRefresh.includes('/a/c/a/b/has_dir_now_too')).toBe(true);
1022 });
1023
1024 it('should remove deleted directories', () => {
1025 // Create one new directory and refresh in order to have it added to the directories list
1026 mockLib.mkDir('/a/c', 'will_be_removed_shortly', 0, 0);
1027 component.refreshAllDirectories();
1028 const dirsBeforeRefresh = dirsByPath();
1029 expect(dirsBeforeRefresh.includes('/a/c/will_be_removed_shortly')).toBe(true);
1030 mockData.createdDirs = []; // Mocks the deletion of the directory
1031 // Now the deleted directory will be missing on refresh
1032 component.refreshAllDirectories();
1033 const dirsAfterRefresh = dirsByPath();
1034 expect(dirsAfterRefresh.length - dirsBeforeRefresh.length).toBe(-1);
1035 expect(dirsAfterRefresh.includes('/a/c/will_be_removed_shortly')).toBe(false);
1036 });
1037
1038 describe('loading indicator', () => {
1039 beforeEach(() => {
1040 noAsyncUpdate = true;
1041 });
1042
1043 it('should have set loading indicator to false after refreshing all dirs', fakeAsync(() => {
1044 component.refreshAllDirectories();
1045 expect(component.loadingIndicator).toBe(true);
1046 tick(3000); // To resolve all promises
1047 expect(component.loadingIndicator).toBe(false);
1048 }));
1049
1050 it('should only update the tree once and not on every call', fakeAsync(() => {
1051 const spy = spyOn(component.treeComponent, 'sizeChanged').and.callThrough();
1052 component.refreshAllDirectories();
1053 expect(spy).toHaveBeenCalledTimes(0);
1054 tick(3000); // To resolve all promises
1055 // Called during the interval and at the end of timeout
1056 expect(spy).toHaveBeenCalledTimes(2);
1057 }));
1058
1059 it('should have set all loaded dirs as attribute names of "indicators"', () => {
1060 noAsyncUpdate = false;
1061 component.refreshAllDirectories();
1062 expect(Object.keys(component.loading).sort()).toEqual(calledPaths);
1063 });
1064
1065 it('should set an indicator to true during load', () => {
1066 lsDirSpy.and.callFake(() => Observable.create((): null => null));
1067 component.refreshAllDirectories();
1068 expect(Object.values(component.loading).every((b) => b)).toBe(true);
1069 expect(component.loadingIndicator).toBe(true);
1070 });
1071 });
1072 });
1073});