]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/blob
894cfa08ba767a6040617f16c9a7bb34f4f162e2
[ceph.git] /
1 import { HttpClientTestingModule } from '@angular/common/http/testing';
2 import { ComponentFixture, TestBed } from '@angular/core/testing';
3 import { Validators } from '@angular/forms';
4 import { RouterTestingModule } from '@angular/router/testing';
5
6 import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
7 import { NodeEvent, Tree, TreeModel, TreeModule } from 'ng2-tree';
8 import { BsModalRef, BsModalService, ModalModule } from 'ngx-bootstrap/modal';
9 import { ToastrModule } from 'ngx-toastr';
10 import { of } from 'rxjs';
11
12 import {
13   configureTestBed,
14   i18nProviders,
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 { FormModalComponent } from '../../../shared/components/form-modal/form-modal.component';
21 import { NotificationType } from '../../../shared/enum/notification-type.enum';
22 import { CdValidators } from '../../../shared/forms/cd-validators';
23 import { CdTableAction } from '../../../shared/models/cd-table-action';
24 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
25 import {
26   CephfsDir,
27   CephfsQuotas,
28   CephfsSnapshot
29 } from '../../../shared/models/cephfs-directory-models';
30 import { NotificationService } from '../../../shared/services/notification.service';
31 import { SharedModule } from '../../../shared/shared.module';
32 import { CephfsDirectoriesComponent } from './cephfs-directories.component';
33
34 describe('CephfsDirectoriesComponent', () => {
35   let component: CephfsDirectoriesComponent;
36   let fixture: ComponentFixture<CephfsDirectoriesComponent>;
37   let cephfsService: CephfsService;
38   let lsDirSpy: jasmine.Spy;
39   let modalShowSpy: jasmine.Spy;
40   let notificationShowSpy: jasmine.Spy;
41   let minValidator: jasmine.Spy;
42   let maxValidator: jasmine.Spy;
43   let minBinaryValidator: jasmine.Spy;
44   let maxBinaryValidator: jasmine.Spy;
45   let modal: any;
46
47   // Get's private attributes or functions
48   const get = {
49     nodeIds: (): { [path: string]: CephfsDir } => component['nodeIds'],
50     dirs: (): CephfsDir[] => component['dirs'],
51     requestedPaths: (): string[] => component['requestedPaths']
52   };
53
54   // Object contains mock data that will be reset before each test.
55   let mockData: {
56     nodes: TreeModel[];
57     parent: Tree;
58     createdSnaps: CephfsSnapshot[] | any[];
59     deletedSnaps: CephfsSnapshot[] | any[];
60     updatedQuotas: { [path: string]: CephfsQuotas };
61   };
62
63   // Object contains mock functions
64   const mockLib = {
65     quotas: (max_bytes: number, max_files: number): CephfsQuotas => ({ max_bytes, max_files }),
66     snapshots: (dirPath: string, howMany: number): CephfsSnapshot[] => {
67       const name = 'someSnapshot';
68       const snapshots = [];
69       const oneDay = 3600 * 24 * 1000;
70       for (let i = 0; i < howMany; i++) {
71         const snapName = `${name}${i + 1}`;
72         const path = `${dirPath}/.snap/${snapName}`;
73         const created = new Date(+new Date() - oneDay * i).toString();
74         snapshots.push({ name: snapName, path, created });
75       }
76       return snapshots;
77     },
78     dir: (parentPath: string, name: string, modifier: number): CephfsDir => {
79       const dirPath = `${parentPath === '/' ? '' : parentPath}/${name}`;
80       let snapshots = mockLib.snapshots(parentPath, modifier);
81       const extraSnapshots = mockData.createdSnaps.filter((s) => s.path === dirPath);
82       if (extraSnapshots.length > 0) {
83         snapshots = snapshots.concat(extraSnapshots);
84       }
85       const deletedSnapshots = mockData.deletedSnaps
86         .filter((s) => s.path === dirPath)
87         .map((s) => s.name);
88       if (deletedSnapshots.length > 0) {
89         snapshots = snapshots.filter((s) => !deletedSnapshots.includes(s.name));
90       }
91       return {
92         name,
93         path: dirPath,
94         parent: parentPath,
95         quotas: Object.assign(
96           mockLib.quotas(1024 * modifier, 10 * modifier),
97           mockData.updatedQuotas[dirPath] || {}
98         ),
99         snapshots: snapshots
100       };
101     },
102     // Only used inside other mocks
103     lsSingleDir: (path = ''): CephfsDir[] => {
104       if (path.includes('b')) {
105         // 'b' has no sub directories
106         return [];
107       }
108       return [
109         // Directories are not sorted!
110         mockLib.dir(path, 'c', 3),
111         mockLib.dir(path, 'a', 1),
112         mockLib.dir(path, 'b', 2)
113       ];
114     },
115     lsDir: (_id: number, path = '') => {
116       // will return 2 levels deep
117       let data = mockLib.lsSingleDir(path);
118       const paths = data.map((dir) => dir.path);
119       paths.forEach((pathL2) => {
120         data = data.concat(mockLib.lsSingleDir(pathL2));
121       });
122       return of(data);
123     },
124     mkSnapshot: (_id: any, path: string, name: string) => {
125       mockData.createdSnaps.push({
126         name,
127         path,
128         created: new Date().toString()
129       });
130       return of(name);
131     },
132     rmSnapshot: (_id: any, path: string, name: string) => {
133       mockData.deletedSnaps.push({
134         name,
135         path,
136         created: new Date().toString()
137       });
138       return of(name);
139     },
140     updateQuota: (_id: any, path: string, updated: CephfsQuotas) => {
141       mockData.updatedQuotas[path] = Object.assign(mockData.updatedQuotas[path] || {}, updated);
142       return of('Response');
143     },
144     modalShow: (comp: any, init: any) => {
145       modal = modalServiceShow(comp, init);
146       return modal.ref;
147     },
148     getControllerByPath: (path: string) => {
149       return {
150         expand: () => mockLib.expand(path),
151         select: () => component.onNodeSelected(mockLib.getNodeEvent(path))
152       };
153     },
154     // Only used inside other mocks to mock "tree.expand" of every node
155     expand: (path: string) => {
156       component.updateDirectory(path, (nodes) => {
157         mockData.nodes = mockData.nodes.concat(nodes);
158       });
159     },
160     getNodeEvent: (path: string): NodeEvent => {
161       const tree = mockData.nodes.find((n) => n.id === path) as Tree;
162       if (mockData.parent) {
163         tree.parent = mockData.parent;
164       } else {
165         const dir = get.nodeIds()[path];
166         const parentNode = mockData.nodes.find((n) => n.id === dir.parent);
167         tree.parent = parentNode as Tree;
168       }
169       return { node: tree } as NodeEvent;
170     },
171     changeId: (id: number) => {
172       component.id = id;
173       component.ngOnChanges();
174       mockData.nodes = [component.tree].concat(component.tree.children);
175     },
176     selectNode: (path: string) => {
177       mockLib.getControllerByPath(path).select();
178     },
179     mkDir: (path: string, name: string, maxFiles: number, maxBytes: number) => {
180       const dir = mockLib.dir(path, name, 3);
181       dir.quotas.max_bytes = maxBytes * 1024;
182       dir.quotas.max_files = maxFiles;
183       get.nodeIds()[dir.path] = dir;
184       mockData.nodes.push({
185         id: dir.path,
186         value: name
187       });
188     },
189     createSnapshotThroughModal: (name: string) => {
190       component.createSnapshot();
191       modal.component.onSubmitForm({ name });
192     },
193     deleteSnapshotsThroughModal: (snapshots: CephfsSnapshot[]) => {
194       component.snapshot.selection.selected = snapshots;
195       component.deleteSnapshotModal();
196       modal.component.callSubmitAction();
197     },
198     updateQuotaThroughModal: (attribute: string, value: number) => {
199       component.quota.selection.selected = component.settings.filter(
200         (q) => q.quotaKey === attribute
201       );
202       component.updateQuotaModal();
203       modal.component.onSubmitForm({ [attribute]: value });
204     },
205     unsetQuotaThroughModal: (attribute: string) => {
206       component.quota.selection.selected = component.settings.filter(
207         (q) => q.quotaKey === attribute
208       );
209       component.unsetQuotaModal();
210       modal.component.onSubmit();
211     },
212     setFourQuotaDirs: (quotas: number[][]) => {
213       expect(quotas.length).toBe(4); // Make sure this function is used correctly
214       let path = '';
215       quotas.forEach((quota, index) => {
216         index += 1;
217         mockLib.mkDir(path, index.toString(), quota[0], quota[1]);
218         path += '/' + index;
219       });
220       mockData.parent = {
221         value: '3',
222         id: '/1/2/3',
223         parent: {
224           value: '2',
225           id: '/1/2',
226           parent: {
227             value: '1',
228             id: '/1',
229             parent: { value: '/', id: '/' }
230           }
231         }
232       } as Tree;
233       mockLib.selectNode('/1/2/3/4');
234     }
235   };
236
237   // Expects that are used frequently
238   const assert = {
239     dirLength: (n: number) => expect(get.dirs().length).toBe(n),
240     nodeLength: (n: number) => expect(mockData.nodes.length).toBe(n),
241     lsDirCalledTimes: (n: number) => expect(lsDirSpy).toHaveBeenCalledTimes(n),
242     requestedPaths: (expected: string[]) => expect(get.requestedPaths()).toEqual(expected),
243     snapshotsByName: (snaps: string[]) =>
244       expect(component.selectedDir.snapshots.map((s) => s.name)).toEqual(snaps),
245     dirQuotas: (bytes: number, files: number) => {
246       expect(component.selectedDir.quotas).toEqual({ max_bytes: bytes, max_files: files });
247     },
248     noQuota: (key: 'bytes' | 'files') => {
249       assert.quotaRow(key, '', 0, '');
250     },
251     quotaIsNotInherited: (key: 'bytes' | 'files', shownValue: any, nextMaximum: number) => {
252       const dir = component.selectedDir;
253       const path = dir.path;
254       assert.quotaRow(key, shownValue, nextMaximum, path);
255     },
256     quotaIsInherited: (key: 'bytes' | 'files', shownValue: any, path: string) => {
257       const isBytes = key === 'bytes';
258       const nextMaximum = get.nodeIds()[path].quotas[isBytes ? 'max_bytes' : 'max_files'];
259       assert.quotaRow(key, shownValue, nextMaximum, path);
260     },
261     quotaRow: (
262       key: 'bytes' | 'files',
263       shownValue: number | string,
264       nextTreeMaximum: number,
265       originPath: string
266     ) => {
267       const isBytes = key === 'bytes';
268       expect(component.settings[isBytes ? 1 : 0]).toEqual({
269         row: {
270           name: `Max ${isBytes ? 'size' : key}`,
271           value: shownValue,
272           originPath
273         },
274         quotaKey: `max_${key}`,
275         dirValue: expect.any(Number),
276         nextTreeMaximum: {
277           value: nextTreeMaximum,
278           path: expect.any(String)
279         }
280       });
281     },
282     quotaUnsetModalTexts: (titleText: string, message: string, notificationMsg: string) => {
283       expect(modalShowSpy).toHaveBeenCalledWith(ConfirmationModalComponent, {
284         initialState: expect.objectContaining({
285           titleText,
286           description: message,
287           buttonText: 'Unset'
288         })
289       });
290       expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
291     },
292     quotaUpdateModalTexts: (titleText: string, message: string, notificationMsg: string) => {
293       expect(modalShowSpy).toHaveBeenCalledWith(FormModalComponent, {
294         initialState: expect.objectContaining({
295           titleText,
296           message,
297           submitButtonText: 'Save'
298         })
299       });
300       expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
301     },
302     quotaUpdateModalField: (
303       type: string,
304       label: string,
305       key: string,
306       value: number,
307       max: number,
308       errors?: { [key: string]: string }
309     ) => {
310       expect(modalShowSpy).toHaveBeenCalledWith(FormModalComponent, {
311         initialState: expect.objectContaining({
312           fields: [
313             {
314               type,
315               label,
316               errors,
317               name: key,
318               value,
319               validators: expect.anything(),
320               required: true
321             }
322           ]
323         })
324       });
325       if (type === 'binary') {
326         expect(minBinaryValidator).toHaveBeenCalledWith(0);
327         expect(maxBinaryValidator).toHaveBeenCalledWith(max);
328       } else {
329         expect(minValidator).toHaveBeenCalledWith(0);
330         expect(maxValidator).toHaveBeenCalledWith(max);
331       }
332     }
333   };
334
335   configureTestBed({
336     imports: [
337       HttpClientTestingModule,
338       SharedModule,
339       RouterTestingModule,
340       TreeModule,
341       NgBootstrapFormValidationModule.forRoot(),
342       ToastrModule.forRoot(),
343       ModalModule.forRoot()
344     ],
345     declarations: [CephfsDirectoriesComponent],
346     providers: [i18nProviders, BsModalRef]
347   });
348
349   beforeEach(() => {
350     mockData = {
351       nodes: undefined,
352       parent: undefined,
353       createdSnaps: [],
354       deletedSnaps: [],
355       updatedQuotas: {}
356     };
357
358     cephfsService = TestBed.get(CephfsService);
359     lsDirSpy = spyOn(cephfsService, 'lsDir').and.callFake(mockLib.lsDir);
360     spyOn(cephfsService, 'mkSnapshot').and.callFake(mockLib.mkSnapshot);
361     spyOn(cephfsService, 'rmSnapshot').and.callFake(mockLib.rmSnapshot);
362     spyOn(cephfsService, 'updateQuota').and.callFake(mockLib.updateQuota);
363
364     modalShowSpy = spyOn(TestBed.get(BsModalService), 'show').and.callFake(mockLib.modalShow);
365     notificationShowSpy = spyOn(TestBed.get(NotificationService), 'show').and.stub();
366
367     fixture = TestBed.createComponent(CephfsDirectoriesComponent);
368     component = fixture.componentInstance;
369     fixture.detectChanges();
370
371     spyOn(component.treeComponent, 'getControllerByNodeId').and.callFake((id) =>
372       mockLib.getControllerByPath(id)
373     );
374   });
375
376   it('should create', () => {
377     expect(component).toBeTruthy();
378   });
379
380   describe('mock self test', () => {
381     it('tests snapshots mock', () => {
382       expect(mockLib.snapshots('/a', 1).map((s) => ({ name: s.name, path: s.path }))).toEqual([
383         {
384           name: 'someSnapshot1',
385           path: '/a/.snap/someSnapshot1'
386         }
387       ]);
388       expect(mockLib.snapshots('/a/b', 3).map((s) => ({ name: s.name, path: s.path }))).toEqual([
389         {
390           name: 'someSnapshot1',
391           path: '/a/b/.snap/someSnapshot1'
392         },
393         {
394           name: 'someSnapshot2',
395           path: '/a/b/.snap/someSnapshot2'
396         },
397         {
398           name: 'someSnapshot3',
399           path: '/a/b/.snap/someSnapshot3'
400         }
401       ]);
402     });
403
404     it('tests dir mock', () => {
405       const path = '/a/b/c';
406       mockData.createdSnaps = [{ path, name: 's1' }, { path, name: 's2' }];
407       mockData.deletedSnaps = [{ path, name: 'someSnapshot2' }, { path, name: 's2' }];
408       const dir = mockLib.dir('/a/b', 'c', 2);
409       expect(dir.path).toBe('/a/b/c');
410       expect(dir.parent).toBe('/a/b');
411       expect(dir.quotas).toEqual({ max_bytes: 2048, max_files: 20 });
412       expect(dir.snapshots.map((s) => s.name)).toEqual(['someSnapshot1', 's1']);
413     });
414
415     it('tests lsdir mock', () => {
416       let dirs: CephfsDir[] = [];
417       mockLib.lsDir(2, '/a').subscribe((x) => (dirs = x));
418       expect(dirs.map((d) => d.path)).toEqual([
419         '/a/c',
420         '/a/a',
421         '/a/b',
422         '/a/c/c',
423         '/a/c/a',
424         '/a/c/b',
425         '/a/a/c',
426         '/a/a/a',
427         '/a/a/b'
428       ]);
429     });
430
431     describe('test quota update mock', () => {
432       const PATH = '/a';
433       const ID = 2;
434
435       const updateQuota = (quotas: CephfsQuotas) => mockLib.updateQuota(ID, PATH, quotas);
436
437       const expectMockUpdate = (max_bytes?: number, max_files?: number) =>
438         expect(mockData.updatedQuotas[PATH]).toEqual({
439           max_bytes,
440           max_files
441         });
442
443       const expectLsUpdate = (max_bytes?: number, max_files?: number) => {
444         let dir: CephfsDir;
445         mockLib.lsDir(ID, '/').subscribe((dirs) => (dir = dirs.find((d) => d.path === PATH)));
446         expect(dir.quotas).toEqual({
447           max_bytes,
448           max_files
449         });
450       };
451
452       it('tests to set quotas', () => {
453         expectLsUpdate(1024, 10);
454
455         updateQuota({ max_bytes: 512 });
456         expectMockUpdate(512);
457         expectLsUpdate(512, 10);
458
459         updateQuota({ max_files: 100 });
460         expectMockUpdate(512, 100);
461         expectLsUpdate(512, 100);
462       });
463
464       it('tests to unset quotas', () => {
465         updateQuota({ max_files: 0 });
466         expectMockUpdate(undefined, 0);
467         expectLsUpdate(1024, 0);
468
469         updateQuota({ max_bytes: 0 });
470         expectMockUpdate(0, 0);
471         expectLsUpdate(0, 0);
472       });
473     });
474   });
475
476   it('calls lsDir only if an id exits', () => {
477     component.ngOnChanges();
478     assert.lsDirCalledTimes(0);
479
480     mockLib.changeId(1);
481     assert.lsDirCalledTimes(1);
482     expect(lsDirSpy).toHaveBeenCalledWith(1, '/');
483
484     mockLib.changeId(2);
485     assert.lsDirCalledTimes(2);
486     expect(lsDirSpy).toHaveBeenCalledWith(2, '/');
487   });
488
489   describe('listing sub directories', () => {
490     beforeEach(() => {
491       mockLib.changeId(1);
492       /**
493        * Tree looks like this:
494        * v /
495        *   > a
496        *   * b
497        *   > c
498        * */
499     });
500
501     it('expands first level', () => {
502       // Tree will only show '*' if nor 'loadChildren' or 'children' are defined
503       expect(
504         mockData.nodes.map((node) => ({ [node.id]: Boolean(node.loadChildren || node.children) }))
505       ).toEqual([{ '/': true }, { '/a': true }, { '/b': false }, { '/c': true }]);
506     });
507
508     it('resets all dynamic content on id change', () => {
509       mockLib.selectNode('/a');
510       /**
511        * Tree looks like this:
512        * v /
513        *   v a <- Selected
514        *     > a
515        *     * b
516        *     > c
517        *   * b
518        *   > c
519        * */
520       assert.requestedPaths(['/', '/a']);
521       assert.nodeLength(7);
522       assert.dirLength(15);
523       expect(component.selectedDir).toBeDefined();
524
525       mockLib.changeId(undefined);
526       assert.dirLength(0);
527       assert.requestedPaths([]);
528       expect(component.selectedDir).not.toBeDefined();
529     });
530
531     it('should select a node and show the directory contents', () => {
532       mockLib.selectNode('/a');
533       const dir = get.dirs().find((d) => d.path === '/a');
534       expect(component.selectedDir).toEqual(dir);
535       assert.quotaIsNotInherited('files', 10, 0);
536       assert.quotaIsNotInherited('bytes', '1 KiB', 0);
537     });
538
539     it('should extend the list by subdirectories when expanding and omit already called path', () => {
540       mockLib.selectNode('/a');
541       mockLib.selectNode('/a/c');
542       /**
543        * Tree looks like this:
544        * v /
545        *   v a
546        *     > a
547        *     * b
548        *     v c <- Selected
549        *       > a
550        *       * b
551        *       > c
552        *   * b
553        *   > c
554        * */
555       assert.lsDirCalledTimes(3);
556       assert.requestedPaths(['/', '/a', '/a/c']);
557       assert.dirLength(21);
558       assert.nodeLength(10);
559     });
560
561     it('should select parent by path', () => {
562       mockLib.selectNode('/a');
563       mockLib.selectNode('/a/c');
564       mockLib.selectNode('/a/c/a');
565       component.selectOrigin('/a');
566       expect(component.selectedDir.path).toBe('/a');
567     });
568
569     it('should omit call for directories that have no sub directories', () => {
570       mockLib.selectNode('/b');
571       /**
572        * Tree looks like this:
573        * v /
574        *   > a
575        *   * b <- Selected
576        *   > c
577        * */
578       assert.lsDirCalledTimes(1);
579       assert.requestedPaths(['/']);
580       assert.nodeLength(4);
581     });
582
583     describe('used quotas', () => {
584       it('should use no quota if none is set', () => {
585         mockLib.setFourQuotaDirs([[0, 0], [0, 0], [0, 0], [0, 0]]);
586         assert.noQuota('files');
587         assert.noQuota('bytes');
588         assert.dirQuotas(0, 0);
589       });
590
591       it('should use quota from upper parents', () => {
592         mockLib.setFourQuotaDirs([[100, 0], [0, 8], [0, 0], [0, 0]]);
593         assert.quotaIsInherited('files', 100, '/1');
594         assert.quotaIsInherited('bytes', '8 KiB', '/1/2');
595         assert.dirQuotas(0, 0);
596       });
597
598       it('should use quota from the parent with the lowest value (deep inheritance)', () => {
599         mockLib.setFourQuotaDirs([[200, 1], [100, 4], [400, 3], [300, 2]]);
600         assert.quotaIsInherited('files', 100, '/1/2');
601         assert.quotaIsInherited('bytes', '1 KiB', '/1');
602         assert.dirQuotas(2048, 300);
603       });
604
605       it('should use current value', () => {
606         mockLib.setFourQuotaDirs([[200, 2], [300, 4], [400, 3], [100, 1]]);
607         assert.quotaIsNotInherited('files', 100, 200);
608         assert.quotaIsNotInherited('bytes', '1 KiB', 2048);
609         assert.dirQuotas(1024, 100);
610       });
611     });
612   });
613
614   describe('snapshots', () => {
615     beforeEach(() => {
616       mockLib.changeId(1);
617       mockLib.selectNode('/a');
618     });
619
620     it('should create a snapshot', () => {
621       mockLib.createSnapshotThroughModal('newSnap');
622       expect(cephfsService.mkSnapshot).toHaveBeenCalledWith(1, '/a', 'newSnap');
623       assert.snapshotsByName(['someSnapshot1', 'newSnap']);
624     });
625
626     it('should delete a snapshot', () => {
627       mockLib.createSnapshotThroughModal('deleteMe');
628       mockLib.deleteSnapshotsThroughModal([component.selectedDir.snapshots[1]]);
629       assert.snapshotsByName(['someSnapshot1']);
630     });
631
632     it('should delete all snapshots', () => {
633       mockLib.createSnapshotThroughModal('deleteAll');
634       mockLib.deleteSnapshotsThroughModal(component.selectedDir.snapshots);
635       assert.snapshotsByName([]);
636     });
637   });
638
639   it('should test all snapshot table actions combinations', () => {
640     const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
641     const tableActions = permissionHelper.setPermissionsAndGetActions(
642       component.snapshot.tableActions
643     );
644
645     expect(tableActions).toEqual({
646       'create,update,delete': {
647         actions: ['Create', 'Delete'],
648         primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
649       },
650       'create,update': {
651         actions: ['Create'],
652         primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
653       },
654       'create,delete': {
655         actions: ['Create', 'Delete'],
656         primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
657       },
658       create: {
659         actions: ['Create'],
660         primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
661       },
662       'update,delete': {
663         actions: ['Delete'],
664         primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
665       },
666       update: {
667         actions: [],
668         primary: { multiple: '', executing: '', single: '', no: '' }
669       },
670       delete: {
671         actions: ['Delete'],
672         primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
673       },
674       'no-permissions': {
675         actions: [],
676         primary: { multiple: '', executing: '', single: '', no: '' }
677       }
678     });
679   });
680
681   describe('quotas', () => {
682     beforeEach(() => {
683       // Spies
684       minValidator = spyOn(Validators, 'min').and.callThrough();
685       maxValidator = spyOn(Validators, 'max').and.callThrough();
686       minBinaryValidator = spyOn(CdValidators, 'binaryMin').and.callThrough();
687       maxBinaryValidator = spyOn(CdValidators, 'binaryMax').and.callThrough();
688       // Select /a/c/b
689       mockLib.changeId(1);
690       mockLib.selectNode('/a');
691       mockLib.selectNode('/a/c');
692       mockLib.selectNode('/a/c/b');
693       // Quotas after selection
694       assert.quotaIsInherited('files', 10, '/a');
695       assert.quotaIsInherited('bytes', '1 KiB', '/a');
696       assert.dirQuotas(2048, 20);
697     });
698
699     describe('update modal', () => {
700       describe('max_files', () => {
701         beforeEach(() => {
702           mockLib.updateQuotaThroughModal('max_files', 5);
703         });
704
705         it('should update max_files correctly', () => {
706           expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 5 });
707           assert.quotaIsNotInherited('files', 5, 10);
708         });
709
710         it('uses the correct form field', () => {
711           assert.quotaUpdateModalField('number', 'Max files', 'max_files', 20, 10, {
712             min: 'Value has to be at least 0 or more',
713             max: 'Value has to be at most 10 or less'
714           });
715         });
716
717         it('shows the right texts', () => {
718           assert.quotaUpdateModalTexts(
719             "Update CephFS files quota for '/a/c/b'",
720             "The inherited files quota 10 from '/a' is the maximum value to be used.",
721             "Updated CephFS files quota for '/a/c/b'"
722           );
723         });
724       });
725
726       describe('max_bytes', () => {
727         beforeEach(() => {
728           mockLib.updateQuotaThroughModal('max_bytes', 512);
729         });
730
731         it('should update max_files correctly', () => {
732           expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 512 });
733           assert.quotaIsNotInherited('bytes', '512 B', 1024);
734         });
735
736         it('uses the correct form field', () => {
737           mockLib.updateQuotaThroughModal('max_bytes', 512);
738           assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 2048, 1024);
739         });
740
741         it('shows the right texts', () => {
742           assert.quotaUpdateModalTexts(
743             "Update CephFS size quota for '/a/c/b'",
744             "The inherited size quota 1 KiB from '/a' is the maximum value to be used.",
745             "Updated CephFS size quota for '/a/c/b'"
746           );
747         });
748       });
749
750       describe('action behaviour', () => {
751         it('opens with next maximum as maximum if directory holds the current maximum', () => {
752           mockLib.updateQuotaThroughModal('max_bytes', 512);
753           mockLib.updateQuotaThroughModal('max_bytes', 888);
754           assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 512, 1024);
755         });
756
757         it("uses 'Set' action instead of 'Update' if the quota is not set (0)", () => {
758           mockLib.updateQuotaThroughModal('max_bytes', 0);
759           mockLib.updateQuotaThroughModal('max_bytes', 200);
760           assert.quotaUpdateModalTexts(
761             "Set CephFS size quota for '/a/c/b'",
762             "The inherited size quota 1 KiB from '/a' is the maximum value to be used.",
763             "Set CephFS size quota for '/a/c/b'"
764           );
765         });
766       });
767     });
768
769     describe('unset modal', () => {
770       describe('max_files', () => {
771         beforeEach(() => {
772           mockLib.updateQuotaThroughModal('max_files', 5); // Sets usable quota
773           mockLib.unsetQuotaThroughModal('max_files');
774         });
775
776         it('should unset max_files correctly', () => {
777           expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 0 });
778           assert.dirQuotas(2048, 0);
779         });
780
781         it('shows the right texts', () => {
782           assert.quotaUnsetModalTexts(
783             "Unset CephFS files quota for '/a/c/b'",
784             "Unset files quota 5 from '/a/c/b' in order to inherit files quota 10 from '/a'.",
785             "Unset CephFS files quota for '/a/c/b'"
786           );
787         });
788       });
789
790       describe('max_bytes', () => {
791         beforeEach(() => {
792           mockLib.updateQuotaThroughModal('max_bytes', 512); // Sets usable quota
793           mockLib.unsetQuotaThroughModal('max_bytes');
794         });
795
796         it('should unset max_files correctly', () => {
797           expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 0 });
798           assert.dirQuotas(0, 20);
799         });
800
801         it('shows the right texts', () => {
802           assert.quotaUnsetModalTexts(
803             "Unset CephFS size quota for '/a/c/b'",
804             "Unset size quota 512 B from '/a/c/b' in order to inherit size quota 1 KiB from '/a'.",
805             "Unset CephFS size quota for '/a/c/b'"
806           );
807         });
808       });
809
810       describe('action behaviour', () => {
811         it('uses different Text if no quota is inherited', () => {
812           mockLib.selectNode('/a');
813           mockLib.unsetQuotaThroughModal('max_bytes');
814           assert.quotaUnsetModalTexts(
815             "Unset CephFS size quota for '/a'",
816             "Unset size quota 1 KiB from '/a' in order to have no quota on the directory.",
817             "Unset CephFS size quota for '/a'"
818           );
819         });
820
821         it('uses different Text if quota is already inherited', () => {
822           mockLib.unsetQuotaThroughModal('max_bytes');
823           assert.quotaUnsetModalTexts(
824             "Unset CephFS size quota for '/a/c/b'",
825             "Unset size quota 2 KiB from '/a/c/b' which isn't used because of the inheritance " +
826               "of size quota 1 KiB from '/a'.",
827             "Unset CephFS size quota for '/a/c/b'"
828           );
829         });
830       });
831     });
832   });
833
834   describe('table actions', () => {
835     let actions: CdTableAction[];
836
837     const empty = (): CdTableSelection => {
838       const selection = new CdTableSelection();
839       return selection;
840     };
841
842     const select = (value: number): CdTableSelection => {
843       const selection = new CdTableSelection();
844       selection.selected = [{ dirValue: value }];
845       return selection;
846     };
847
848     beforeEach(() => {
849       actions = component.quota.tableActions;
850     });
851
852     it("shows 'Set' for empty and not set quotas", () => {
853       const isSetVisible = actions[0].visible;
854       expect(isSetVisible(empty())).toBe(true);
855       expect(isSetVisible(select(0))).toBe(true);
856       expect(isSetVisible(select(1))).toBe(false);
857     });
858
859     it("shows 'Update' for set quotas only", () => {
860       const isUpdateVisible = actions[1].visible;
861       expect(isUpdateVisible(empty())).toBeFalsy();
862       expect(isUpdateVisible(select(0))).toBe(false);
863       expect(isUpdateVisible(select(1))).toBe(true);
864     });
865
866     it("only enables 'Unset' for set quotas only", () => {
867       const isUnsetDisabled = actions[2].disable;
868       expect(isUnsetDisabled(empty())).toBe(true);
869       expect(isUnsetDisabled(select(0))).toBe(true);
870       expect(isUnsetDisabled(select(1))).toBe(false);
871     });
872
873     it('should test all quota table actions permission combinations', () => {
874       const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
875       const tableActions = permissionHelper.setPermissionsAndGetActions(
876         component.quota.tableActions
877       );
878
879       expect(tableActions).toEqual({
880         'create,update,delete': {
881           actions: ['Set', 'Update', 'Unset'],
882           primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
883         },
884         'create,update': {
885           actions: ['Set', 'Update', 'Unset'],
886           primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
887         },
888         'create,delete': {
889           actions: [],
890           primary: { multiple: '', executing: '', single: '', no: '' }
891         },
892         create: {
893           actions: [],
894           primary: { multiple: '', executing: '', single: '', no: '' }
895         },
896         'update,delete': {
897           actions: ['Set', 'Update', 'Unset'],
898           primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
899         },
900         update: {
901           actions: ['Set', 'Update', 'Unset'],
902           primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
903         },
904         delete: {
905           actions: [],
906           primary: { multiple: '', executing: '', single: '', no: '' }
907         },
908         'no-permissions': {
909           actions: [],
910           primary: { multiple: '', executing: '', single: '', no: '' }
911         }
912       });
913     });
914   });
915 });