1 import { HttpClientTestingModule } from '@angular/common/http/testing';
2 import { DebugElement, 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 { TreeViewComponent, TreeviewModule } from 'carbon-components-angular';
8 import { NgbActiveModal, NgbModalModule, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
9 import { ToastrModule } from 'ngx-toastr';
10 import { Observable, of } from 'rxjs';
11 import _ from 'lodash';
13 import { CephfsService } from '~/app/shared/api/cephfs.service';
14 import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
15 import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
16 import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
17 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
18 import { CdValidators } from '~/app/shared/forms/cd-validators';
19 import { CdTableAction } from '~/app/shared/models/cd-table-action';
20 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
25 } from '~/app/shared/models/cephfs-directory-models';
26 import { ModalService } from '~/app/shared/services/modal.service';
27 import { NotificationService } from '~/app/shared/services/notification.service';
28 import { SharedModule } from '~/app/shared/shared.module';
29 import { configureTestBed, modalServiceShow, PermissionHelper } from '~/testing/unit-test-helper';
30 import { CephfsDirectoriesComponent } from './cephfs-directories.component';
31 import { Node } from 'carbon-components-angular/treeview/tree-node.types';
32 import { By } from '@angular/platform-browser';
34 describe('CephfsDirectoriesComponent', () => {
35 let component: CephfsDirectoriesComponent;
36 let fixture: ComponentFixture<CephfsDirectoriesComponent>;
37 let cephfsService: CephfsService;
38 let noAsyncUpdate: boolean;
39 let lsDirSpy: jasmine.Spy;
40 let modalShowSpy: jasmine.Spy;
41 let notificationShowSpy: jasmine.Spy;
42 let minValidator: jasmine.Spy;
43 let maxValidator: jasmine.Spy;
44 let minBinaryValidator: jasmine.Spy;
45 let maxBinaryValidator: jasmine.Spy;
46 let modal: NgbModalRef;
47 let treeComponent: DebugElement;
48 let testUsedQuotas: boolean;
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
110 { name: 'c', modifier: 3 },
111 { name: 'a', modifier: 1 },
112 { name: 'b', modifier: 2 }
115 const customDirs = mockData.createdDirs.filter((d) => d.parent === path);
116 const isCustomDir = mockData.createdDirs.some((d) => d.path === path);
117 if (isCustomDir || path.includes('b')) {
118 // 'b' has no sub directories
121 return customDirs.concat(
122 // Directories are not sorted!
123 names.map((x: any) => mockLib.dir(x?.path || path, x.name, x.modifier))
126 lsDir: (_id: number, path = ''): Observable<CephfsDir[]> => {
127 // will return 2 levels deep
128 let data = mockLib.lsSingleDir(path);
130 if (testUsedQuotas) {
131 const parents = mockLib.lsSingleDir(path, [
132 { name: 'c', modifier: 3 },
133 { name: 'a', modifier: 1 },
134 { name: 'b', modifier: 2 },
135 { path: '', name: '1', modifier: 1 },
136 { path: '/1', name: '2', modifier: 1 },
137 { path: '/1/2', name: '3', modifier: 1 }
139 data = data.concat(parents);
141 const paths = data.map((dir) => dir.path);
142 paths.forEach((pathL2) => {
143 data = data.concat(mockLib.lsSingleDir(pathL2));
145 if (path === '' || path === '/') {
146 // Adds root directory on ls of '/' to the directories list.
147 const root = mockLib.dir(path, '/', 1);
149 root.parent = undefined;
150 root.quotas = undefined;
151 data = [root].concat(data);
155 mkSnapshot: (_id: any, path: string, name: string): Observable<string> => {
156 mockData.createdSnaps.push({
159 created: new Date().toString()
163 rmSnapshot: (_id: any, path: string, name: string): Observable<string> => {
164 mockData.deletedSnaps.push({
167 created: new Date().toString()
171 updateQuota: (_id: any, path: string, updated: CephfsQuotas): Observable<string> => {
172 mockData.updatedQuotas[path] = Object.assign(mockData.updatedQuotas[path] || {}, updated);
173 return of('Response');
175 modalShow: (comp: Type<any>, init: any): any => {
176 modal = modalServiceShow(comp, init);
179 getNodeById: (path: string) => {
180 return mockLib.useNode(path);
182 updateNodes: (path: string) => {
183 // const p: Promise<any[]> = component.treeOptions.getChildren({ id: path });
184 const p: Promise<Node[]> = component.updateDirectory(path);
185 return noAsyncUpdate ? () => p : mockLib.asyncNodeUpdate(p);
187 asyncNodeUpdate: fakeAsync((p: Promise<any[]>) => {
189 mockData.nodes = mockData.nodes.concat(nodes);
193 flattenTree: (tree: Node[], memoised: Node[] = []) => {
194 let result = memoised;
195 tree.some((node) => {
196 result = [node, ...mockLib.flattenTree(node?.children || [], result)];
198 return _.sortBy(result, 'id');
200 changeId: (id: number) => {
202 component.ngOnChanges();
203 mockData.nodes = mockLib.flattenTree(component.nodes).concat(mockData.nodes);
205 selectNode: (path: string) => {
206 // component.treeOptions.actionMapping.mouse.click(undefined, mockLib.useNode(path), undefined);
207 const node = mockLib.useNode(path);
208 component.selectNode(node);
210 // Creates TreeNode with parents until root
211 useNode: (path: string): Node => {
212 const parentPath = path.split('/');
214 const parentIsRoot = parentPath.length === 1;
215 const parent = parentIsRoot ? { id: '/' } : mockLib.useNode(parentPath.join('/'));
220 value: { parent: parent?.id }
224 toggleActive: (node: Node) => {
225 return mockLib.updateNodes(node.id);
228 mkDir: (path: string, name: string, maxFiles: number, maxBytes: number) => {
229 const dir = mockLib.dir(path, name, 3);
230 dir.quotas.max_bytes = maxBytes * 1024;
231 dir.quotas.max_files = maxFiles;
232 mockData.createdDirs.push(dir);
233 // Below is needed for quota tests only where 4 dirs are mocked
234 get.nodeIds()[dir.path] = dir;
235 const node = mockLib.useNode(dir.path);
236 mockData.nodes.push(node);
238 createSnapshotThroughModal: (name: string) => {
239 component.createSnapshot();
240 modal.componentInstance.onSubmitForm({ name });
242 deleteSnapshotsThroughModal: (snapshots: CephfsSnapshot[]) => {
243 component.snapshot.selection.selected = snapshots;
244 component.deleteSnapshotModal();
245 modal.componentInstance.callSubmitAction();
247 updateQuotaThroughModal: (attribute: string, value: number) => {
248 component.quota.selection.selected = component.settings.filter(
249 (q) => q.quotaKey === attribute
251 component.updateQuotaModal();
252 modal.componentInstance.onSubmitForm({ [attribute]: value });
254 unsetQuotaThroughModal: (attribute: string) => {
255 component.quota.selection.selected = component.settings.filter(
256 (q) => q.quotaKey === attribute
258 component.unsetQuotaModal();
259 modal.componentInstance.onSubmit();
261 setFourQuotaDirs: (quotas: number[][]) => {
262 expect(quotas.length).toBe(4); // Make sure this function is used correctly
264 quotas.forEach((quota, index) => {
266 mockLib.mkDir(path === '' ? '/' : path, index.toString(), quota[0], quota[1]);
278 parent: { value: '/', id: '/' }
282 mockLib.selectNode('/1/2/3/4');
286 // Expects that are used frequently
288 dirLength: (n: number) => expect(get.dirs().length).toBe(n),
289 nodeLength: (n: number) => expect(mockData.nodes?.length).toBe(n),
290 lsDirCalledTimes: (n: number) => expect(lsDirSpy).toHaveBeenCalledTimes(n),
291 lsDirHasBeenCalledWith: (id: number, paths: string[]) => {
292 paths.forEach((path) => expect(lsDirSpy).toHaveBeenCalledWith(id, path));
293 assert.lsDirCalledTimes(paths.length);
295 requestedPaths: (expected: string[]) => expect(get.requestedPaths()).toEqual(expected),
296 snapshotsByName: (snaps: string[]) =>
297 expect(component.selectedDir.snapshots.map((s) => s.name)).toEqual(snaps),
298 dirQuotas: (bytes: number, files: number) => {
299 expect(component.selectedDir.quotas).toEqual({ max_bytes: bytes, max_files: files });
301 noQuota: (key: 'bytes' | 'files') => {
302 assert.quotaRow(key, '', 0, '');
304 quotaIsNotInherited: (key: 'bytes' | 'files', shownValue: any, nextMaximum: number) => {
305 const dir = component.selectedDir;
306 const path = dir.path;
307 assert.quotaRow(key, shownValue, nextMaximum, path);
309 quotaIsInherited: (key: 'bytes' | 'files', shownValue: any, path: string) => {
310 const isBytes = key === 'bytes';
311 const nextMaximum = get.nodeIds()[path].quotas[isBytes ? 'max_bytes' : 'max_files'];
312 assert.quotaRow(key, shownValue, nextMaximum, path);
315 key: 'bytes' | 'files',
316 shownValue: number | string,
317 nextTreeMaximum: number,
320 const isBytes = key === 'bytes';
321 expect(component.settings[isBytes ? 1 : 0]).toEqual({
323 name: `Max ${isBytes ? 'size' : key}`,
327 quotaKey: `max_${key}`,
328 dirValue: expect.any(Number),
330 value: nextTreeMaximum,
331 path: expect.any(String)
335 quotaUnsetModalTexts: (titleText: string, message: string, notificationMsg: string) => {
336 expect(modalShowSpy).toHaveBeenCalledWith(
337 ConfirmationModalComponent,
338 expect.objectContaining({
340 description: message,
344 expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
346 quotaUpdateModalTexts: (titleText: string, message: string, notificationMsg: string) => {
347 expect(modalShowSpy).toHaveBeenCalledWith(
349 expect.objectContaining({
352 submitButtonText: 'Save'
355 expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
357 quotaUpdateModalField: (
363 errors?: { [key: string]: string }
365 expect(modalShowSpy).toHaveBeenCalledWith(
367 expect.objectContaining({
375 validators: expect.anything(),
381 if (type === 'binary') {
382 expect(minBinaryValidator).toHaveBeenCalledWith(0);
383 expect(maxBinaryValidator).toHaveBeenCalledWith(max);
385 expect(minValidator).toHaveBeenCalledWith(0);
386 expect(maxValidator).toHaveBeenCalledWith(max);
394 HttpClientTestingModule,
398 ToastrModule.forRoot(),
401 declarations: [CephfsDirectoriesComponent],
402 providers: [NgbActiveModal]
404 [CriticalConfirmationModalComponent, FormModalComponent, ConfirmationModalComponent]
408 noAsyncUpdate = false;
418 cephfsService = TestBed.inject(CephfsService);
419 lsDirSpy = spyOn(cephfsService, 'lsDir').and.callFake(mockLib.lsDir);
420 spyOn(cephfsService, 'mkSnapshot').and.callFake(mockLib.mkSnapshot);
421 spyOn(cephfsService, 'rmSnapshot').and.callFake(mockLib.rmSnapshot);
422 spyOn(cephfsService, 'quota').and.callFake(mockLib.updateQuota);
423 spyOn(global, 'setTimeout').and.callFake((fn) => fn());
425 modalShowSpy = spyOn(TestBed.inject(ModalService), 'show').and.callFake(mockLib.modalShow);
426 notificationShowSpy = spyOn(TestBed.inject(NotificationService), 'show').and.stub();
428 fixture = TestBed.createComponent(CephfsDirectoriesComponent);
429 component = fixture.componentInstance;
430 fixture.detectChanges();
431 treeComponent = fixture.debugElement.query(By.directive(TreeViewComponent));
433 // spyOn(TREE_ACTIONS, 'TOGGLE_ACTIVE').and.callFake(mockLib.treeActions.toggleActive);
434 // spyOn(component, 'selectNode').and.callFake(mockLib.treeActions.toggleActive);
435 // spyOn(component, 'getNode').and.callFake(mockLib.useNode);
437 component.treeComponent = treeComponent.componentInstance as TreeViewComponent;
440 it('should create', () => {
441 expect(component).toBeTruthy();
444 describe('mock self test', () => {
445 it('tests snapshots mock', () => {
446 expect(mockLib.snapshots('/a', 1).map((s) => ({ name: s.name, path: s.path }))).toEqual([
448 name: 'someSnapshot1',
449 path: '/a/.snap/someSnapshot1'
452 expect(mockLib.snapshots('/a/b', 3).map((s) => ({ name: s.name, path: s.path }))).toEqual([
454 name: 'someSnapshot1',
455 path: '/a/b/.snap/someSnapshot1'
458 name: 'someSnapshot2',
459 path: '/a/b/.snap/someSnapshot2'
462 name: 'someSnapshot3',
463 path: '/a/b/.snap/someSnapshot3'
468 it('tests dir mock', () => {
469 const path = '/a/b/c';
470 mockData.createdSnaps = [
471 { path, name: 's1' },
474 mockData.deletedSnaps = [
475 { path, name: 'someSnapshot2' },
478 const dir = mockLib.dir('/a/b', 'c', 2);
479 expect(dir.path).toBe('/a/b/c');
480 expect(dir.parent).toBe('/a/b');
481 expect(dir.quotas).toEqual({ max_bytes: 2048, max_files: 20 });
482 expect(dir.snapshots.map((s) => s.name)).toEqual(['someSnapshot1', 's1']);
485 it('tests lsdir mock', () => {
486 let dirs: CephfsDir[] = [];
487 mockLib.lsDir(2, '/a').subscribe((x) => (dirs = x));
488 expect(dirs.map((d) => d.path)).toEqual([
501 describe('test quota update mock', () => {
505 const updateQuota = (quotas: CephfsQuotas) => mockLib.updateQuota(ID, PATH, quotas);
507 const expectMockUpdate = (max_bytes?: number, max_files?: number) =>
508 expect(mockData.updatedQuotas[PATH]).toEqual({
513 const expectLsUpdate = (max_bytes?: number, max_files?: number) => {
515 mockLib.lsDir(ID, '/').subscribe((dirs) => (dir = dirs.find((d) => d.path === PATH)));
516 expect(dir.quotas).toEqual({
522 it('tests to set quotas', () => {
523 expectLsUpdate(1024, 10);
525 updateQuota({ max_bytes: 512 });
526 expectMockUpdate(512);
527 expectLsUpdate(512, 10);
529 updateQuota({ max_files: 100 });
530 expectMockUpdate(512, 100);
531 expectLsUpdate(512, 100);
534 it('tests to unset quotas', () => {
535 updateQuota({ max_files: 0 });
536 expectMockUpdate(undefined, 0);
537 expectLsUpdate(1024, 0);
539 updateQuota({ max_bytes: 0 });
540 expectMockUpdate(0, 0);
541 expectLsUpdate(0, 0);
546 it('calls lsDir only if an id exits', () => {
547 assert.lsDirCalledTimes(0);
550 assert.lsDirCalledTimes(1);
551 expect(lsDirSpy).toHaveBeenCalledWith(1, '/');
554 assert.lsDirCalledTimes(2);
555 expect(lsDirSpy).toHaveBeenCalledWith(2, '/');
558 describe('listing sub directories', () => {
562 * Tree looks like this:
570 it('expands first level', () => {
571 // Tree will only show '*' if nor 'loadChildren' or 'children' are defined
572 const actual = mockData.nodes.map((node: Node) => ({
573 [node.id]: node?.expanded || Boolean(node?.children?.length)
607 expect(actual).toEqual(expected);
610 it('resets all dynamic content on id change', () => {
611 mockLib.selectNode('/a');
613 * Tree looks like this:
622 assert.requestedPaths(['/', '/a']);
623 assert.nodeLength(10);
624 assert.dirLength(16);
625 expect(component.selectedDir).toBeDefined();
627 mockLib.changeId(undefined);
629 assert.requestedPaths([]);
630 expect(component.selectedDir).not.toBeDefined();
633 it('should select a node and show the directory contents', () => {
634 mockLib.selectNode('/a');
635 const dir = get.dirs().find((d) => d.path === '/a');
636 expect(component.selectedDir).toEqual(dir);
637 assert.quotaIsNotInherited('files', 10, 0);
638 assert.quotaIsNotInherited('bytes', '1 KiB', 0);
641 it('should extend the list by subdirectories when expanding', () => {
642 mockLib.selectNode('/a');
643 mockLib.selectNode('/a/c');
645 * Tree looks like this:
657 assert.lsDirCalledTimes(3);
658 assert.requestedPaths(['/', '/a', '/a/c']);
659 assert.dirLength(22);
660 assert.nodeLength(10);
663 it('should update the tree after each selection', () => {
664 const spy = spyOn(component, 'selectNode').and.callThrough();
665 expect(spy).toHaveBeenCalledTimes(0);
666 mockLib.selectNode('/a');
667 expect(spy).toHaveBeenCalledTimes(1);
668 mockLib.selectNode('/a/c');
669 expect(spy).toHaveBeenCalledTimes(2);
672 it('should select parent by path', () => {
673 mockLib.selectNode('/a');
674 mockLib.selectNode('/a/c');
675 mockLib.selectNode('/a/c/a');
676 component.selectOrigin('/a');
677 console.debug('component.selectedDir', component.selectedDir);
678 expect(component.selectedDir.path).toBe('/a');
681 it('should refresh directories with no sub directories as they could have some now', () => {
682 mockLib.selectNode('/b');
684 * Tree looks like this:
690 assert.lsDirCalledTimes(2);
691 assert.requestedPaths(['/', '/b']);
692 assert.nodeLength(10);
695 describe('used quotas', () => {
697 testUsedQuotas = true;
701 testUsedQuotas = false;
704 it('should use no quota if none is set', () => {
705 mockLib.setFourQuotaDirs([
711 assert.noQuota('files');
712 assert.noQuota('bytes');
713 assert.dirQuotas(0, 0);
716 it('should use quota from upper parents', () => {
717 mockLib.setFourQuotaDirs([
723 assert.quotaIsInherited('files', 100, '/1');
724 assert.quotaIsInherited('bytes', '8 KiB', '/1/2');
725 assert.dirQuotas(0, 0);
728 it('should use quota from the parent with the lowest value (deep inheritance)', () => {
729 mockLib.setFourQuotaDirs([
735 assert.quotaIsInherited('files', 100, '/1/2');
736 assert.quotaIsInherited('bytes', '1 KiB', '/1');
737 assert.dirQuotas(2048, 300);
740 it('should use current value', () => {
741 mockLib.setFourQuotaDirs([
747 assert.quotaIsNotInherited('files', 100, 200);
748 assert.quotaIsNotInherited('bytes', '1 KiB', 2048);
749 assert.dirQuotas(1024, 100);
754 // skipping this since cds-modal is currently not testable
755 // within the unit tests because of the absence of placeholder7
756 describe.skip('snapshots', () => {
759 mockLib.selectNode('/a');
762 it('should create a snapshot', () => {
763 mockLib.createSnapshotThroughModal('newSnap');
764 expect(cephfsService.mkSnapshot).toHaveBeenCalledWith(1, '/a', 'newSnap');
765 assert.snapshotsByName(['someSnapshot1', 'newSnap']);
768 it('should delete a snapshot', () => {
769 mockLib.createSnapshotThroughModal('deleteMe');
770 mockLib.deleteSnapshotsThroughModal([component.selectedDir.snapshots[1]]);
771 assert.snapshotsByName(['someSnapshot1']);
774 it('should delete all snapshots', () => {
775 mockLib.createSnapshotThroughModal('deleteAll');
776 mockLib.deleteSnapshotsThroughModal(component.selectedDir.snapshots);
777 assert.snapshotsByName([]);
781 // Need to change PermissionHelper to reflect latest changes to table actions component
782 it.skip('should test all snapshot table actions combinations', () => {
783 const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
784 const tableActions = permissionHelper.setPermissionsAndGetActions(
785 component.snapshot.tableActions
788 expect(tableActions).toEqual({
789 'create,update,delete': {
790 actions: ['Create', 'Delete'],
791 primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
795 primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
798 actions: ['Create', 'Delete'],
799 primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
803 primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
807 primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
811 primary: { multiple: '', executing: '', single: '', no: '' }
815 primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
819 primary: { multiple: '', executing: '', single: '', no: '' }
824 // skipping this since cds-modal is currently not testable
825 // within the unit tests because of the absence of placeholder
826 describe.skip('quotas', () => {
829 minValidator = spyOn(Validators, 'min').and.callThrough();
830 maxValidator = spyOn(Validators, 'max').and.callThrough();
831 minBinaryValidator = spyOn(CdValidators, 'binaryMin').and.callThrough();
832 maxBinaryValidator = spyOn(CdValidators, 'binaryMax').and.callThrough();
835 mockLib.selectNode('/a');
836 mockLib.selectNode('/a/c');
837 mockLib.selectNode('/a/c/b');
838 // Quotas after selection
839 assert.quotaIsInherited('files', 10, '/a');
840 assert.quotaIsInherited('bytes', '1 KiB', '/a');
841 assert.dirQuotas(2048, 20);
844 describe('update modal', () => {
845 describe('max_files', () => {
847 mockLib.updateQuotaThroughModal('max_files', 5);
850 it('should update max_files correctly', () => {
851 expect(cephfsService.quota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 5 });
852 assert.quotaIsNotInherited('files', 5, 10);
855 it('uses the correct form field', () => {
856 assert.quotaUpdateModalField('number', 'Max files', 'max_files', 20, 10, {
857 min: 'Value has to be at least 0 or more',
858 max: 'Value has to be at most 10 or less'
862 it('shows the right texts', () => {
863 assert.quotaUpdateModalTexts(
864 `Update CephFS files quota for '/a/c/b'`,
865 `The inherited files quota 10 from '/a' is the maximum value to be used.`,
866 `Updated CephFS files quota for '/a/c/b'`
871 describe('max_bytes', () => {
873 mockLib.updateQuotaThroughModal('max_bytes', 512);
876 it('should update max_files correctly', () => {
877 expect(cephfsService.quota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 512 });
878 assert.quotaIsNotInherited('bytes', '512 B', 1024);
881 it('uses the correct form field', () => {
882 mockLib.updateQuotaThroughModal('max_bytes', 512);
883 assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 2048, 1024);
886 it('shows the right texts', () => {
887 assert.quotaUpdateModalTexts(
888 `Update CephFS size quota for '/a/c/b'`,
889 `The inherited size quota 1 KiB from '/a' is the maximum value to be used.`,
890 `Updated CephFS size quota for '/a/c/b'`
895 describe('action behaviour', () => {
896 it('opens with next maximum as maximum if directory holds the current maximum', () => {
897 mockLib.updateQuotaThroughModal('max_bytes', 512);
898 mockLib.updateQuotaThroughModal('max_bytes', 888);
899 assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 512, 1024);
902 it(`uses 'Set' action instead of 'Update' if the quota is not set (0)`, () => {
903 mockLib.updateQuotaThroughModal('max_bytes', 0);
904 mockLib.updateQuotaThroughModal('max_bytes', 200);
905 assert.quotaUpdateModalTexts(
906 `Set CephFS size quota for '/a/c/b'`,
907 `The inherited size quota 1 KiB from '/a' is the maximum value to be used.`,
908 `Set CephFS size quota for '/a/c/b'`
914 describe('unset modal', () => {
915 describe('max_files', () => {
917 mockLib.updateQuotaThroughModal('max_files', 5); // Sets usable quota
918 mockLib.unsetQuotaThroughModal('max_files');
921 it('should unset max_files correctly', () => {
922 expect(cephfsService.quota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 0 });
923 assert.dirQuotas(2048, 0);
926 it('shows the right texts', () => {
927 assert.quotaUnsetModalTexts(
928 `Unset CephFS files quota for '/a/c/b'`,
929 `Unset files quota 5 from '/a/c/b' in order to inherit files quota 10 from '/a'.`,
930 `Unset CephFS files quota for '/a/c/b'`
935 describe('max_bytes', () => {
937 mockLib.updateQuotaThroughModal('max_bytes', 512); // Sets usable quota
938 mockLib.unsetQuotaThroughModal('max_bytes');
941 it('should unset max_files correctly', () => {
942 expect(cephfsService.quota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 0 });
943 assert.dirQuotas(0, 20);
946 it('shows the right texts', () => {
947 assert.quotaUnsetModalTexts(
948 `Unset CephFS size quota for '/a/c/b'`,
949 `Unset size quota 512 B from '/a/c/b' in order to inherit size quota 1 KiB from '/a'.`,
950 `Unset CephFS size quota for '/a/c/b'`
955 describe('action behaviour', () => {
956 it('uses different Text if no quota is inherited', () => {
957 mockLib.selectNode('/a');
958 mockLib.unsetQuotaThroughModal('max_bytes');
959 assert.quotaUnsetModalTexts(
960 `Unset CephFS size quota for '/a'`,
961 `Unset size quota 1 KiB from '/a' in order to have no quota on the directory.`,
962 `Unset CephFS size quota for '/a'`
966 it('uses different Text if quota is already inherited', () => {
967 mockLib.unsetQuotaThroughModal('max_bytes');
968 assert.quotaUnsetModalTexts(
969 `Unset CephFS size quota for '/a/c/b'`,
970 `Unset size quota 2 KiB from '/a/c/b' which isn't used because of the inheritance ` +
971 `of size quota 1 KiB from '/a'.`,
972 `Unset CephFS size quota for '/a/c/b'`
979 describe('table actions', () => {
980 let actions: CdTableAction[];
982 const empty = (): CdTableSelection => new CdTableSelection();
984 const select = (value: number): CdTableSelection => {
985 const selection = new CdTableSelection();
986 selection.selected = [{ dirValue: value }];
991 actions = component.quota.tableActions;
994 it(`shows 'Set' for empty and not set quotas`, () => {
995 const isSetVisible = actions[0].visible;
996 expect(isSetVisible(empty())).toBe(true);
997 expect(isSetVisible(select(0))).toBe(true);
998 expect(isSetVisible(select(1))).toBe(false);
1001 it(`shows 'Update' for set quotas only`, () => {
1002 const isUpdateVisible = actions[1].visible;
1003 expect(isUpdateVisible(empty())).toBeFalsy();
1004 expect(isUpdateVisible(select(0))).toBe(false);
1005 expect(isUpdateVisible(select(1))).toBe(true);
1008 it(`only enables 'Unset' for set quotas only`, () => {
1009 const isUnsetDisabled = actions[2].disable;
1010 expect(isUnsetDisabled(empty())).toBe(true);
1011 expect(isUnsetDisabled(select(0))).toBe(true);
1012 expect(isUnsetDisabled(select(1))).toBe(false);
1015 // Need to change PermissionHelper to reflect latest changes to table actions component
1016 it.skip('should test all quota table actions permission combinations', () => {
1017 const permissionHelper: PermissionHelper = new PermissionHelper(component.permission, {
1018 single: { dirValue: 0 },
1019 multiple: [{ dirValue: 0 }, {}]
1021 const tableActions = permissionHelper.setPermissionsAndGetActions(
1022 component.quota.tableActions
1025 expect(tableActions).toEqual({
1026 'create,update,delete': {
1027 actions: ['Set', 'Update', 'Unset'],
1028 primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
1031 actions: ['Set', 'Update', 'Unset'],
1032 primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
1036 primary: { multiple: '', executing: '', single: '', no: '' }
1040 primary: { multiple: '', executing: '', single: '', no: '' }
1043 actions: ['Set', 'Update', 'Unset'],
1044 primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
1047 actions: ['Set', 'Update', 'Unset'],
1048 primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
1052 primary: { multiple: '', executing: '', single: '', no: '' }
1056 primary: { multiple: '', executing: '', single: '', no: '' }
1062 describe('reload all', () => {
1063 const calledPaths = ['/', '/a', '/a/c', '/a/c/a', '/a/c/a/b'];
1065 const dirsByPath = (): string[] => get.dirs().map((d) => d.path);
1068 mockLib.changeId(1);
1069 mockLib.selectNode('/a');
1070 mockLib.selectNode('/a/c');
1071 mockLib.selectNode('/a/c/a');
1072 mockLib.selectNode('/a/c/a/b');
1075 it('should reload all requested paths', () => {
1076 assert.lsDirHasBeenCalledWith(1, calledPaths);
1077 lsDirSpy.calls.reset();
1078 assert.lsDirHasBeenCalledWith(1, []);
1079 // component.refreshAllDirectories();
1080 // assert.lsDirHasBeenCalledWith(1, calledPaths);
1083 it('should reload all requested paths if not selected anything', () => {
1084 lsDirSpy.calls.reset();
1085 mockLib.changeId(2);
1086 assert.lsDirHasBeenCalledWith(2, ['/']);
1087 lsDirSpy.calls.reset();
1088 component.refreshAllDirectories();
1089 lsDirSpy.calls.reset();
1090 mockLib.changeId(2);
1091 assert.lsDirHasBeenCalledWith(2, ['/']);
1094 it('should add new directories', () => {
1095 // Create two new directories in preparation
1096 const dirsBeforeRefresh = dirsByPath();
1097 expect(dirsBeforeRefresh.includes('/a/c/has_dir_now')).toBe(false);
1098 mockLib.mkDir('/a/c', 'has_dir_now', 0, 0);
1099 mockLib.mkDir('/a/c/a/b', 'has_dir_now_too', 0, 0);
1100 // Now the new directories will be fetched
1101 component.refreshAllDirectories();
1102 const dirsAfterRefresh = dirsByPath();
1103 expect(dirsAfterRefresh.length - dirsBeforeRefresh.length).toBe(2);
1104 expect(dirsAfterRefresh.includes('/a/c/has_dir_now')).toBe(true);
1105 expect(dirsAfterRefresh.includes('/a/c/a/b/has_dir_now_too')).toBe(true);
1108 it('should remove deleted directories', () => {
1109 // Create one new directory and refresh in order to have it added to the directories list
1110 mockLib.mkDir('/a/c', 'will_be_removed_shortly', 0, 0);
1111 component.refreshAllDirectories();
1112 const dirsBeforeRefresh = dirsByPath();
1113 expect(dirsBeforeRefresh.includes('/a/c/will_be_removed_shortly')).toBe(true);
1114 mockData.createdDirs = []; // Mocks the deletion of the directory
1115 // Now the deleted directory will be missing on refresh
1116 component.refreshAllDirectories();
1117 const dirsAfterRefresh = dirsByPath();
1118 expect(dirsAfterRefresh.length - dirsBeforeRefresh.length).toBe(-1);
1119 expect(dirsAfterRefresh.includes('/a/c/will_be_removed_shortly')).toBe(false);
1122 describe('loading indicator', () => {
1124 noAsyncUpdate = true;
1127 it('should have set loading indicator to false after refreshing all dirs', fakeAsync(() => {
1128 component.refreshAllDirectories();
1129 expect(component.loadingIndicator).toBe(true);
1130 tick(3000); // To resolve all promises
1131 expect(component.loadingIndicator).toBe(false);
1134 it('should have set all loaded dirs as attribute names of "indicators"', () => {
1135 noAsyncUpdate = false;
1136 component.refreshAllDirectories();
1137 expect(Object.keys(component.loading).sort()).toEqual(calledPaths);
1140 it('should set an indicator to true during load', () => {
1141 lsDirSpy.and.callFake(() => new Observable((): null => null));
1142 component.refreshAllDirectories();
1144 Object.keys(component.loading)
1145 .filter((x) => x !== '/')
1146 .every((key) => component.loading[key])
1150 describe('disable create snapshot', () => {
1151 let actions: CdTableAction[];
1153 actions = component.snapshot.tableActions;
1154 mockLib.mkDir('/', 'volumes', 2, 2);
1155 mockLib.mkDir('/volumes', 'group1', 2, 2);
1156 mockLib.mkDir('/volumes/group1', 'subvol', 2, 2);
1157 mockLib.mkDir('/volumes/group1/subvol', 'subfile', 2, 2);
1160 const empty = (): CdTableSelection => new CdTableSelection();
1162 it('should return a descriptive message to explain why it is disabled', () => {
1163 const path = '/volumes/group1/subvol/subfile';
1164 const res = 'Cannot create snapshots for files/folders in the subvolume subvol';
1165 mockLib.selectNode(path);
1166 expect(actions[0].disable(empty())).toContain(res);
1169 it('should return false if it is not a subvolume node', () => {
1171 '/volumes/group1/subvol',
1178 testCases.forEach((testCase) => {
1179 mockLib.selectNode(testCase);
1180 expect(actions[0].disable(empty())).toBeFalsy();
1186 describe('tree node helper methods', () => {
1187 describe('getParent', () => {
1188 it('should return the parent node for a given path', () => {
1189 const dirs: CephfsDir[] = [
1190 mockLib.dir('/', 'parent', 2),
1191 mockLib.dir('/parent', 'some', 2)
1194 const parentNode = component.getParent(dirs, '/parent');
1196 expect(parentNode).not.toBeNull();
1197 expect(parentNode?.id).toEqual('/parent');
1198 expect(parentNode?.label).toEqual('parent');
1199 expect(parentNode?.value?.parent).toEqual('/');
1202 it('should return null if no parent node is found', () => {
1203 const dirs: CephfsDir[] = [mockLib.dir('/', 'no parent', 2)];
1205 const parentNode = component.getParent(dirs, '/some/other/path');
1207 expect(parentNode).toBeNull();
1210 it('should handle an empty dirs array', () => {
1211 const dirs: CephfsDir[] = [];
1213 const parentNode = component.getParent(dirs, '/some/path');
1215 expect(parentNode).toBeNull();
1219 describe('toNode', () => {
1220 it('should convert a CephfsDir to a Node', () => {
1221 const directory: CephfsDir = mockLib.dir('/some/parent', '/some/path', 2);
1223 const node: Node = component.toNode(directory);
1225 expect(node.id).toEqual(directory.path);
1226 expect(node.label).toEqual(directory.name);
1227 expect(node.children).toEqual([]);
1228 expect(node.expanded).toBe(false);
1229 expect(node.value).toEqual({ parent: directory.parent });
1232 it('should handle a CephfsDir with no parent', () => {
1233 const directory: CephfsDir = mockLib.dir(undefined, '/some/path', 2);
1235 const node: Node = component.toNode(directory);
1237 expect(node.value).toEqual({ parent: undefined });