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 { TreeComponent, TreeModule, TREE_ACTIONS } from '@circlon/angular-tree-component';
8 import { NgbActiveModal, NgbModalModule, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
9 import { ToastrModule } from 'ngx-toastr';
10 import { Observable, of } from 'rxjs';
12 import { CephfsService } from '~/app/shared/api/cephfs.service';
13 import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
14 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
15 import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
16 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
17 import { CdValidators } from '~/app/shared/forms/cd-validators';
18 import { CdTableAction } from '~/app/shared/models/cd-table-action';
19 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
24 } from '~/app/shared/models/cephfs-directory-models';
25 import { ModalService } from '~/app/shared/services/modal.service';
26 import { NotificationService } from '~/app/shared/services/notification.service';
27 import { SharedModule } from '~/app/shared/shared.module';
28 import { configureTestBed, modalServiceShow, PermissionHelper } from '~/testing/unit-test-helper';
29 import { CephfsDirectoriesComponent } from './cephfs-directories.component';
31 describe('CephfsDirectoriesComponent', () => {
32 let component: CephfsDirectoriesComponent;
33 let fixture: ComponentFixture<CephfsDirectoriesComponent>;
34 let cephfsService: CephfsService;
35 let noAsyncUpdate: boolean;
36 let lsDirSpy: jasmine.Spy;
37 let modalShowSpy: jasmine.Spy;
38 let notificationShowSpy: jasmine.Spy;
39 let minValidator: jasmine.Spy;
40 let maxValidator: jasmine.Spy;
41 let minBinaryValidator: jasmine.Spy;
42 let maxBinaryValidator: jasmine.Spy;
43 let modal: NgbModalRef;
45 // Get's private attributes or functions
47 nodeIds: (): { [path: string]: CephfsDir } => component['nodeIds'],
48 dirs: (): CephfsDir[] => component['dirs'],
49 requestedPaths: (): string[] => component['requestedPaths']
52 // Object contains mock data that will be reset before each test.
56 createdSnaps: CephfsSnapshot[] | any[];
57 deletedSnaps: CephfsSnapshot[] | any[];
58 updatedQuotas: { [path: string]: CephfsQuotas };
59 createdDirs: CephfsDir[];
62 // Object contains mock functions
64 quotas: (max_bytes: number, max_files: number): CephfsQuotas => ({ max_bytes, max_files }),
65 snapshots: (dirPath: string, howMany: number): CephfsSnapshot[] => {
66 const name = 'someSnapshot';
68 const oneDay = 3600 * 24 * 1000;
69 for (let i = 0; i < howMany; i++) {
70 const snapName = `${name}${i + 1}`;
71 const path = `${dirPath}/.snap/${snapName}`;
72 const created = new Date(+new Date() - oneDay * i).toString();
73 snapshots.push({ name: snapName, path, created });
77 dir: (parentPath: string, name: string, modifier: number): CephfsDir => {
78 const dirPath = `${parentPath === '/' ? '' : parentPath}/${name}`;
79 let snapshots = mockLib.snapshots(parentPath, modifier);
80 const extraSnapshots = mockData.createdSnaps.filter((s) => s.path === dirPath);
81 if (extraSnapshots.length > 0) {
82 snapshots = snapshots.concat(extraSnapshots);
84 const deletedSnapshots = mockData.deletedSnaps
85 .filter((s) => s.path === dirPath)
87 if (deletedSnapshots.length > 0) {
88 snapshots = snapshots.filter((s) => !deletedSnapshots.includes(s.name));
94 quotas: Object.assign(
95 mockLib.quotas(1024 * modifier, 10 * modifier),
96 mockData.updatedQuotas[dirPath] || {}
101 // Only used inside other mocks
102 lsSingleDir: (path = ''): CephfsDir[] => {
103 const customDirs = mockData.createdDirs.filter((d) => d.parent === path);
104 const isCustomDir = mockData.createdDirs.some((d) => d.path === path);
105 if (isCustomDir || path.includes('b')) {
106 // 'b' has no sub directories
109 return customDirs.concat([
110 // Directories are not sorted!
111 mockLib.dir(path, 'c', 3),
112 mockLib.dir(path, 'a', 1),
113 mockLib.dir(path, 'b', 2)
116 lsDir: (_id: number, path = ''): Observable<CephfsDir[]> => {
117 // will return 2 levels deep
118 let data = mockLib.lsSingleDir(path);
119 const paths = data.map((dir) => dir.path);
120 paths.forEach((pathL2) => {
121 data = data.concat(mockLib.lsSingleDir(pathL2));
123 if (path === '' || path === '/') {
124 // Adds root directory on ls of '/' to the directories list.
125 const root = mockLib.dir(path, '/', 1);
127 root.parent = undefined;
128 root.quotas = undefined;
129 data = [root].concat(data);
133 mkSnapshot: (_id: any, path: string, name: string): Observable<string> => {
134 mockData.createdSnaps.push({
137 created: new Date().toString()
141 rmSnapshot: (_id: any, path: string, name: string): Observable<string> => {
142 mockData.deletedSnaps.push({
145 created: new Date().toString()
149 updateQuota: (_id: any, path: string, updated: CephfsQuotas): Observable<string> => {
150 mockData.updatedQuotas[path] = Object.assign(mockData.updatedQuotas[path] || {}, updated);
151 return of('Response');
153 modalShow: (comp: Type<any>, init: any): any => {
154 modal = modalServiceShow(comp, init);
157 getNodeById: (path: string) => {
158 return mockLib.useNode(path);
160 updateNodes: (path: string) => {
161 const p: Promise<any[]> = component.treeOptions.getChildren({ id: path });
162 return noAsyncUpdate ? () => p : mockLib.asyncNodeUpdate(p);
164 asyncNodeUpdate: fakeAsync((p: Promise<any[]>) => {
166 mockData.nodes = mockData.nodes.concat(nodes);
170 changeId: (id: number) => {
171 // For some reason this spy has to be renewed after usage
172 spyOn(global, 'setTimeout').and.callFake((fn) => fn());
174 component.ngOnChanges();
175 mockData.nodes = component.nodes.concat(mockData.nodes);
177 selectNode: (path: string) => {
178 component.treeOptions.actionMapping.mouse.click(undefined, mockLib.useNode(path), undefined);
180 // Creates TreeNode with parents until root
181 useNode: (path: string): { id: string; parent: any; data: any; loadNodeChildren: Function } => {
182 const parentPath = path.split('/');
184 const parentIsRoot = parentPath.length === 1;
185 const parent = parentIsRoot ? { id: '/' } : mockLib.useNode(parentPath.join('/'));
190 loadNodeChildren: () => mockLib.updateNodes(path)
194 toggleActive: (_a: any, node: any, _b: any) => {
195 return mockLib.updateNodes(node.id);
198 mkDir: (path: string, name: string, maxFiles: number, maxBytes: number) => {
199 const dir = mockLib.dir(path, name, 3);
200 dir.quotas.max_bytes = maxBytes * 1024;
201 dir.quotas.max_files = maxFiles;
202 mockData.createdDirs.push(dir);
203 // Below is needed for quota tests only where 4 dirs are mocked
204 get.nodeIds()[dir.path] = dir;
205 mockData.nodes.push({ id: dir.path });
207 createSnapshotThroughModal: (name: string) => {
208 component.createSnapshot();
209 modal.componentInstance.onSubmitForm({ name });
211 deleteSnapshotsThroughModal: (snapshots: CephfsSnapshot[]) => {
212 component.snapshot.selection.selected = snapshots;
213 component.deleteSnapshotModal();
214 modal.componentInstance.callSubmitAction();
216 updateQuotaThroughModal: (attribute: string, value: number) => {
217 component.quota.selection.selected = component.settings.filter(
218 (q) => q.quotaKey === attribute
220 component.updateQuotaModal();
221 modal.componentInstance.onSubmitForm({ [attribute]: value });
223 unsetQuotaThroughModal: (attribute: string) => {
224 component.quota.selection.selected = component.settings.filter(
225 (q) => q.quotaKey === attribute
227 component.unsetQuotaModal();
228 modal.componentInstance.onSubmit();
230 setFourQuotaDirs: (quotas: number[][]) => {
231 expect(quotas.length).toBe(4); // Make sure this function is used correctly
233 quotas.forEach((quota, index) => {
235 mockLib.mkDir(path === '' ? '/' : path, index.toString(), quota[0], quota[1]);
247 parent: { value: '/', id: '/' }
251 mockLib.selectNode('/1/2/3/4');
255 // Expects that are used frequently
257 dirLength: (n: number) => expect(get.dirs().length).toBe(n),
258 nodeLength: (n: number) => expect(mockData.nodes.length).toBe(n),
259 lsDirCalledTimes: (n: number) => expect(lsDirSpy).toHaveBeenCalledTimes(n),
260 lsDirHasBeenCalledWith: (id: number, paths: string[]) => {
261 paths.forEach((path) => expect(lsDirSpy).toHaveBeenCalledWith(id, path));
262 assert.lsDirCalledTimes(paths.length);
264 requestedPaths: (expected: string[]) => expect(get.requestedPaths()).toEqual(expected),
265 snapshotsByName: (snaps: string[]) =>
266 expect(component.selectedDir.snapshots.map((s) => s.name)).toEqual(snaps),
267 dirQuotas: (bytes: number, files: number) => {
268 expect(component.selectedDir.quotas).toEqual({ max_bytes: bytes, max_files: files });
270 noQuota: (key: 'bytes' | 'files') => {
271 assert.quotaRow(key, '', 0, '');
273 quotaIsNotInherited: (key: 'bytes' | 'files', shownValue: any, nextMaximum: number) => {
274 const dir = component.selectedDir;
275 const path = dir.path;
276 assert.quotaRow(key, shownValue, nextMaximum, path);
278 quotaIsInherited: (key: 'bytes' | 'files', shownValue: any, path: string) => {
279 const isBytes = key === 'bytes';
280 const nextMaximum = get.nodeIds()[path].quotas[isBytes ? 'max_bytes' : 'max_files'];
281 assert.quotaRow(key, shownValue, nextMaximum, path);
284 key: 'bytes' | 'files',
285 shownValue: number | string,
286 nextTreeMaximum: number,
289 const isBytes = key === 'bytes';
290 expect(component.settings[isBytes ? 1 : 0]).toEqual({
292 name: `Max ${isBytes ? 'size' : key}`,
296 quotaKey: `max_${key}`,
297 dirValue: expect.any(Number),
299 value: nextTreeMaximum,
300 path: expect.any(String)
304 quotaUnsetModalTexts: (titleText: string, message: string, notificationMsg: string) => {
305 expect(modalShowSpy).toHaveBeenCalledWith(
306 ConfirmationModalComponent,
307 expect.objectContaining({
309 description: message,
313 expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
315 quotaUpdateModalTexts: (titleText: string, message: string, notificationMsg: string) => {
316 expect(modalShowSpy).toHaveBeenCalledWith(
318 expect.objectContaining({
321 submitButtonText: 'Save'
324 expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
326 quotaUpdateModalField: (
332 errors?: { [key: string]: string }
334 expect(modalShowSpy).toHaveBeenCalledWith(
336 expect.objectContaining({
344 validators: expect.anything(),
350 if (type === 'binary') {
351 expect(minBinaryValidator).toHaveBeenCalledWith(0);
352 expect(maxBinaryValidator).toHaveBeenCalledWith(max);
354 expect(minValidator).toHaveBeenCalledWith(0);
355 expect(maxValidator).toHaveBeenCalledWith(max);
363 HttpClientTestingModule,
367 ToastrModule.forRoot(),
370 declarations: [CephfsDirectoriesComponent],
373 { provide: 'titleText', useValue: '' },
374 { provide: 'buttonText', useValue: '' },
375 { provide: 'onSubmit', useValue: new Function() }
378 [CriticalConfirmationModalComponent, FormModalComponent, ConfirmationModalComponent]
382 noAsyncUpdate = false;
392 cephfsService = TestBed.inject(CephfsService);
393 lsDirSpy = spyOn(cephfsService, 'lsDir').and.callFake(mockLib.lsDir);
394 spyOn(cephfsService, 'mkSnapshot').and.callFake(mockLib.mkSnapshot);
395 spyOn(cephfsService, 'rmSnapshot').and.callFake(mockLib.rmSnapshot);
396 spyOn(cephfsService, 'quota').and.callFake(mockLib.updateQuota);
398 modalShowSpy = spyOn(TestBed.inject(ModalService), 'show').and.callFake(mockLib.modalShow);
399 notificationShowSpy = spyOn(TestBed.inject(NotificationService), 'show').and.stub();
401 fixture = TestBed.createComponent(CephfsDirectoriesComponent);
402 component = fixture.componentInstance;
403 fixture.detectChanges();
405 spyOn(TREE_ACTIONS, 'TOGGLE_ACTIVE').and.callFake(mockLib.treeActions.toggleActive);
407 component.treeComponent = {
408 sizeChanged: () => null,
409 treeModel: { getNodeById: mockLib.getNodeById, update: () => null }
413 it('should create', () => {
414 expect(component).toBeTruthy();
417 describe('mock self test', () => {
418 it('tests snapshots mock', () => {
419 expect(mockLib.snapshots('/a', 1).map((s) => ({ name: s.name, path: s.path }))).toEqual([
421 name: 'someSnapshot1',
422 path: '/a/.snap/someSnapshot1'
425 expect(mockLib.snapshots('/a/b', 3).map((s) => ({ name: s.name, path: s.path }))).toEqual([
427 name: 'someSnapshot1',
428 path: '/a/b/.snap/someSnapshot1'
431 name: 'someSnapshot2',
432 path: '/a/b/.snap/someSnapshot2'
435 name: 'someSnapshot3',
436 path: '/a/b/.snap/someSnapshot3'
441 it('tests dir mock', () => {
442 const path = '/a/b/c';
443 mockData.createdSnaps = [
444 { path, name: 's1' },
447 mockData.deletedSnaps = [
448 { path, name: 'someSnapshot2' },
451 const dir = mockLib.dir('/a/b', 'c', 2);
452 expect(dir.path).toBe('/a/b/c');
453 expect(dir.parent).toBe('/a/b');
454 expect(dir.quotas).toEqual({ max_bytes: 2048, max_files: 20 });
455 expect(dir.snapshots.map((s) => s.name)).toEqual(['someSnapshot1', 's1']);
458 it('tests lsdir mock', () => {
459 let dirs: CephfsDir[] = [];
460 mockLib.lsDir(2, '/a').subscribe((x) => (dirs = x));
461 expect(dirs.map((d) => d.path)).toEqual([
474 describe('test quota update mock', () => {
478 const updateQuota = (quotas: CephfsQuotas) => mockLib.updateQuota(ID, PATH, quotas);
480 const expectMockUpdate = (max_bytes?: number, max_files?: number) =>
481 expect(mockData.updatedQuotas[PATH]).toEqual({
486 const expectLsUpdate = (max_bytes?: number, max_files?: number) => {
488 mockLib.lsDir(ID, '/').subscribe((dirs) => (dir = dirs.find((d) => d.path === PATH)));
489 expect(dir.quotas).toEqual({
495 it('tests to set quotas', () => {
496 expectLsUpdate(1024, 10);
498 updateQuota({ max_bytes: 512 });
499 expectMockUpdate(512);
500 expectLsUpdate(512, 10);
502 updateQuota({ max_files: 100 });
503 expectMockUpdate(512, 100);
504 expectLsUpdate(512, 100);
507 it('tests to unset quotas', () => {
508 updateQuota({ max_files: 0 });
509 expectMockUpdate(undefined, 0);
510 expectLsUpdate(1024, 0);
512 updateQuota({ max_bytes: 0 });
513 expectMockUpdate(0, 0);
514 expectLsUpdate(0, 0);
519 it('calls lsDir only if an id exits', () => {
520 assert.lsDirCalledTimes(0);
523 assert.lsDirCalledTimes(1);
524 expect(lsDirSpy).toHaveBeenCalledWith(1, '/');
527 assert.lsDirCalledTimes(2);
528 expect(lsDirSpy).toHaveBeenCalledWith(2, '/');
531 describe('listing sub directories', () => {
535 * Tree looks like this:
543 it('expands first level', () => {
544 // Tree will only show '*' if nor 'loadChildren' or 'children' are defined
546 mockData.nodes.map((node: any) => ({
547 [node.id]: node.hasChildren || node.isExpanded || Boolean(node.children)
549 ).toEqual([{ '/': true }, { '/a': true }, { '/b': false }, { '/c': true }]);
552 it('resets all dynamic content on id change', () => {
553 mockLib.selectNode('/a');
555 * Tree looks like this:
564 assert.requestedPaths(['/', '/a']);
565 assert.nodeLength(7);
566 assert.dirLength(16);
567 expect(component.selectedDir).toBeDefined();
569 mockLib.changeId(undefined);
571 assert.requestedPaths([]);
572 expect(component.selectedDir).not.toBeDefined();
575 it('should select a node and show the directory contents', () => {
576 mockLib.selectNode('/a');
577 const dir = get.dirs().find((d) => d.path === '/a');
578 expect(component.selectedDir).toEqual(dir);
579 assert.quotaIsNotInherited('files', 10, 0);
580 assert.quotaIsNotInherited('bytes', '1 KiB', 0);
583 it('should extend the list by subdirectories when expanding', () => {
584 mockLib.selectNode('/a');
585 mockLib.selectNode('/a/c');
587 * Tree looks like this:
599 assert.lsDirCalledTimes(3);
600 assert.requestedPaths(['/', '/a', '/a/c']);
601 assert.dirLength(22);
602 assert.nodeLength(10);
605 it('should update the tree after each selection', () => {
606 const spy = spyOn(component.treeComponent, 'sizeChanged').and.callThrough();
607 expect(spy).toHaveBeenCalledTimes(0);
608 mockLib.selectNode('/a');
609 expect(spy).toHaveBeenCalledTimes(1);
610 mockLib.selectNode('/a/c');
611 expect(spy).toHaveBeenCalledTimes(2);
614 it('should select parent by path', () => {
615 mockLib.selectNode('/a');
616 mockLib.selectNode('/a/c');
617 mockLib.selectNode('/a/c/a');
618 component.selectOrigin('/a');
619 expect(component.selectedDir.path).toBe('/a');
622 it('should refresh directories with no sub directories as they could have some now', () => {
623 mockLib.selectNode('/b');
625 * Tree looks like this:
631 assert.lsDirCalledTimes(2);
632 assert.requestedPaths(['/', '/b']);
633 assert.nodeLength(4);
636 describe('used quotas', () => {
637 it('should use no quota if none is set', () => {
638 mockLib.setFourQuotaDirs([
644 assert.noQuota('files');
645 assert.noQuota('bytes');
646 assert.dirQuotas(0, 0);
649 it('should use quota from upper parents', () => {
650 mockLib.setFourQuotaDirs([
656 assert.quotaIsInherited('files', 100, '/1');
657 assert.quotaIsInherited('bytes', '8 KiB', '/1/2');
658 assert.dirQuotas(0, 0);
661 it('should use quota from the parent with the lowest value (deep inheritance)', () => {
662 mockLib.setFourQuotaDirs([
668 assert.quotaIsInherited('files', 100, '/1/2');
669 assert.quotaIsInherited('bytes', '1 KiB', '/1');
670 assert.dirQuotas(2048, 300);
673 it('should use current value', () => {
674 mockLib.setFourQuotaDirs([
680 assert.quotaIsNotInherited('files', 100, 200);
681 assert.quotaIsNotInherited('bytes', '1 KiB', 2048);
682 assert.dirQuotas(1024, 100);
687 // skipping this since cds-modal is currently not testable
688 // within the unit tests because of the absence of placeholder
689 describe.skip('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'],
740 actions: ['Create', 'Delete'],
796 // skipping this since cds-modal is currently not testable
797 // within the unit tests because of the absence of placeholder
798 describe.skip('quotas', () => {
801 minValidator = spyOn(Validators, 'min').and.callThrough();
802 maxValidator = spyOn(Validators, 'max').and.callThrough();
803 minBinaryValidator = spyOn(CdValidators, 'binaryMin').and.callThrough();
804 maxBinaryValidator = spyOn(CdValidators, 'binaryMax').and.callThrough();
807 mockLib.selectNode('/a');
808 mockLib.selectNode('/a/c');
809 mockLib.selectNode('/a/c/b');
810 // Quotas after selection
811 assert.quotaIsInherited('files', 10, '/a');
812 assert.quotaIsInherited('bytes', '1 KiB', '/a');
813 assert.dirQuotas(2048, 20);
816 describe('update modal', () => {
817 describe('max_files', () => {
819 mockLib.updateQuotaThroughModal('max_files', 5);
822 it('should update max_files correctly', () => {
823 expect(cephfsService.quota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 5 });
824 assert.quotaIsNotInherited('files', 5, 10);
827 it('uses the correct form field', () => {
828 assert.quotaUpdateModalField('number', 'Max files', 'max_files', 20, 10, {
829 min: 'Value has to be at least 0 or more',
830 max: 'Value has to be at most 10 or less'
834 it('shows the right texts', () => {
835 assert.quotaUpdateModalTexts(
836 `Update CephFS files quota for '/a/c/b'`,
837 `The inherited files quota 10 from '/a' is the maximum value to be used.`,
838 `Updated CephFS files quota for '/a/c/b'`
843 describe('max_bytes', () => {
845 mockLib.updateQuotaThroughModal('max_bytes', 512);
848 it('should update max_files correctly', () => {
849 expect(cephfsService.quota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 512 });
850 assert.quotaIsNotInherited('bytes', '512 B', 1024);
853 it('uses the correct form field', () => {
854 mockLib.updateQuotaThroughModal('max_bytes', 512);
855 assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 2048, 1024);
858 it('shows the right texts', () => {
859 assert.quotaUpdateModalTexts(
860 `Update CephFS size quota for '/a/c/b'`,
861 `The inherited size quota 1 KiB from '/a' is the maximum value to be used.`,
862 `Updated CephFS size quota for '/a/c/b'`
867 describe('action behaviour', () => {
868 it('opens with next maximum as maximum if directory holds the current maximum', () => {
869 mockLib.updateQuotaThroughModal('max_bytes', 512);
870 mockLib.updateQuotaThroughModal('max_bytes', 888);
871 assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 512, 1024);
874 it(`uses 'Set' action instead of 'Update' if the quota is not set (0)`, () => {
875 mockLib.updateQuotaThroughModal('max_bytes', 0);
876 mockLib.updateQuotaThroughModal('max_bytes', 200);
877 assert.quotaUpdateModalTexts(
878 `Set CephFS size quota for '/a/c/b'`,
879 `The inherited size quota 1 KiB from '/a' is the maximum value to be used.`,
880 `Set CephFS size quota for '/a/c/b'`
886 describe('unset modal', () => {
887 describe('max_files', () => {
889 mockLib.updateQuotaThroughModal('max_files', 5); // Sets usable quota
890 mockLib.unsetQuotaThroughModal('max_files');
893 it('should unset max_files correctly', () => {
894 expect(cephfsService.quota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 0 });
895 assert.dirQuotas(2048, 0);
898 it('shows the right texts', () => {
899 assert.quotaUnsetModalTexts(
900 `Unset CephFS files quota for '/a/c/b'`,
901 `Unset files quota 5 from '/a/c/b' in order to inherit files quota 10 from '/a'.`,
902 `Unset CephFS files quota for '/a/c/b'`
907 describe('max_bytes', () => {
909 mockLib.updateQuotaThroughModal('max_bytes', 512); // Sets usable quota
910 mockLib.unsetQuotaThroughModal('max_bytes');
913 it('should unset max_files correctly', () => {
914 expect(cephfsService.quota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 0 });
915 assert.dirQuotas(0, 20);
918 it('shows the right texts', () => {
919 assert.quotaUnsetModalTexts(
920 `Unset CephFS size quota for '/a/c/b'`,
921 `Unset size quota 512 B from '/a/c/b' in order to inherit size quota 1 KiB from '/a'.`,
922 `Unset CephFS size quota for '/a/c/b'`
927 describe('action behaviour', () => {
928 it('uses different Text if no quota is inherited', () => {
929 mockLib.selectNode('/a');
930 mockLib.unsetQuotaThroughModal('max_bytes');
931 assert.quotaUnsetModalTexts(
932 `Unset CephFS size quota for '/a'`,
933 `Unset size quota 1 KiB from '/a' in order to have no quota on the directory.`,
934 `Unset CephFS size quota for '/a'`
938 it('uses different Text if quota is already inherited', () => {
939 mockLib.unsetQuotaThroughModal('max_bytes');
940 assert.quotaUnsetModalTexts(
941 `Unset CephFS size quota for '/a/c/b'`,
942 `Unset size quota 2 KiB from '/a/c/b' which isn't used because of the inheritance ` +
943 `of size quota 1 KiB from '/a'.`,
944 `Unset CephFS size quota for '/a/c/b'`
951 describe('table actions', () => {
952 let actions: CdTableAction[];
954 const empty = (): CdTableSelection => new CdTableSelection();
956 const select = (value: number): CdTableSelection => {
957 const selection = new CdTableSelection();
958 selection.selected = [{ dirValue: value }];
963 actions = component.quota.tableActions;
966 it(`shows 'Set' for empty and not set quotas`, () => {
967 const isSetVisible = actions[0].visible;
968 expect(isSetVisible(empty())).toBe(true);
969 expect(isSetVisible(select(0))).toBe(true);
970 expect(isSetVisible(select(1))).toBe(false);
973 it(`shows 'Update' for set quotas only`, () => {
974 const isUpdateVisible = actions[1].visible;
975 expect(isUpdateVisible(empty())).toBeFalsy();
976 expect(isUpdateVisible(select(0))).toBe(false);
977 expect(isUpdateVisible(select(1))).toBe(true);
980 it(`only enables 'Unset' for set quotas only`, () => {
981 const isUnsetDisabled = actions[2].disable;
982 expect(isUnsetDisabled(empty())).toBe(true);
983 expect(isUnsetDisabled(select(0))).toBe(true);
984 expect(isUnsetDisabled(select(1))).toBe(false);
987 it('should test all quota table actions permission combinations', () => {
988 const permissionHelper: PermissionHelper = new PermissionHelper(component.permission, {
989 single: { dirValue: 0 },
990 multiple: [{ dirValue: 0 }, {}]
992 const tableActions = permissionHelper.setPermissionsAndGetActions(
993 component.quota.tableActions
996 expect(tableActions).toEqual({
997 'create,update,delete': {
998 actions: ['Set', 'Update', 'Unset'],
1007 actions: ['Set', 'Update', 'Unset'],
1034 actions: ['Set', 'Update', 'Unset'],
1043 actions: ['Set', 'Update', 'Unset'],
1073 describe('reload all', () => {
1074 const calledPaths = ['/', '/a', '/a/c', '/a/c/a', '/a/c/a/b'];
1076 const dirsByPath = (): string[] => get.dirs().map((d) => d.path);
1079 mockLib.changeId(1);
1080 mockLib.selectNode('/a');
1081 mockLib.selectNode('/a/c');
1082 mockLib.selectNode('/a/c/a');
1083 mockLib.selectNode('/a/c/a/b');
1086 it('should reload all requested paths', () => {
1087 assert.lsDirHasBeenCalledWith(1, calledPaths);
1088 lsDirSpy.calls.reset();
1089 assert.lsDirHasBeenCalledWith(1, []);
1090 component.refreshAllDirectories();
1091 assert.lsDirHasBeenCalledWith(1, calledPaths);
1094 it('should reload all requested paths if not selected anything', () => {
1095 lsDirSpy.calls.reset();
1096 mockLib.changeId(2);
1097 assert.lsDirHasBeenCalledWith(2, ['/']);
1098 lsDirSpy.calls.reset();
1099 component.refreshAllDirectories();
1100 assert.lsDirHasBeenCalledWith(2, ['/']);
1103 it('should add new directories', () => {
1104 // Create two new directories in preparation
1105 const dirsBeforeRefresh = dirsByPath();
1106 expect(dirsBeforeRefresh.includes('/a/c/has_dir_now')).toBe(false);
1107 mockLib.mkDir('/a/c', 'has_dir_now', 0, 0);
1108 mockLib.mkDir('/a/c/a/b', 'has_dir_now_too', 0, 0);
1109 // Now the new directories will be fetched
1110 component.refreshAllDirectories();
1111 const dirsAfterRefresh = dirsByPath();
1112 expect(dirsAfterRefresh.length - dirsBeforeRefresh.length).toBe(2);
1113 expect(dirsAfterRefresh.includes('/a/c/has_dir_now')).toBe(true);
1114 expect(dirsAfterRefresh.includes('/a/c/a/b/has_dir_now_too')).toBe(true);
1117 it('should remove deleted directories', () => {
1118 // Create one new directory and refresh in order to have it added to the directories list
1119 mockLib.mkDir('/a/c', 'will_be_removed_shortly', 0, 0);
1120 component.refreshAllDirectories();
1121 const dirsBeforeRefresh = dirsByPath();
1122 expect(dirsBeforeRefresh.includes('/a/c/will_be_removed_shortly')).toBe(true);
1123 mockData.createdDirs = []; // Mocks the deletion of the directory
1124 // Now the deleted directory will be missing on refresh
1125 component.refreshAllDirectories();
1126 const dirsAfterRefresh = dirsByPath();
1127 expect(dirsAfterRefresh.length - dirsBeforeRefresh.length).toBe(-1);
1128 expect(dirsAfterRefresh.includes('/a/c/will_be_removed_shortly')).toBe(false);
1131 describe('loading indicator', () => {
1133 noAsyncUpdate = true;
1136 it('should have set loading indicator to false after refreshing all dirs', fakeAsync(() => {
1137 component.refreshAllDirectories();
1138 expect(component.loadingIndicator).toBe(true);
1139 tick(3000); // To resolve all promises
1140 expect(component.loadingIndicator).toBe(false);
1143 it('should only update the tree once and not on every call', fakeAsync(() => {
1144 const spy = spyOn(component.treeComponent, 'sizeChanged').and.callThrough();
1145 component.refreshAllDirectories();
1146 expect(spy).toHaveBeenCalledTimes(0);
1147 tick(3000); // To resolve all promises
1148 // Called during the interval and at the end of timeout
1149 expect(spy).toHaveBeenCalledTimes(2);
1152 it('should have set all loaded dirs as attribute names of "indicators"', () => {
1153 noAsyncUpdate = false;
1154 component.refreshAllDirectories();
1155 expect(Object.keys(component.loading).sort()).toEqual(calledPaths);
1158 it('should set an indicator to true during load', () => {
1159 lsDirSpy.and.callFake(() => new Observable((): null => null));
1160 component.refreshAllDirectories();
1161 expect(Object.values(component.loading).every((b) => b)).toBe(true);
1162 expect(component.loadingIndicator).toBe(true);
1165 describe('disable create snapshot', () => {
1166 let actions: CdTableAction[];
1168 actions = component.snapshot.tableActions;
1169 mockLib.mkDir('/', 'volumes', 2, 2);
1170 mockLib.mkDir('/volumes', 'group1', 2, 2);
1171 mockLib.mkDir('/volumes/group1', 'subvol', 2, 2);
1172 mockLib.mkDir('/volumes/group1/subvol', 'subfile', 2, 2);
1175 const empty = (): CdTableSelection => new CdTableSelection();
1177 it('should return a descriptive message to explain why it is disabled', () => {
1178 const path = '/volumes/group1/subvol/subfile';
1179 const res = 'Cannot create snapshots for files/folders in the subvolume subvol';
1180 mockLib.selectNode(path);
1181 expect(actions[0].disable(empty())).toContain(res);
1184 it('should return false if it is not a subvolume node', () => {
1186 '/volumes/group1/subvol',
1193 testCases.forEach((testCase) => {
1194 mockLib.selectNode(testCase);
1195 expect(actions[0].disable(empty())).toBeFalsy();