]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/blob
4017453b3b9e4c4fcea0eb49672db35bedebe195
[ceph-ci.git] /
1 import { HttpClientTestingModule } from '@angular/common/http/testing';
2 import { Type } from '@angular/core';
3 import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
4 import { Validators } from '@angular/forms';
5 import { RouterTestingModule } from '@angular/router/testing';
6
7 import { TreeComponent, TreeModule, TREE_ACTIONS } from 'angular-tree-component';
8 import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
9 import { BsModalRef, BsModalService, ModalModule } from 'ngx-bootstrap/modal';
10 import { ToastrModule } from 'ngx-toastr';
11 import { Observable, of } from 'rxjs';
12
13 import {
14   configureTestBed,
15   i18nProviders,
16   modalServiceShow,
17   PermissionHelper
18 } from '../../../../testing/unit-test-helper';
19 import { CephfsService } from '../../../shared/api/cephfs.service';
20 import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component';
21 import { FormModalComponent } from '../../../shared/components/form-modal/form-modal.component';
22 import { NotificationType } from '../../../shared/enum/notification-type.enum';
23 import { CdValidators } from '../../../shared/forms/cd-validators';
24 import { CdTableAction } from '../../../shared/models/cd-table-action';
25 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
26 import {
27   CephfsDir,
28   CephfsQuotas,
29   CephfsSnapshot
30 } from '../../../shared/models/cephfs-directory-models';
31 import { NotificationService } from '../../../shared/services/notification.service';
32 import { SharedModule } from '../../../shared/shared.module';
33 import { CephfsDirectoriesComponent } from './cephfs-directories.component';
34
35 describe('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.inject(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.inject(BsModalService), 'show').and.callFake(mockLib.modalShow);
393     notificationShowSpy = spyOn(TestBed.inject(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';
437       mockData.createdSnaps = [
438         { path, name: 's1' },
439         { path, name: 's2' }
440       ];
441       mockData.deletedSnaps = [
442         { path, name: 'someSnapshot2' },
443         { path, name: 's2' }
444       ];
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', () => {
632         mockLib.setFourQuotaDirs([
633           [0, 0],
634           [0, 0],
635           [0, 0],
636           [0, 0]
637         ]);
638         assert.noQuota('files');
639         assert.noQuota('bytes');
640         assert.dirQuotas(0, 0);
641       });
642
643       it('should use quota from upper parents', () => {
644         mockLib.setFourQuotaDirs([
645           [100, 0],
646           [0, 8],
647           [0, 0],
648           [0, 0]
649         ]);
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)', () => {
656         mockLib.setFourQuotaDirs([
657           [200, 1],
658           [100, 4],
659           [400, 3],
660           [300, 2]
661         ]);
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', () => {
668         mockLib.setFourQuotaDirs([
669           [200, 2],
670           [300, 4],
671           [400, 3],
672           [100, 1]
673         ]);
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(() => new Observable((): 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 });