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';
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';
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';
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;
50 // Get's private attributes or functions
52 nodeIds: (): { [path: string]: CephfsDir } => component['nodeIds'],
53 dirs: (): CephfsDir[] => component['dirs'],
54 requestedPaths: (): string[] => component['requestedPaths']
57 // Object contains mock data that will be reset before each test.
61 createdSnaps: CephfsSnapshot[] | any[];
62 deletedSnaps: CephfsSnapshot[] | any[];
63 updatedQuotas: { [path: string]: CephfsQuotas };
64 createdDirs: CephfsDir[];
67 // Object contains mock functions
69 quotas: (max_bytes: number, max_files: number): CephfsQuotas => ({ max_bytes, max_files }),
70 snapshots: (dirPath: string, howMany: number): CephfsSnapshot[] => {
71 const name = 'someSnapshot';
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 });
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);
89 const deletedSnapshots = mockData.deletedSnaps
90 .filter((s) => s.path === dirPath)
92 if (deletedSnapshots.length > 0) {
93 snapshots = snapshots.filter((s) => !deletedSnapshots.includes(s.name));
99 quotas: Object.assign(
100 mockLib.quotas(1024 * modifier, 10 * modifier),
101 mockData.updatedQuotas[dirPath] || {}
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
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)
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));
128 if (path === '' || path === '/') {
129 // Adds root directory on ls of '/' to the directories list.
130 const root = mockLib.dir(path, '/', 1);
132 root.parent = undefined;
133 root.quotas = undefined;
134 data = [root].concat(data);
138 mkSnapshot: (_id: any, path: string, name: string): Observable<string> => {
139 mockData.createdSnaps.push({
142 created: new Date().toString()
146 rmSnapshot: (_id: any, path: string, name: string): Observable<string> => {
147 mockData.deletedSnaps.push({
150 created: new Date().toString()
154 updateQuota: (_id: any, path: string, updated: CephfsQuotas): Observable<string> => {
155 mockData.updatedQuotas[path] = Object.assign(mockData.updatedQuotas[path] || {}, updated);
156 return of('Response');
158 modalShow: (comp: Type<any>, init: any): any => {
159 modal = modalServiceShow(comp, init);
162 getNodeById: (path: string) => {
163 return mockLib.useNode(path);
165 updateNodes: (path: string) => {
166 const p: Promise<any[]> = component.treeOptions.getChildren({ id: path });
167 return noAsyncUpdate ? () => p : mockLib.asyncNodeUpdate(p);
169 asyncNodeUpdate: fakeAsync((p: Promise<any[]>) => {
171 mockData.nodes = mockData.nodes.concat(nodes);
175 changeId: (id: number) => {
176 // For some reason this spy has to be renewed after usage
177 spyOn(global, 'setTimeout').and.callFake((fn) => fn());
179 component.ngOnChanges();
180 mockData.nodes = component.nodes.concat(mockData.nodes);
182 selectNode: (path: string) => {
183 component.treeOptions.actionMapping.mouse.click(undefined, mockLib.useNode(path), undefined);
185 // Creates TreeNode with parents until root
186 useNode: (path: string): { id: string; parent: any; data: any; loadNodeChildren: Function } => {
187 const parentPath = path.split('/');
189 const parentIsRoot = parentPath.length === 1;
190 const parent = parentIsRoot ? { id: '/' } : mockLib.useNode(parentPath.join('/'));
195 loadNodeChildren: () => mockLib.updateNodes(path)
199 toggleActive: (_a: any, node: any, _b: any) => {
200 return mockLib.updateNodes(node.id);
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 });
212 createSnapshotThroughModal: (name: string) => {
213 component.createSnapshot();
214 modal.componentInstance.onSubmitForm({ name });
216 deleteSnapshotsThroughModal: (snapshots: CephfsSnapshot[]) => {
217 component.snapshot.selection.selected = snapshots;
218 component.deleteSnapshotModal();
219 modal.componentInstance.callSubmitAction();
221 updateQuotaThroughModal: (attribute: string, value: number) => {
222 component.quota.selection.selected = component.settings.filter(
223 (q) => q.quotaKey === attribute
225 component.updateQuotaModal();
226 modal.componentInstance.onSubmitForm({ [attribute]: value });
228 unsetQuotaThroughModal: (attribute: string) => {
229 component.quota.selection.selected = component.settings.filter(
230 (q) => q.quotaKey === attribute
232 component.unsetQuotaModal();
233 modal.componentInstance.onSubmit();
235 setFourQuotaDirs: (quotas: number[][]) => {
236 expect(quotas.length).toBe(4); // Make sure this function is used correctly
238 quotas.forEach((quota, index) => {
240 mockLib.mkDir(path === '' ? '/' : path, index.toString(), quota[0], quota[1]);
252 parent: { value: '/', id: '/' }
256 mockLib.selectNode('/1/2/3/4');
260 // Expects that are used frequently
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);
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 });
275 noQuota: (key: 'bytes' | 'files') => {
276 assert.quotaRow(key, '', 0, '');
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);
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);
289 key: 'bytes' | 'files',
290 shownValue: number | string,
291 nextTreeMaximum: number,
294 const isBytes = key === 'bytes';
295 expect(component.settings[isBytes ? 1 : 0]).toEqual({
297 name: `Max ${isBytes ? 'size' : key}`,
301 quotaKey: `max_${key}`,
302 dirValue: expect.any(Number),
304 value: nextTreeMaximum,
305 path: expect.any(String)
309 quotaUnsetModalTexts: (titleText: string, message: string, notificationMsg: string) => {
310 expect(modalShowSpy).toHaveBeenCalledWith(
311 ConfirmationModalComponent,
312 expect.objectContaining({
314 description: message,
318 expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
320 quotaUpdateModalTexts: (titleText: string, message: string, notificationMsg: string) => {
321 expect(modalShowSpy).toHaveBeenCalledWith(
323 expect.objectContaining({
326 submitButtonText: 'Save'
329 expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
331 quotaUpdateModalField: (
337 errors?: { [key: string]: string }
339 expect(modalShowSpy).toHaveBeenCalledWith(
341 expect.objectContaining({
349 validators: expect.anything(),
355 if (type === 'binary') {
356 expect(minBinaryValidator).toHaveBeenCalledWith(0);
357 expect(maxBinaryValidator).toHaveBeenCalledWith(max);
359 expect(minValidator).toHaveBeenCalledWith(0);
360 expect(maxValidator).toHaveBeenCalledWith(max);
368 HttpClientTestingModule,
371 TreeModule.forRoot(),
372 NgBootstrapFormValidationModule.forRoot(),
373 ToastrModule.forRoot(),
376 declarations: [CephfsDirectoriesComponent],
377 providers: [NgbActiveModal]
379 [CriticalConfirmationModalComponent, FormModalComponent, ConfirmationModalComponent]
383 noAsyncUpdate = false;
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);
399 modalShowSpy = spyOn(TestBed.inject(ModalService), 'show').and.callFake(mockLib.modalShow);
400 notificationShowSpy = spyOn(TestBed.inject(NotificationService), 'show').and.stub();
402 fixture = TestBed.createComponent(CephfsDirectoriesComponent);
403 component = fixture.componentInstance;
404 fixture.detectChanges();
406 spyOn(TREE_ACTIONS, 'TOGGLE_ACTIVE').and.callFake(mockLib.treeActions.toggleActive);
408 component.treeComponent = {
409 sizeChanged: () => null,
410 treeModel: { getNodeById: mockLib.getNodeById, update: () => null }
414 it('should create', () => {
415 expect(component).toBeTruthy();
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([
422 name: 'someSnapshot1',
423 path: '/a/.snap/someSnapshot1'
426 expect(mockLib.snapshots('/a/b', 3).map((s) => ({ name: s.name, path: s.path }))).toEqual([
428 name: 'someSnapshot1',
429 path: '/a/b/.snap/someSnapshot1'
432 name: 'someSnapshot2',
433 path: '/a/b/.snap/someSnapshot2'
436 name: 'someSnapshot3',
437 path: '/a/b/.snap/someSnapshot3'
442 it('tests dir mock', () => {
443 const path = '/a/b/c';
444 mockData.createdSnaps = [
445 { path, name: 's1' },
448 mockData.deletedSnaps = [
449 { path, name: 'someSnapshot2' },
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']);
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([
475 describe('test quota update mock', () => {
479 const updateQuota = (quotas: CephfsQuotas) => mockLib.updateQuota(ID, PATH, quotas);
481 const expectMockUpdate = (max_bytes?: number, max_files?: number) =>
482 expect(mockData.updatedQuotas[PATH]).toEqual({
487 const expectLsUpdate = (max_bytes?: number, max_files?: number) => {
489 mockLib.lsDir(ID, '/').subscribe((dirs) => (dir = dirs.find((d) => d.path === PATH)));
490 expect(dir.quotas).toEqual({
496 it('tests to set quotas', () => {
497 expectLsUpdate(1024, 10);
499 updateQuota({ max_bytes: 512 });
500 expectMockUpdate(512);
501 expectLsUpdate(512, 10);
503 updateQuota({ max_files: 100 });
504 expectMockUpdate(512, 100);
505 expectLsUpdate(512, 100);
508 it('tests to unset quotas', () => {
509 updateQuota({ max_files: 0 });
510 expectMockUpdate(undefined, 0);
511 expectLsUpdate(1024, 0);
513 updateQuota({ max_bytes: 0 });
514 expectMockUpdate(0, 0);
515 expectLsUpdate(0, 0);
520 it('calls lsDir only if an id exits', () => {
521 assert.lsDirCalledTimes(0);
524 assert.lsDirCalledTimes(1);
525 expect(lsDirSpy).toHaveBeenCalledWith(1, '/');
528 assert.lsDirCalledTimes(2);
529 expect(lsDirSpy).toHaveBeenCalledWith(2, '/');
532 describe('listing sub directories', () => {
536 * Tree looks like this:
544 it('expands first level', () => {
545 // Tree will only show '*' if nor 'loadChildren' or 'children' are defined
547 mockData.nodes.map((node: any) => ({
548 [node.id]: node.hasChildren || node.isExpanded || Boolean(node.children)
550 ).toEqual([{ '/': true }, { '/a': true }, { '/b': false }, { '/c': true }]);
553 it('resets all dynamic content on id change', () => {
554 mockLib.selectNode('/a');
556 * Tree looks like this:
565 assert.requestedPaths(['/', '/a']);
566 assert.nodeLength(7);
567 assert.dirLength(16);
568 expect(component.selectedDir).toBeDefined();
570 mockLib.changeId(undefined);
572 assert.requestedPaths([]);
573 expect(component.selectedDir).not.toBeDefined();
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);
584 it('should extend the list by subdirectories when expanding', () => {
585 mockLib.selectNode('/a');
586 mockLib.selectNode('/a/c');
588 * Tree looks like this:
600 assert.lsDirCalledTimes(3);
601 assert.requestedPaths(['/', '/a', '/a/c']);
602 assert.dirLength(22);
603 assert.nodeLength(10);
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);
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');
623 it('should refresh directories with no sub directories as they could have some now', () => {
624 mockLib.selectNode('/b');
626 * Tree looks like this:
632 assert.lsDirCalledTimes(2);
633 assert.requestedPaths(['/', '/b']);
634 assert.nodeLength(4);
637 describe('used quotas', () => {
638 it('should use no quota if none is set', () => {
639 mockLib.setFourQuotaDirs([
645 assert.noQuota('files');
646 assert.noQuota('bytes');
647 assert.dirQuotas(0, 0);
650 it('should use quota from upper parents', () => {
651 mockLib.setFourQuotaDirs([
657 assert.quotaIsInherited('files', 100, '/1');
658 assert.quotaIsInherited('bytes', '8 KiB', '/1/2');
659 assert.dirQuotas(0, 0);
662 it('should use quota from the parent with the lowest value (deep inheritance)', () => {
663 mockLib.setFourQuotaDirs([
669 assert.quotaIsInherited('files', 100, '/1/2');
670 assert.quotaIsInherited('bytes', '1 KiB', '/1');
671 assert.dirQuotas(2048, 300);
674 it('should use current value', () => {
675 mockLib.setFourQuotaDirs([
681 assert.quotaIsNotInherited('files', 100, 200);
682 assert.quotaIsNotInherited('bytes', '1 KiB', 2048);
683 assert.dirQuotas(1024, 100);
688 describe('snapshots', () => {
691 mockLib.selectNode('/a');
694 it('should create a snapshot', () => {
695 mockLib.createSnapshotThroughModal('newSnap');
696 expect(cephfsService.mkSnapshot).toHaveBeenCalledWith(1, '/a', 'newSnap');
697 assert.snapshotsByName(['someSnapshot1', 'newSnap']);
700 it('should delete a snapshot', () => {
701 mockLib.createSnapshotThroughModal('deleteMe');
702 mockLib.deleteSnapshotsThroughModal([component.selectedDir.snapshots[1]]);
703 assert.snapshotsByName(['someSnapshot1']);
706 it('should delete all snapshots', () => {
707 mockLib.createSnapshotThroughModal('deleteAll');
708 mockLib.deleteSnapshotsThroughModal(component.selectedDir.snapshots);
709 assert.snapshotsByName([]);
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
719 expect(tableActions).toEqual({
720 'create,update,delete': {
721 actions: ['Create', 'Delete'],
722 primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
726 primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
729 actions: ['Create', 'Delete'],
730 primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
734 primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
738 primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
742 primary: { multiple: '', executing: '', single: '', no: '' }
746 primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
750 primary: { multiple: '', executing: '', single: '', no: '' }
755 describe('quotas', () => {
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();
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);
773 describe('update modal', () => {
774 describe('max_files', () => {
776 mockLib.updateQuotaThroughModal('max_files', 5);
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);
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'
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'`
800 describe('max_bytes', () => {
802 mockLib.updateQuotaThroughModal('max_bytes', 512);
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);
810 it('uses the correct form field', () => {
811 mockLib.updateQuotaThroughModal('max_bytes', 512);
812 assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 2048, 1024);
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'`
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);
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'`
843 describe('unset modal', () => {
844 describe('max_files', () => {
846 mockLib.updateQuotaThroughModal('max_files', 5); // Sets usable quota
847 mockLib.unsetQuotaThroughModal('max_files');
850 it('should unset max_files correctly', () => {
851 expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 0 });
852 assert.dirQuotas(2048, 0);
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'`
864 describe('max_bytes', () => {
866 mockLib.updateQuotaThroughModal('max_bytes', 512); // Sets usable quota
867 mockLib.unsetQuotaThroughModal('max_bytes');
870 it('should unset max_files correctly', () => {
871 expect(cephfsService.updateQuota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 0 });
872 assert.dirQuotas(0, 20);
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'`
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'`
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'`
908 describe('table actions', () => {
909 let actions: CdTableAction[];
911 const empty = (): CdTableSelection => new CdTableSelection();
913 const select = (value: number): CdTableSelection => {
914 const selection = new CdTableSelection();
915 selection.selected = [{ dirValue: value }];
920 actions = component.quota.tableActions;
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);
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);
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);
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
950 expect(tableActions).toEqual({
951 'create,update,delete': {
952 actions: ['Set', 'Update', 'Unset'],
953 primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
956 actions: ['Set', 'Update', 'Unset'],
957 primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
961 primary: { multiple: '', executing: '', single: '', no: '' }
965 primary: { multiple: '', executing: '', single: '', no: '' }
968 actions: ['Set', 'Update', 'Unset'],
969 primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
972 actions: ['Set', 'Update', 'Unset'],
973 primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
977 primary: { multiple: '', executing: '', single: '', no: '' }
981 primary: { multiple: '', executing: '', single: '', no: '' }
987 describe('reload all', () => {
988 const calledPaths = ['/', '/a', '/a/c', '/a/c/a', '/a/c/a/b'];
990 const dirsByPath = (): string[] => get.dirs().map((d) => d.path);
994 mockLib.selectNode('/a');
995 mockLib.selectNode('/a/c');
996 mockLib.selectNode('/a/c/a');
997 mockLib.selectNode('/a/c/a/b');
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);
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, ['/']);
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);
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);
1045 describe('loading indicator', () => {
1047 noAsyncUpdate = true;
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);
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);
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);
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);