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