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