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