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';
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';
18 } from '../../../../testing/unit-test-helper';
19 import { CephfsService } from '../../../shared/api/cephfs.service';
20 import { ConfirmationModalComponent } from '../../../shared/components/confirmation-modal/confirmation-modal.component';
21 import { CriticalConfirmationModalComponent } from '../../../shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
22 import { FormModalComponent } from '../../../shared/components/form-modal/form-modal.component';
23 import { NotificationType } from '../../../shared/enum/notification-type.enum';
24 import { CdValidators } from '../../../shared/forms/cd-validators';
25 import { CdTableAction } from '../../../shared/models/cd-table-action';
26 import { CdTableSelection } from '../../../shared/models/cd-table-selection';
31 } from '../../../shared/models/cephfs-directory-models';
32 import { ModalService } from '../../../shared/services/modal.service';
33 import { NotificationService } from '../../../shared/services/notification.service';
34 import { SharedModule } from '../../../shared/shared.module';
35 import { CephfsDirectoriesComponent } from './cephfs-directories.component';
37 describe('CephfsDirectoriesComponent', () => {
38 let component: CephfsDirectoriesComponent;
39 let fixture: ComponentFixture<CephfsDirectoriesComponent>;
40 let cephfsService: CephfsService;
41 let noAsyncUpdate: boolean;
42 let lsDirSpy: jasmine.Spy;
43 let modalShowSpy: jasmine.Spy;
44 let notificationShowSpy: jasmine.Spy;
45 let minValidator: jasmine.Spy;
46 let maxValidator: jasmine.Spy;
47 let minBinaryValidator: jasmine.Spy;
48 let maxBinaryValidator: jasmine.Spy;
49 let modal: NgbModalRef;
51 // Get's private attributes or functions
53 nodeIds: (): { [path: string]: CephfsDir } => component['nodeIds'],
54 dirs: (): CephfsDir[] => component['dirs'],
55 requestedPaths: (): string[] => component['requestedPaths']
58 // Object contains mock data that will be reset before each test.
62 createdSnaps: CephfsSnapshot[] | any[];
63 deletedSnaps: CephfsSnapshot[] | any[];
64 updatedQuotas: { [path: string]: CephfsQuotas };
65 createdDirs: CephfsDir[];
68 // Object contains mock functions
70 quotas: (max_bytes: number, max_files: number): CephfsQuotas => ({ max_bytes, max_files }),
71 snapshots: (dirPath: string, howMany: number): CephfsSnapshot[] => {
72 const name = 'someSnapshot';
74 const oneDay = 3600 * 24 * 1000;
75 for (let i = 0; i < howMany; i++) {
76 const snapName = `${name}${i + 1}`;
77 const path = `${dirPath}/.snap/${snapName}`;
78 const created = new Date(+new Date() - oneDay * i).toString();
79 snapshots.push({ name: snapName, path, created });
83 dir: (parentPath: string, name: string, modifier: number): CephfsDir => {
84 const dirPath = `${parentPath === '/' ? '' : parentPath}/${name}`;
85 let snapshots = mockLib.snapshots(parentPath, modifier);
86 const extraSnapshots = mockData.createdSnaps.filter((s) => s.path === dirPath);
87 if (extraSnapshots.length > 0) {
88 snapshots = snapshots.concat(extraSnapshots);
90 const deletedSnapshots = mockData.deletedSnaps
91 .filter((s) => s.path === dirPath)
93 if (deletedSnapshots.length > 0) {
94 snapshots = snapshots.filter((s) => !deletedSnapshots.includes(s.name));
100 quotas: Object.assign(
101 mockLib.quotas(1024 * modifier, 10 * modifier),
102 mockData.updatedQuotas[dirPath] || {}
107 // Only used inside other mocks
108 lsSingleDir: (path = ''): CephfsDir[] => {
109 const customDirs = mockData.createdDirs.filter((d) => d.parent === path);
110 const isCustomDir = mockData.createdDirs.some((d) => d.path === path);
111 if (isCustomDir || path.includes('b')) {
112 // 'b' has no sub directories
115 return customDirs.concat([
116 // Directories are not sorted!
117 mockLib.dir(path, 'c', 3),
118 mockLib.dir(path, 'a', 1),
119 mockLib.dir(path, 'b', 2)
122 lsDir: (_id: number, path = ''): Observable<CephfsDir[]> => {
123 // will return 2 levels deep
124 let data = mockLib.lsSingleDir(path);
125 const paths = data.map((dir) => dir.path);
126 paths.forEach((pathL2) => {
127 data = data.concat(mockLib.lsSingleDir(pathL2));
129 if (path === '' || path === '/') {
130 // Adds root directory on ls of '/' to the directories list.
131 const root = mockLib.dir(path, '/', 1);
133 root.parent = undefined;
134 root.quotas = undefined;
135 data = [root].concat(data);
139 mkSnapshot: (_id: any, path: string, name: string): Observable<string> => {
140 mockData.createdSnaps.push({
143 created: new Date().toString()
147 rmSnapshot: (_id: any, path: string, name: string): Observable<string> => {
148 mockData.deletedSnaps.push({
151 created: new Date().toString()
155 updateQuota: (_id: any, path: string, updated: CephfsQuotas): Observable<string> => {
156 mockData.updatedQuotas[path] = Object.assign(mockData.updatedQuotas[path] || {}, updated);
157 return of('Response');
159 modalShow: (comp: Type<any>, init: any): any => {
160 modal = modalServiceShow(comp, init);
163 getNodeById: (path: string) => {
164 return mockLib.useNode(path);
166 updateNodes: (path: string) => {
167 const p: Promise<any[]> = component.treeOptions.getChildren({ id: path });
168 return noAsyncUpdate ? () => p : mockLib.asyncNodeUpdate(p);
170 asyncNodeUpdate: fakeAsync((p: Promise<any[]>) => {
172 mockData.nodes = mockData.nodes.concat(nodes);
176 changeId: (id: number) => {
177 // For some reason this spy has to be renewed after usage
178 spyOn(global, 'setTimeout').and.callFake((fn) => fn());
180 component.ngOnChanges();
181 mockData.nodes = component.nodes.concat(mockData.nodes);
183 selectNode: (path: string) => {
184 component.treeOptions.actionMapping.mouse.click(undefined, mockLib.useNode(path), undefined);
186 // Creates TreeNode with parents until root
187 useNode: (path: string): { id: string; parent: any; data: any; loadNodeChildren: Function } => {
188 const parentPath = path.split('/');
190 const parentIsRoot = parentPath.length === 1;
191 const parent = parentIsRoot ? { id: '/' } : mockLib.useNode(parentPath.join('/'));
196 loadNodeChildren: () => mockLib.updateNodes(path)
200 toggleActive: (_a: any, node: any, _b: any) => {
201 return mockLib.updateNodes(node.id);
204 mkDir: (path: string, name: string, maxFiles: number, maxBytes: number) => {
205 const dir = mockLib.dir(path, name, 3);
206 dir.quotas.max_bytes = maxBytes * 1024;
207 dir.quotas.max_files = maxFiles;
208 mockData.createdDirs.push(dir);
209 // Below is needed for quota tests only where 4 dirs are mocked
210 get.nodeIds()[dir.path] = dir;
211 mockData.nodes.push({ id: dir.path });
213 createSnapshotThroughModal: (name: string) => {
214 component.createSnapshot();
215 modal.componentInstance.onSubmitForm({ name });
217 deleteSnapshotsThroughModal: (snapshots: CephfsSnapshot[]) => {
218 component.snapshot.selection.selected = snapshots;
219 component.deleteSnapshotModal();
220 modal.componentInstance.callSubmitAction();
222 updateQuotaThroughModal: (attribute: string, value: number) => {
223 component.quota.selection.selected = component.settings.filter(
224 (q) => q.quotaKey === attribute
226 component.updateQuotaModal();
227 modal.componentInstance.onSubmitForm({ [attribute]: value });
229 unsetQuotaThroughModal: (attribute: string) => {
230 component.quota.selection.selected = component.settings.filter(
231 (q) => q.quotaKey === attribute
233 component.unsetQuotaModal();
234 modal.componentInstance.onSubmit();
236 setFourQuotaDirs: (quotas: number[][]) => {
237 expect(quotas.length).toBe(4); // Make sure this function is used correctly
239 quotas.forEach((quota, index) => {
241 mockLib.mkDir(path === '' ? '/' : path, index.toString(), quota[0], quota[1]);
253 parent: { value: '/', id: '/' }
257 mockLib.selectNode('/1/2/3/4');
261 // Expects that are used frequently
263 dirLength: (n: number) => expect(get.dirs().length).toBe(n),
264 nodeLength: (n: number) => expect(mockData.nodes.length).toBe(n),
265 lsDirCalledTimes: (n: number) => expect(lsDirSpy).toHaveBeenCalledTimes(n),
266 lsDirHasBeenCalledWith: (id: number, paths: string[]) => {
267 paths.forEach((path) => expect(lsDirSpy).toHaveBeenCalledWith(id, path));
268 assert.lsDirCalledTimes(paths.length);
270 requestedPaths: (expected: string[]) => expect(get.requestedPaths()).toEqual(expected),
271 snapshotsByName: (snaps: string[]) =>
272 expect(component.selectedDir.snapshots.map((s) => s.name)).toEqual(snaps),
273 dirQuotas: (bytes: number, files: number) => {
274 expect(component.selectedDir.quotas).toEqual({ max_bytes: bytes, max_files: files });
276 noQuota: (key: 'bytes' | 'files') => {
277 assert.quotaRow(key, '', 0, '');
279 quotaIsNotInherited: (key: 'bytes' | 'files', shownValue: any, nextMaximum: number) => {
280 const dir = component.selectedDir;
281 const path = dir.path;
282 assert.quotaRow(key, shownValue, nextMaximum, path);
284 quotaIsInherited: (key: 'bytes' | 'files', shownValue: any, path: string) => {
285 const isBytes = key === 'bytes';
286 const nextMaximum = get.nodeIds()[path].quotas[isBytes ? 'max_bytes' : 'max_files'];
287 assert.quotaRow(key, shownValue, nextMaximum, path);
290 key: 'bytes' | 'files',
291 shownValue: number | string,
292 nextTreeMaximum: number,
295 const isBytes = key === 'bytes';
296 expect(component.settings[isBytes ? 1 : 0]).toEqual({
298 name: `Max ${isBytes ? 'size' : key}`,
302 quotaKey: `max_${key}`,
303 dirValue: expect.any(Number),
305 value: nextTreeMaximum,
306 path: expect.any(String)
310 quotaUnsetModalTexts: (titleText: string, message: string, notificationMsg: string) => {
311 expect(modalShowSpy).toHaveBeenCalledWith(
312 ConfirmationModalComponent,
313 expect.objectContaining({
315 description: message,
319 expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
321 quotaUpdateModalTexts: (titleText: string, message: string, notificationMsg: string) => {
322 expect(modalShowSpy).toHaveBeenCalledWith(
324 expect.objectContaining({
327 submitButtonText: 'Save'
330 expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
332 quotaUpdateModalField: (
338 errors?: { [key: string]: string }
340 expect(modalShowSpy).toHaveBeenCalledWith(
342 expect.objectContaining({
350 validators: expect.anything(),
356 if (type === 'binary') {
357 expect(minBinaryValidator).toHaveBeenCalledWith(0);
358 expect(maxBinaryValidator).toHaveBeenCalledWith(max);
360 expect(minValidator).toHaveBeenCalledWith(0);
361 expect(maxValidator).toHaveBeenCalledWith(max);
369 HttpClientTestingModule,
372 TreeModule.forRoot(),
373 NgBootstrapFormValidationModule.forRoot(),
374 ToastrModule.forRoot(),
377 declarations: [CephfsDirectoriesComponent],
378 providers: [i18nProviders, NgbActiveModal]
380 [CriticalConfirmationModalComponent, FormModalComponent, ConfirmationModalComponent]
384 noAsyncUpdate = false;
394 cephfsService = TestBed.inject(CephfsService);
395 lsDirSpy = spyOn(cephfsService, 'lsDir').and.callFake(mockLib.lsDir);
396 spyOn(cephfsService, 'mkSnapshot').and.callFake(mockLib.mkSnapshot);
397 spyOn(cephfsService, 'rmSnapshot').and.callFake(mockLib.rmSnapshot);
398 spyOn(cephfsService, 'updateQuota').and.callFake(mockLib.updateQuota);
400 modalShowSpy = spyOn(TestBed.inject(ModalService), 'show').and.callFake(mockLib.modalShow);
401 notificationShowSpy = spyOn(TestBed.inject(NotificationService), 'show').and.stub();
403 fixture = TestBed.createComponent(CephfsDirectoriesComponent);
404 component = fixture.componentInstance;
405 fixture.detectChanges();
407 spyOn(TREE_ACTIONS, 'TOGGLE_ACTIVE').and.callFake(mockLib.treeActions.toggleActive);
409 component.treeComponent = {
410 sizeChanged: () => null,
411 treeModel: { getNodeById: mockLib.getNodeById, update: () => null }
415 it('should create', () => {
416 expect(component).toBeTruthy();
419 describe('mock self test', () => {
420 it('tests snapshots mock', () => {
421 expect(mockLib.snapshots('/a', 1).map((s) => ({ name: s.name, path: s.path }))).toEqual([
423 name: 'someSnapshot1',
424 path: '/a/.snap/someSnapshot1'
427 expect(mockLib.snapshots('/a/b', 3).map((s) => ({ name: s.name, path: s.path }))).toEqual([
429 name: 'someSnapshot1',
430 path: '/a/b/.snap/someSnapshot1'
433 name: 'someSnapshot2',
434 path: '/a/b/.snap/someSnapshot2'
437 name: 'someSnapshot3',
438 path: '/a/b/.snap/someSnapshot3'
443 it('tests dir mock', () => {
444 const path = '/a/b/c';
445 mockData.createdSnaps = [
446 { path, name: 's1' },
449 mockData.deletedSnaps = [
450 { path, name: 'someSnapshot2' },
453 const dir = mockLib.dir('/a/b', 'c', 2);
454 expect(dir.path).toBe('/a/b/c');
455 expect(dir.parent).toBe('/a/b');
456 expect(dir.quotas).toEqual({ max_bytes: 2048, max_files: 20 });
457 expect(dir.snapshots.map((s) => s.name)).toEqual(['someSnapshot1', 's1']);
460 it('tests lsdir mock', () => {
461 let dirs: CephfsDir[] = [];
462 mockLib.lsDir(2, '/a').subscribe((x) => (dirs = x));
463 expect(dirs.map((d) => d.path)).toEqual([
476 describe('test quota update mock', () => {
480 const updateQuota = (quotas: CephfsQuotas) => mockLib.updateQuota(ID, PATH, quotas);
482 const expectMockUpdate = (max_bytes?: number, max_files?: number) =>
483 expect(mockData.updatedQuotas[PATH]).toEqual({
488 const expectLsUpdate = (max_bytes?: number, max_files?: number) => {
490 mockLib.lsDir(ID, '/').subscribe((dirs) => (dir = dirs.find((d) => d.path === PATH)));
491 expect(dir.quotas).toEqual({
497 it('tests to set quotas', () => {
498 expectLsUpdate(1024, 10);
500 updateQuota({ max_bytes: 512 });
501 expectMockUpdate(512);
502 expectLsUpdate(512, 10);
504 updateQuota({ max_files: 100 });
505 expectMockUpdate(512, 100);
506 expectLsUpdate(512, 100);
509 it('tests to unset quotas', () => {
510 updateQuota({ max_files: 0 });
511 expectMockUpdate(undefined, 0);
512 expectLsUpdate(1024, 0);
514 updateQuota({ max_bytes: 0 });
515 expectMockUpdate(0, 0);
516 expectLsUpdate(0, 0);
521 it('calls lsDir only if an id exits', () => {
522 assert.lsDirCalledTimes(0);
525 assert.lsDirCalledTimes(1);
526 expect(lsDirSpy).toHaveBeenCalledWith(1, '/');
529 assert.lsDirCalledTimes(2);
530 expect(lsDirSpy).toHaveBeenCalledWith(2, '/');
533 describe('listing sub directories', () => {
537 * Tree looks like this:
545 it('expands first level', () => {
546 // Tree will only show '*' if nor 'loadChildren' or 'children' are defined
548 mockData.nodes.map((node: any) => ({
549 [node.id]: node.hasChildren || node.isExpanded || Boolean(node.children)
551 ).toEqual([{ '/': true }, { '/a': true }, { '/b': false }, { '/c': true }]);
554 it('resets all dynamic content on id change', () => {
555 mockLib.selectNode('/a');
557 * Tree looks like this:
566 assert.requestedPaths(['/', '/a']);
567 assert.nodeLength(7);
568 assert.dirLength(16);
569 expect(component.selectedDir).toBeDefined();
571 mockLib.changeId(undefined);
573 assert.requestedPaths([]);
574 expect(component.selectedDir).not.toBeDefined();
577 it('should select a node and show the directory contents', () => {
578 mockLib.selectNode('/a');
579 const dir = get.dirs().find((d) => d.path === '/a');
580 expect(component.selectedDir).toEqual(dir);
581 assert.quotaIsNotInherited('files', 10, 0);
582 assert.quotaIsNotInherited('bytes', '1 KiB', 0);
585 it('should extend the list by subdirectories when expanding', () => {
586 mockLib.selectNode('/a');
587 mockLib.selectNode('/a/c');
589 * Tree looks like this:
601 assert.lsDirCalledTimes(3);
602 assert.requestedPaths(['/', '/a', '/a/c']);
603 assert.dirLength(22);
604 assert.nodeLength(10);
607 it('should update the tree after each selection', () => {
608 const spy = spyOn(component.treeComponent, 'sizeChanged').and.callThrough();
609 expect(spy).toHaveBeenCalledTimes(0);
610 mockLib.selectNode('/a');
611 expect(spy).toHaveBeenCalledTimes(1);
612 mockLib.selectNode('/a/c');
613 expect(spy).toHaveBeenCalledTimes(2);
616 it('should select parent by path', () => {
617 mockLib.selectNode('/a');
618 mockLib.selectNode('/a/c');
619 mockLib.selectNode('/a/c/a');
620 component.selectOrigin('/a');
621 expect(component.selectedDir.path).toBe('/a');
624 it('should refresh directories with no sub directories as they could have some now', () => {
625 mockLib.selectNode('/b');
627 * Tree looks like this:
633 assert.lsDirCalledTimes(2);
634 assert.requestedPaths(['/', '/b']);
635 assert.nodeLength(4);
638 describe('used quotas', () => {
639 it('should use no quota if none is set', () => {
640 mockLib.setFourQuotaDirs([
646 assert.noQuota('files');
647 assert.noQuota('bytes');
648 assert.dirQuotas(0, 0);
651 it('should use quota from upper parents', () => {
652 mockLib.setFourQuotaDirs([
658 assert.quotaIsInherited('files', 100, '/1');
659 assert.quotaIsInherited('bytes', '8 KiB', '/1/2');
660 assert.dirQuotas(0, 0);
663 it('should use quota from the parent with the lowest value (deep inheritance)', () => {
664 mockLib.setFourQuotaDirs([
670 assert.quotaIsInherited('files', 100, '/1/2');
671 assert.quotaIsInherited('bytes', '1 KiB', '/1');
672 assert.dirQuotas(2048, 300);
675 it('should use current value', () => {
676 mockLib.setFourQuotaDirs([
682 assert.quotaIsNotInherited('files', 100, 200);
683 assert.quotaIsNotInherited('bytes', '1 KiB', 2048);
684 assert.dirQuotas(1024, 100);
689 describe('snapshots', () => {
692 mockLib.selectNode('/a');
695 it('should create a snapshot', () => {
696 mockLib.createSnapshotThroughModal('newSnap');
697 expect(cephfsService.mkSnapshot).toHaveBeenCalledWith(1, '/a', 'newSnap');
698 assert.snapshotsByName(['someSnapshot1', 'newSnap']);
701 it('should delete a snapshot', () => {
702 mockLib.createSnapshotThroughModal('deleteMe');
703 mockLib.deleteSnapshotsThroughModal([component.selectedDir.snapshots[1]]);
704 assert.snapshotsByName(['someSnapshot1']);
707 it('should delete all snapshots', () => {
708 mockLib.createSnapshotThroughModal('deleteAll');
709 mockLib.deleteSnapshotsThroughModal(component.selectedDir.snapshots);
710 assert.snapshotsByName([]);
714 it('should test all snapshot table actions combinations', () => {
715 const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
716 const tableActions = permissionHelper.setPermissionsAndGetActions(
717 component.snapshot.tableActions
720 expect(tableActions).toEqual({
721 'create,update,delete': {
722 actions: ['Create', 'Delete'],
723 primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
727 primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
730 actions: ['Create', 'Delete'],
731 primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
735 primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
739 primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
743 primary: { multiple: '', executing: '', single: '', no: '' }
747 primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
751 primary: { multiple: '', executing: '', single: '', no: '' }
756 describe('quotas', () => {
759 minValidator = spyOn(Validators, 'min').and.callThrough();
760 maxValidator = spyOn(Validators, 'max').and.callThrough();
761 minBinaryValidator = spyOn(CdValidators, 'binaryMin').and.callThrough();
762 maxBinaryValidator = spyOn(CdValidators, 'binaryMax').and.callThrough();
765 mockLib.selectNode('/a');
766 mockLib.selectNode('/a/c');
767 mockLib.selectNode('/a/c/b');
768 // Quotas after selection
769 assert.quotaIsInherited('files', 10, '/a');
770 assert.quotaIsInherited('bytes', '1 KiB', '/a');
771 assert.dirQuotas(2048, 20);
774 describe('update modal', () => {
775 describe('max_files', () => {
777 mockLib.updateQuotaThroughModal('max_files', 5);
780 it('should update max_files correctly', () => {
781 expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 5 });
782 assert.quotaIsNotInherited('files', 5, 10);
785 it('uses the correct form field', () => {
786 assert.quotaUpdateModalField('number', 'Max files', 'max_files', 20, 10, {
787 min: 'Value has to be at least 0 or more',
788 max: 'Value has to be at most 10 or less'
792 it('shows the right texts', () => {
793 assert.quotaUpdateModalTexts(
794 `Update CephFS files quota for '/a/c/b'`,
795 `The inherited files quota 10 from '/a' is the maximum value to be used.`,
796 `Updated CephFS files quota for '/a/c/b'`
801 describe('max_bytes', () => {
803 mockLib.updateQuotaThroughModal('max_bytes', 512);
806 it('should update max_files correctly', () => {
807 expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 512 });
808 assert.quotaIsNotInherited('bytes', '512 B', 1024);
811 it('uses the correct form field', () => {
812 mockLib.updateQuotaThroughModal('max_bytes', 512);
813 assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 2048, 1024);
816 it('shows the right texts', () => {
817 assert.quotaUpdateModalTexts(
818 `Update CephFS size quota for '/a/c/b'`,
819 `The inherited size quota 1 KiB from '/a' is the maximum value to be used.`,
820 `Updated CephFS size quota for '/a/c/b'`
825 describe('action behaviour', () => {
826 it('opens with next maximum as maximum if directory holds the current maximum', () => {
827 mockLib.updateQuotaThroughModal('max_bytes', 512);
828 mockLib.updateQuotaThroughModal('max_bytes', 888);
829 assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 512, 1024);
832 it(`uses 'Set' action instead of 'Update' if the quota is not set (0)`, () => {
833 mockLib.updateQuotaThroughModal('max_bytes', 0);
834 mockLib.updateQuotaThroughModal('max_bytes', 200);
835 assert.quotaUpdateModalTexts(
836 `Set CephFS size quota for '/a/c/b'`,
837 `The inherited size quota 1 KiB from '/a' is the maximum value to be used.`,
838 `Set CephFS size quota for '/a/c/b'`
844 describe('unset modal', () => {
845 describe('max_files', () => {
847 mockLib.updateQuotaThroughModal('max_files', 5); // Sets usable quota
848 mockLib.unsetQuotaThroughModal('max_files');
851 it('should unset max_files correctly', () => {
852 expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 0 });
853 assert.dirQuotas(2048, 0);
856 it('shows the right texts', () => {
857 assert.quotaUnsetModalTexts(
858 `Unset CephFS files quota for '/a/c/b'`,
859 `Unset files quota 5 from '/a/c/b' in order to inherit files quota 10 from '/a'.`,
860 `Unset CephFS files quota for '/a/c/b'`
865 describe('max_bytes', () => {
867 mockLib.updateQuotaThroughModal('max_bytes', 512); // Sets usable quota
868 mockLib.unsetQuotaThroughModal('max_bytes');
871 it('should unset max_files correctly', () => {
872 expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 0 });
873 assert.dirQuotas(0, 20);
876 it('shows the right texts', () => {
877 assert.quotaUnsetModalTexts(
878 `Unset CephFS size quota for '/a/c/b'`,
879 `Unset size quota 512 B from '/a/c/b' in order to inherit size quota 1 KiB from '/a'.`,
880 `Unset CephFS size quota for '/a/c/b'`
885 describe('action behaviour', () => {
886 it('uses different Text if no quota is inherited', () => {
887 mockLib.selectNode('/a');
888 mockLib.unsetQuotaThroughModal('max_bytes');
889 assert.quotaUnsetModalTexts(
890 `Unset CephFS size quota for '/a'`,
891 `Unset size quota 1 KiB from '/a' in order to have no quota on the directory.`,
892 `Unset CephFS size quota for '/a'`
896 it('uses different Text if quota is already inherited', () => {
897 mockLib.unsetQuotaThroughModal('max_bytes');
898 assert.quotaUnsetModalTexts(
899 `Unset CephFS size quota for '/a/c/b'`,
900 `Unset size quota 2 KiB from '/a/c/b' which isn't used because of the inheritance ` +
901 `of size quota 1 KiB from '/a'.`,
902 `Unset CephFS size quota for '/a/c/b'`
909 describe('table actions', () => {
910 let actions: CdTableAction[];
912 const empty = (): CdTableSelection => new CdTableSelection();
914 const select = (value: number): CdTableSelection => {
915 const selection = new CdTableSelection();
916 selection.selected = [{ dirValue: value }];
921 actions = component.quota.tableActions;
924 it(`shows 'Set' for empty and not set quotas`, () => {
925 const isSetVisible = actions[0].visible;
926 expect(isSetVisible(empty())).toBe(true);
927 expect(isSetVisible(select(0))).toBe(true);
928 expect(isSetVisible(select(1))).toBe(false);
931 it(`shows 'Update' for set quotas only`, () => {
932 const isUpdateVisible = actions[1].visible;
933 expect(isUpdateVisible(empty())).toBeFalsy();
934 expect(isUpdateVisible(select(0))).toBe(false);
935 expect(isUpdateVisible(select(1))).toBe(true);
938 it(`only enables 'Unset' for set quotas only`, () => {
939 const isUnsetDisabled = actions[2].disable;
940 expect(isUnsetDisabled(empty())).toBe(true);
941 expect(isUnsetDisabled(select(0))).toBe(true);
942 expect(isUnsetDisabled(select(1))).toBe(false);
945 it('should test all quota table actions permission combinations', () => {
946 const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
947 const tableActions = permissionHelper.setPermissionsAndGetActions(
948 component.quota.tableActions
951 expect(tableActions).toEqual({
952 'create,update,delete': {
953 actions: ['Set', 'Update', 'Unset'],
954 primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
957 actions: ['Set', 'Update', 'Unset'],
958 primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
962 primary: { multiple: '', executing: '', single: '', no: '' }
966 primary: { multiple: '', executing: '', single: '', no: '' }
969 actions: ['Set', 'Update', 'Unset'],
970 primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
973 actions: ['Set', 'Update', 'Unset'],
974 primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
978 primary: { multiple: '', executing: '', single: '', no: '' }
982 primary: { multiple: '', executing: '', single: '', no: '' }
988 describe('reload all', () => {
989 const calledPaths = ['/', '/a', '/a/c', '/a/c/a', '/a/c/a/b'];
991 const dirsByPath = (): string[] => get.dirs().map((d) => d.path);
995 mockLib.selectNode('/a');
996 mockLib.selectNode('/a/c');
997 mockLib.selectNode('/a/c/a');
998 mockLib.selectNode('/a/c/a/b');
1001 it('should reload all requested paths', () => {
1002 assert.lsDirHasBeenCalledWith(1, calledPaths);
1003 lsDirSpy.calls.reset();
1004 assert.lsDirHasBeenCalledWith(1, []);
1005 component.refreshAllDirectories();
1006 assert.lsDirHasBeenCalledWith(1, calledPaths);
1009 it('should reload all requested paths if not selected anything', () => {
1010 lsDirSpy.calls.reset();
1011 mockLib.changeId(2);
1012 assert.lsDirHasBeenCalledWith(2, ['/']);
1013 lsDirSpy.calls.reset();
1014 component.refreshAllDirectories();
1015 assert.lsDirHasBeenCalledWith(2, ['/']);
1018 it('should add new directories', () => {
1019 // Create two new directories in preparation
1020 const dirsBeforeRefresh = dirsByPath();
1021 expect(dirsBeforeRefresh.includes('/a/c/has_dir_now')).toBe(false);
1022 mockLib.mkDir('/a/c', 'has_dir_now', 0, 0);
1023 mockLib.mkDir('/a/c/a/b', 'has_dir_now_too', 0, 0);
1024 // Now the new directories will be fetched
1025 component.refreshAllDirectories();
1026 const dirsAfterRefresh = dirsByPath();
1027 expect(dirsAfterRefresh.length - dirsBeforeRefresh.length).toBe(2);
1028 expect(dirsAfterRefresh.includes('/a/c/has_dir_now')).toBe(true);
1029 expect(dirsAfterRefresh.includes('/a/c/a/b/has_dir_now_too')).toBe(true);
1032 it('should remove deleted directories', () => {
1033 // Create one new directory and refresh in order to have it added to the directories list
1034 mockLib.mkDir('/a/c', 'will_be_removed_shortly', 0, 0);
1035 component.refreshAllDirectories();
1036 const dirsBeforeRefresh = dirsByPath();
1037 expect(dirsBeforeRefresh.includes('/a/c/will_be_removed_shortly')).toBe(true);
1038 mockData.createdDirs = []; // Mocks the deletion of the directory
1039 // Now the deleted directory will be missing on refresh
1040 component.refreshAllDirectories();
1041 const dirsAfterRefresh = dirsByPath();
1042 expect(dirsAfterRefresh.length - dirsBeforeRefresh.length).toBe(-1);
1043 expect(dirsAfterRefresh.includes('/a/c/will_be_removed_shortly')).toBe(false);
1046 describe('loading indicator', () => {
1048 noAsyncUpdate = true;
1051 it('should have set loading indicator to false after refreshing all dirs', fakeAsync(() => {
1052 component.refreshAllDirectories();
1053 expect(component.loadingIndicator).toBe(true);
1054 tick(3000); // To resolve all promises
1055 expect(component.loadingIndicator).toBe(false);
1058 it('should only update the tree once and not on every call', fakeAsync(() => {
1059 const spy = spyOn(component.treeComponent, 'sizeChanged').and.callThrough();
1060 component.refreshAllDirectories();
1061 expect(spy).toHaveBeenCalledTimes(0);
1062 tick(3000); // To resolve all promises
1063 // Called during the interval and at the end of timeout
1064 expect(spy).toHaveBeenCalledTimes(2);
1067 it('should have set all loaded dirs as attribute names of "indicators"', () => {
1068 noAsyncUpdate = false;
1069 component.refreshAllDirectories();
1070 expect(Object.keys(component.loading).sort()).toEqual(calledPaths);
1073 it('should set an indicator to true during load', () => {
1074 lsDirSpy.and.callFake(() => new Observable((): null => null));
1075 component.refreshAllDirectories();
1076 expect(Object.values(component.loading).every((b) => b)).toBe(true);
1077 expect(component.loadingIndicator).toBe(true);