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