summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.spec.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.spec.ts')
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.spec.ts1111
1 files changed, 1111 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.spec.ts
new file mode 100644
index 000000000..3a43ac5c7
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-directories/cephfs-directories.component.spec.ts
@@ -0,0 +1,1111 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { Type } from '@angular/core';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { Validators } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { TreeComponent, TreeModule, TREE_ACTIONS } from '@circlon/angular-tree-component';
+import { NgbActiveModal, NgbModalModule, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { Observable, of } from 'rxjs';
+
+import { CephfsService } from '~/app/shared/api/cephfs.service';
+import { ConfirmationModalComponent } from '~/app/shared/components/confirmation-modal/confirmation-modal.component';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { FormModalComponent } from '~/app/shared/components/form-modal/form-modal.component';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import {
+ CephfsDir,
+ CephfsQuotas,
+ CephfsSnapshot
+} from '~/app/shared/models/cephfs-directory-models';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { NotificationService } from '~/app/shared/services/notification.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, modalServiceShow, PermissionHelper } from '~/testing/unit-test-helper';
+import { CephfsDirectoriesComponent } from './cephfs-directories.component';
+
+describe('CephfsDirectoriesComponent', () => {
+ let component: CephfsDirectoriesComponent;
+ let fixture: ComponentFixture<CephfsDirectoriesComponent>;
+ let cephfsService: CephfsService;
+ let noAsyncUpdate: boolean;
+ let lsDirSpy: jasmine.Spy;
+ let modalShowSpy: jasmine.Spy;
+ let notificationShowSpy: jasmine.Spy;
+ let minValidator: jasmine.Spy;
+ let maxValidator: jasmine.Spy;
+ let minBinaryValidator: jasmine.Spy;
+ let maxBinaryValidator: jasmine.Spy;
+ let modal: NgbModalRef;
+
+ // Get's private attributes or functions
+ const get = {
+ nodeIds: (): { [path: string]: CephfsDir } => component['nodeIds'],
+ dirs: (): CephfsDir[] => component['dirs'],
+ requestedPaths: (): string[] => component['requestedPaths']
+ };
+
+ // Object contains mock data that will be reset before each test.
+ let mockData: {
+ nodes: any;
+ parent: any;
+ createdSnaps: CephfsSnapshot[] | any[];
+ deletedSnaps: CephfsSnapshot[] | any[];
+ updatedQuotas: { [path: string]: CephfsQuotas };
+ createdDirs: CephfsDir[];
+ };
+
+ // Object contains mock functions
+ const mockLib = {
+ quotas: (max_bytes: number, max_files: number): CephfsQuotas => ({ max_bytes, max_files }),
+ snapshots: (dirPath: string, howMany: number): CephfsSnapshot[] => {
+ const name = 'someSnapshot';
+ const snapshots = [];
+ const oneDay = 3600 * 24 * 1000;
+ for (let i = 0; i < howMany; i++) {
+ const snapName = `${name}${i + 1}`;
+ const path = `${dirPath}/.snap/${snapName}`;
+ const created = new Date(+new Date() - oneDay * i).toString();
+ snapshots.push({ name: snapName, path, created });
+ }
+ return snapshots;
+ },
+ dir: (parentPath: string, name: string, modifier: number): CephfsDir => {
+ const dirPath = `${parentPath === '/' ? '' : parentPath}/${name}`;
+ let snapshots = mockLib.snapshots(parentPath, modifier);
+ const extraSnapshots = mockData.createdSnaps.filter((s) => s.path === dirPath);
+ if (extraSnapshots.length > 0) {
+ snapshots = snapshots.concat(extraSnapshots);
+ }
+ const deletedSnapshots = mockData.deletedSnaps
+ .filter((s) => s.path === dirPath)
+ .map((s) => s.name);
+ if (deletedSnapshots.length > 0) {
+ snapshots = snapshots.filter((s) => !deletedSnapshots.includes(s.name));
+ }
+ return {
+ name,
+ path: dirPath,
+ parent: parentPath,
+ quotas: Object.assign(
+ mockLib.quotas(1024 * modifier, 10 * modifier),
+ mockData.updatedQuotas[dirPath] || {}
+ ),
+ snapshots: snapshots
+ };
+ },
+ // Only used inside other mocks
+ lsSingleDir: (path = ''): CephfsDir[] => {
+ const customDirs = mockData.createdDirs.filter((d) => d.parent === path);
+ const isCustomDir = mockData.createdDirs.some((d) => d.path === path);
+ if (isCustomDir || path.includes('b')) {
+ // 'b' has no sub directories
+ return customDirs;
+ }
+ return customDirs.concat([
+ // Directories are not sorted!
+ mockLib.dir(path, 'c', 3),
+ mockLib.dir(path, 'a', 1),
+ mockLib.dir(path, 'b', 2)
+ ]);
+ },
+ lsDir: (_id: number, path = ''): Observable<CephfsDir[]> => {
+ // will return 2 levels deep
+ let data = mockLib.lsSingleDir(path);
+ const paths = data.map((dir) => dir.path);
+ paths.forEach((pathL2) => {
+ data = data.concat(mockLib.lsSingleDir(pathL2));
+ });
+ if (path === '' || path === '/') {
+ // Adds root directory on ls of '/' to the directories list.
+ const root = mockLib.dir(path, '/', 1);
+ root.path = '/';
+ root.parent = undefined;
+ root.quotas = undefined;
+ data = [root].concat(data);
+ }
+ return of(data);
+ },
+ mkSnapshot: (_id: any, path: string, name: string): Observable<string> => {
+ mockData.createdSnaps.push({
+ name,
+ path,
+ created: new Date().toString()
+ });
+ return of(name);
+ },
+ rmSnapshot: (_id: any, path: string, name: string): Observable<string> => {
+ mockData.deletedSnaps.push({
+ name,
+ path,
+ created: new Date().toString()
+ });
+ return of(name);
+ },
+ updateQuota: (_id: any, path: string, updated: CephfsQuotas): Observable<string> => {
+ mockData.updatedQuotas[path] = Object.assign(mockData.updatedQuotas[path] || {}, updated);
+ return of('Response');
+ },
+ modalShow: (comp: Type<any>, init: any): any => {
+ modal = modalServiceShow(comp, init);
+ return modal;
+ },
+ getNodeById: (path: string) => {
+ return mockLib.useNode(path);
+ },
+ updateNodes: (path: string) => {
+ const p: Promise<any[]> = component.treeOptions.getChildren({ id: path });
+ return noAsyncUpdate ? () => p : mockLib.asyncNodeUpdate(p);
+ },
+ asyncNodeUpdate: fakeAsync((p: Promise<any[]>) => {
+ p.then((nodes) => {
+ mockData.nodes = mockData.nodes.concat(nodes);
+ });
+ tick();
+ }),
+ changeId: (id: number) => {
+ // For some reason this spy has to be renewed after usage
+ spyOn(global, 'setTimeout').and.callFake((fn) => fn());
+ component.id = id;
+ component.ngOnChanges();
+ mockData.nodes = component.nodes.concat(mockData.nodes);
+ },
+ selectNode: (path: string) => {
+ component.treeOptions.actionMapping.mouse.click(undefined, mockLib.useNode(path), undefined);
+ },
+ // Creates TreeNode with parents until root
+ useNode: (path: string): { id: string; parent: any; data: any; loadNodeChildren: Function } => {
+ const parentPath = path.split('/');
+ parentPath.pop();
+ const parentIsRoot = parentPath.length === 1;
+ const parent = parentIsRoot ? { id: '/' } : mockLib.useNode(parentPath.join('/'));
+ return {
+ id: path,
+ parent,
+ data: {},
+ loadNodeChildren: () => mockLib.updateNodes(path)
+ };
+ },
+ treeActions: {
+ toggleActive: (_a: any, node: any, _b: any) => {
+ return mockLib.updateNodes(node.id);
+ }
+ },
+ mkDir: (path: string, name: string, maxFiles: number, maxBytes: number) => {
+ const dir = mockLib.dir(path, name, 3);
+ dir.quotas.max_bytes = maxBytes * 1024;
+ dir.quotas.max_files = maxFiles;
+ mockData.createdDirs.push(dir);
+ // Below is needed for quota tests only where 4 dirs are mocked
+ get.nodeIds()[dir.path] = dir;
+ mockData.nodes.push({ id: dir.path });
+ },
+ createSnapshotThroughModal: (name: string) => {
+ component.createSnapshot();
+ modal.componentInstance.onSubmitForm({ name });
+ },
+ deleteSnapshotsThroughModal: (snapshots: CephfsSnapshot[]) => {
+ component.snapshot.selection.selected = snapshots;
+ component.deleteSnapshotModal();
+ modal.componentInstance.callSubmitAction();
+ },
+ updateQuotaThroughModal: (attribute: string, value: number) => {
+ component.quota.selection.selected = component.settings.filter(
+ (q) => q.quotaKey === attribute
+ );
+ component.updateQuotaModal();
+ modal.componentInstance.onSubmitForm({ [attribute]: value });
+ },
+ unsetQuotaThroughModal: (attribute: string) => {
+ component.quota.selection.selected = component.settings.filter(
+ (q) => q.quotaKey === attribute
+ );
+ component.unsetQuotaModal();
+ modal.componentInstance.onSubmit();
+ },
+ setFourQuotaDirs: (quotas: number[][]) => {
+ expect(quotas.length).toBe(4); // Make sure this function is used correctly
+ let path = '';
+ quotas.forEach((quota, index) => {
+ index += 1;
+ mockLib.mkDir(path === '' ? '/' : path, index.toString(), quota[0], quota[1]);
+ path += '/' + index;
+ });
+ mockData.parent = {
+ value: '3',
+ id: '/1/2/3',
+ parent: {
+ value: '2',
+ id: '/1/2',
+ parent: {
+ value: '1',
+ id: '/1',
+ parent: { value: '/', id: '/' }
+ }
+ }
+ };
+ mockLib.selectNode('/1/2/3/4');
+ }
+ };
+
+ // Expects that are used frequently
+ const assert = {
+ dirLength: (n: number) => expect(get.dirs().length).toBe(n),
+ nodeLength: (n: number) => expect(mockData.nodes.length).toBe(n),
+ lsDirCalledTimes: (n: number) => expect(lsDirSpy).toHaveBeenCalledTimes(n),
+ lsDirHasBeenCalledWith: (id: number, paths: string[]) => {
+ paths.forEach((path) => expect(lsDirSpy).toHaveBeenCalledWith(id, path));
+ assert.lsDirCalledTimes(paths.length);
+ },
+ requestedPaths: (expected: string[]) => expect(get.requestedPaths()).toEqual(expected),
+ snapshotsByName: (snaps: string[]) =>
+ expect(component.selectedDir.snapshots.map((s) => s.name)).toEqual(snaps),
+ dirQuotas: (bytes: number, files: number) => {
+ expect(component.selectedDir.quotas).toEqual({ max_bytes: bytes, max_files: files });
+ },
+ noQuota: (key: 'bytes' | 'files') => {
+ assert.quotaRow(key, '', 0, '');
+ },
+ quotaIsNotInherited: (key: 'bytes' | 'files', shownValue: any, nextMaximum: number) => {
+ const dir = component.selectedDir;
+ const path = dir.path;
+ assert.quotaRow(key, shownValue, nextMaximum, path);
+ },
+ quotaIsInherited: (key: 'bytes' | 'files', shownValue: any, path: string) => {
+ const isBytes = key === 'bytes';
+ const nextMaximum = get.nodeIds()[path].quotas[isBytes ? 'max_bytes' : 'max_files'];
+ assert.quotaRow(key, shownValue, nextMaximum, path);
+ },
+ quotaRow: (
+ key: 'bytes' | 'files',
+ shownValue: number | string,
+ nextTreeMaximum: number,
+ originPath: string
+ ) => {
+ const isBytes = key === 'bytes';
+ expect(component.settings[isBytes ? 1 : 0]).toEqual({
+ row: {
+ name: `Max ${isBytes ? 'size' : key}`,
+ value: shownValue,
+ originPath
+ },
+ quotaKey: `max_${key}`,
+ dirValue: expect.any(Number),
+ nextTreeMaximum: {
+ value: nextTreeMaximum,
+ path: expect.any(String)
+ }
+ });
+ },
+ quotaUnsetModalTexts: (titleText: string, message: string, notificationMsg: string) => {
+ expect(modalShowSpy).toHaveBeenCalledWith(
+ ConfirmationModalComponent,
+ expect.objectContaining({
+ titleText,
+ description: message,
+ buttonText: 'Unset'
+ })
+ );
+ expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
+ },
+ quotaUpdateModalTexts: (titleText: string, message: string, notificationMsg: string) => {
+ expect(modalShowSpy).toHaveBeenCalledWith(
+ FormModalComponent,
+ expect.objectContaining({
+ titleText,
+ message,
+ submitButtonText: 'Save'
+ })
+ );
+ expect(notificationShowSpy).toHaveBeenCalledWith(NotificationType.success, notificationMsg);
+ },
+ quotaUpdateModalField: (
+ type: string,
+ label: string,
+ key: string,
+ value: number,
+ max: number,
+ errors?: { [key: string]: string }
+ ) => {
+ expect(modalShowSpy).toHaveBeenCalledWith(
+ FormModalComponent,
+ expect.objectContaining({
+ fields: [
+ {
+ type,
+ label,
+ errors,
+ name: key,
+ value,
+ validators: expect.anything(),
+ required: true
+ }
+ ]
+ })
+ );
+ if (type === 'binary') {
+ expect(minBinaryValidator).toHaveBeenCalledWith(0);
+ expect(maxBinaryValidator).toHaveBeenCalledWith(max);
+ } else {
+ expect(minValidator).toHaveBeenCalledWith(0);
+ expect(maxValidator).toHaveBeenCalledWith(max);
+ }
+ }
+ };
+
+ configureTestBed(
+ {
+ imports: [
+ HttpClientTestingModule,
+ SharedModule,
+ RouterTestingModule,
+ TreeModule,
+ ToastrModule.forRoot(),
+ NgbModalModule
+ ],
+ declarations: [CephfsDirectoriesComponent],
+ providers: [NgbActiveModal]
+ },
+ [CriticalConfirmationModalComponent, FormModalComponent, ConfirmationModalComponent]
+ );
+
+ beforeEach(() => {
+ noAsyncUpdate = false;
+ mockData = {
+ nodes: [],
+ parent: undefined,
+ createdSnaps: [],
+ deletedSnaps: [],
+ createdDirs: [],
+ updatedQuotas: {}
+ };
+
+ cephfsService = TestBed.inject(CephfsService);
+ lsDirSpy = spyOn(cephfsService, 'lsDir').and.callFake(mockLib.lsDir);
+ spyOn(cephfsService, 'mkSnapshot').and.callFake(mockLib.mkSnapshot);
+ spyOn(cephfsService, 'rmSnapshot').and.callFake(mockLib.rmSnapshot);
+ spyOn(cephfsService, 'quota').and.callFake(mockLib.updateQuota);
+
+ modalShowSpy = spyOn(TestBed.inject(ModalService), 'show').and.callFake(mockLib.modalShow);
+ notificationShowSpy = spyOn(TestBed.inject(NotificationService), 'show').and.stub();
+
+ fixture = TestBed.createComponent(CephfsDirectoriesComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+
+ spyOn(TREE_ACTIONS, 'TOGGLE_ACTIVE').and.callFake(mockLib.treeActions.toggleActive);
+
+ component.treeComponent = {
+ sizeChanged: () => null,
+ treeModel: { getNodeById: mockLib.getNodeById, update: () => null }
+ } as TreeComponent;
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('mock self test', () => {
+ it('tests snapshots mock', () => {
+ expect(mockLib.snapshots('/a', 1).map((s) => ({ name: s.name, path: s.path }))).toEqual([
+ {
+ name: 'someSnapshot1',
+ path: '/a/.snap/someSnapshot1'
+ }
+ ]);
+ expect(mockLib.snapshots('/a/b', 3).map((s) => ({ name: s.name, path: s.path }))).toEqual([
+ {
+ name: 'someSnapshot1',
+ path: '/a/b/.snap/someSnapshot1'
+ },
+ {
+ name: 'someSnapshot2',
+ path: '/a/b/.snap/someSnapshot2'
+ },
+ {
+ name: 'someSnapshot3',
+ path: '/a/b/.snap/someSnapshot3'
+ }
+ ]);
+ });
+
+ it('tests dir mock', () => {
+ const path = '/a/b/c';
+ mockData.createdSnaps = [
+ { path, name: 's1' },
+ { path, name: 's2' }
+ ];
+ mockData.deletedSnaps = [
+ { path, name: 'someSnapshot2' },
+ { path, name: 's2' }
+ ];
+ const dir = mockLib.dir('/a/b', 'c', 2);
+ expect(dir.path).toBe('/a/b/c');
+ expect(dir.parent).toBe('/a/b');
+ expect(dir.quotas).toEqual({ max_bytes: 2048, max_files: 20 });
+ expect(dir.snapshots.map((s) => s.name)).toEqual(['someSnapshot1', 's1']);
+ });
+
+ it('tests lsdir mock', () => {
+ let dirs: CephfsDir[] = [];
+ mockLib.lsDir(2, '/a').subscribe((x) => (dirs = x));
+ expect(dirs.map((d) => d.path)).toEqual([
+ '/a/c',
+ '/a/a',
+ '/a/b',
+ '/a/c/c',
+ '/a/c/a',
+ '/a/c/b',
+ '/a/a/c',
+ '/a/a/a',
+ '/a/a/b'
+ ]);
+ });
+
+ describe('test quota update mock', () => {
+ const PATH = '/a';
+ const ID = 2;
+
+ const updateQuota = (quotas: CephfsQuotas) => mockLib.updateQuota(ID, PATH, quotas);
+
+ const expectMockUpdate = (max_bytes?: number, max_files?: number) =>
+ expect(mockData.updatedQuotas[PATH]).toEqual({
+ max_bytes,
+ max_files
+ });
+
+ const expectLsUpdate = (max_bytes?: number, max_files?: number) => {
+ let dir: CephfsDir;
+ mockLib.lsDir(ID, '/').subscribe((dirs) => (dir = dirs.find((d) => d.path === PATH)));
+ expect(dir.quotas).toEqual({
+ max_bytes,
+ max_files
+ });
+ };
+
+ it('tests to set quotas', () => {
+ expectLsUpdate(1024, 10);
+
+ updateQuota({ max_bytes: 512 });
+ expectMockUpdate(512);
+ expectLsUpdate(512, 10);
+
+ updateQuota({ max_files: 100 });
+ expectMockUpdate(512, 100);
+ expectLsUpdate(512, 100);
+ });
+
+ it('tests to unset quotas', () => {
+ updateQuota({ max_files: 0 });
+ expectMockUpdate(undefined, 0);
+ expectLsUpdate(1024, 0);
+
+ updateQuota({ max_bytes: 0 });
+ expectMockUpdate(0, 0);
+ expectLsUpdate(0, 0);
+ });
+ });
+ });
+
+ it('calls lsDir only if an id exits', () => {
+ assert.lsDirCalledTimes(0);
+
+ mockLib.changeId(1);
+ assert.lsDirCalledTimes(1);
+ expect(lsDirSpy).toHaveBeenCalledWith(1, '/');
+
+ mockLib.changeId(2);
+ assert.lsDirCalledTimes(2);
+ expect(lsDirSpy).toHaveBeenCalledWith(2, '/');
+ });
+
+ describe('listing sub directories', () => {
+ beforeEach(() => {
+ mockLib.changeId(1);
+ /**
+ * Tree looks like this:
+ * v /
+ * > a
+ * * b
+ * > c
+ * */
+ });
+
+ it('expands first level', () => {
+ // Tree will only show '*' if nor 'loadChildren' or 'children' are defined
+ expect(
+ mockData.nodes.map((node: any) => ({
+ [node.id]: node.hasChildren || node.isExpanded || Boolean(node.children)
+ }))
+ ).toEqual([{ '/': true }, { '/a': true }, { '/b': false }, { '/c': true }]);
+ });
+
+ it('resets all dynamic content on id change', () => {
+ mockLib.selectNode('/a');
+ /**
+ * Tree looks like this:
+ * v /
+ * v a <- Selected
+ * > a
+ * * b
+ * > c
+ * * b
+ * > c
+ * */
+ assert.requestedPaths(['/', '/a']);
+ assert.nodeLength(7);
+ assert.dirLength(16);
+ expect(component.selectedDir).toBeDefined();
+
+ mockLib.changeId(undefined);
+ assert.dirLength(0);
+ assert.requestedPaths([]);
+ expect(component.selectedDir).not.toBeDefined();
+ });
+
+ it('should select a node and show the directory contents', () => {
+ mockLib.selectNode('/a');
+ const dir = get.dirs().find((d) => d.path === '/a');
+ expect(component.selectedDir).toEqual(dir);
+ assert.quotaIsNotInherited('files', 10, 0);
+ assert.quotaIsNotInherited('bytes', '1 KiB', 0);
+ });
+
+ it('should extend the list by subdirectories when expanding', () => {
+ mockLib.selectNode('/a');
+ mockLib.selectNode('/a/c');
+ /**
+ * Tree looks like this:
+ * v /
+ * v a
+ * > a
+ * * b
+ * v c <- Selected
+ * > a
+ * * b
+ * > c
+ * * b
+ * > c
+ * */
+ assert.lsDirCalledTimes(3);
+ assert.requestedPaths(['/', '/a', '/a/c']);
+ assert.dirLength(22);
+ assert.nodeLength(10);
+ });
+
+ it('should update the tree after each selection', () => {
+ const spy = spyOn(component.treeComponent, 'sizeChanged').and.callThrough();
+ expect(spy).toHaveBeenCalledTimes(0);
+ mockLib.selectNode('/a');
+ expect(spy).toHaveBeenCalledTimes(1);
+ mockLib.selectNode('/a/c');
+ expect(spy).toHaveBeenCalledTimes(2);
+ });
+
+ it('should select parent by path', () => {
+ mockLib.selectNode('/a');
+ mockLib.selectNode('/a/c');
+ mockLib.selectNode('/a/c/a');
+ component.selectOrigin('/a');
+ expect(component.selectedDir.path).toBe('/a');
+ });
+
+ it('should refresh directories with no sub directories as they could have some now', () => {
+ mockLib.selectNode('/b');
+ /**
+ * Tree looks like this:
+ * v /
+ * > a
+ * * b <- Selected
+ * > c
+ * */
+ assert.lsDirCalledTimes(2);
+ assert.requestedPaths(['/', '/b']);
+ assert.nodeLength(4);
+ });
+
+ describe('used quotas', () => {
+ it('should use no quota if none is set', () => {
+ mockLib.setFourQuotaDirs([
+ [0, 0],
+ [0, 0],
+ [0, 0],
+ [0, 0]
+ ]);
+ assert.noQuota('files');
+ assert.noQuota('bytes');
+ assert.dirQuotas(0, 0);
+ });
+
+ it('should use quota from upper parents', () => {
+ mockLib.setFourQuotaDirs([
+ [100, 0],
+ [0, 8],
+ [0, 0],
+ [0, 0]
+ ]);
+ assert.quotaIsInherited('files', 100, '/1');
+ assert.quotaIsInherited('bytes', '8 KiB', '/1/2');
+ assert.dirQuotas(0, 0);
+ });
+
+ it('should use quota from the parent with the lowest value (deep inheritance)', () => {
+ mockLib.setFourQuotaDirs([
+ [200, 1],
+ [100, 4],
+ [400, 3],
+ [300, 2]
+ ]);
+ assert.quotaIsInherited('files', 100, '/1/2');
+ assert.quotaIsInherited('bytes', '1 KiB', '/1');
+ assert.dirQuotas(2048, 300);
+ });
+
+ it('should use current value', () => {
+ mockLib.setFourQuotaDirs([
+ [200, 2],
+ [300, 4],
+ [400, 3],
+ [100, 1]
+ ]);
+ assert.quotaIsNotInherited('files', 100, 200);
+ assert.quotaIsNotInherited('bytes', '1 KiB', 2048);
+ assert.dirQuotas(1024, 100);
+ });
+ });
+ });
+
+ describe('snapshots', () => {
+ beforeEach(() => {
+ mockLib.changeId(1);
+ mockLib.selectNode('/a');
+ });
+
+ it('should create a snapshot', () => {
+ mockLib.createSnapshotThroughModal('newSnap');
+ expect(cephfsService.mkSnapshot).toHaveBeenCalledWith(1, '/a', 'newSnap');
+ assert.snapshotsByName(['someSnapshot1', 'newSnap']);
+ });
+
+ it('should delete a snapshot', () => {
+ mockLib.createSnapshotThroughModal('deleteMe');
+ mockLib.deleteSnapshotsThroughModal([component.selectedDir.snapshots[1]]);
+ assert.snapshotsByName(['someSnapshot1']);
+ });
+
+ it('should delete all snapshots', () => {
+ mockLib.createSnapshotThroughModal('deleteAll');
+ mockLib.deleteSnapshotsThroughModal(component.selectedDir.snapshots);
+ assert.snapshotsByName([]);
+ });
+ });
+
+ it('should test all snapshot table actions combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
+ const tableActions = permissionHelper.setPermissionsAndGetActions(
+ component.snapshot.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: ['Create', 'Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
+ },
+ 'create,update': {
+ actions: ['Create'],
+ primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ },
+ 'create,delete': {
+ actions: ['Create', 'Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Create' }
+ },
+ create: {
+ actions: ['Create'],
+ primary: { multiple: 'Create', executing: 'Create', single: 'Create', no: 'Create' }
+ },
+ 'update,delete': {
+ actions: ['Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ },
+ update: {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ },
+ delete: {
+ actions: ['Delete'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+
+ describe('quotas', () => {
+ beforeEach(() => {
+ // Spies
+ minValidator = spyOn(Validators, 'min').and.callThrough();
+ maxValidator = spyOn(Validators, 'max').and.callThrough();
+ minBinaryValidator = spyOn(CdValidators, 'binaryMin').and.callThrough();
+ maxBinaryValidator = spyOn(CdValidators, 'binaryMax').and.callThrough();
+ // Select /a/c/b
+ mockLib.changeId(1);
+ mockLib.selectNode('/a');
+ mockLib.selectNode('/a/c');
+ mockLib.selectNode('/a/c/b');
+ // Quotas after selection
+ assert.quotaIsInherited('files', 10, '/a');
+ assert.quotaIsInherited('bytes', '1 KiB', '/a');
+ assert.dirQuotas(2048, 20);
+ });
+
+ describe('update modal', () => {
+ describe('max_files', () => {
+ beforeEach(() => {
+ mockLib.updateQuotaThroughModal('max_files', 5);
+ });
+
+ it('should update max_files correctly', () => {
+ expect(cephfsService.quota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 5 });
+ assert.quotaIsNotInherited('files', 5, 10);
+ });
+
+ it('uses the correct form field', () => {
+ assert.quotaUpdateModalField('number', 'Max files', 'max_files', 20, 10, {
+ min: 'Value has to be at least 0 or more',
+ max: 'Value has to be at most 10 or less'
+ });
+ });
+
+ it('shows the right texts', () => {
+ assert.quotaUpdateModalTexts(
+ `Update CephFS files quota for '/a/c/b'`,
+ `The inherited files quota 10 from '/a' is the maximum value to be used.`,
+ `Updated CephFS files quota for '/a/c/b'`
+ );
+ });
+ });
+
+ describe('max_bytes', () => {
+ beforeEach(() => {
+ mockLib.updateQuotaThroughModal('max_bytes', 512);
+ });
+
+ it('should update max_files correctly', () => {
+ expect(cephfsService.quota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 512 });
+ assert.quotaIsNotInherited('bytes', '512 B', 1024);
+ });
+
+ it('uses the correct form field', () => {
+ mockLib.updateQuotaThroughModal('max_bytes', 512);
+ assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 2048, 1024);
+ });
+
+ it('shows the right texts', () => {
+ assert.quotaUpdateModalTexts(
+ `Update CephFS size quota for '/a/c/b'`,
+ `The inherited size quota 1 KiB from '/a' is the maximum value to be used.`,
+ `Updated CephFS size quota for '/a/c/b'`
+ );
+ });
+ });
+
+ describe('action behaviour', () => {
+ it('opens with next maximum as maximum if directory holds the current maximum', () => {
+ mockLib.updateQuotaThroughModal('max_bytes', 512);
+ mockLib.updateQuotaThroughModal('max_bytes', 888);
+ assert.quotaUpdateModalField('binary', 'Max size', 'max_bytes', 512, 1024);
+ });
+
+ it(`uses 'Set' action instead of 'Update' if the quota is not set (0)`, () => {
+ mockLib.updateQuotaThroughModal('max_bytes', 0);
+ mockLib.updateQuotaThroughModal('max_bytes', 200);
+ assert.quotaUpdateModalTexts(
+ `Set CephFS size quota for '/a/c/b'`,
+ `The inherited size quota 1 KiB from '/a' is the maximum value to be used.`,
+ `Set CephFS size quota for '/a/c/b'`
+ );
+ });
+ });
+ });
+
+ describe('unset modal', () => {
+ describe('max_files', () => {
+ beforeEach(() => {
+ mockLib.updateQuotaThroughModal('max_files', 5); // Sets usable quota
+ mockLib.unsetQuotaThroughModal('max_files');
+ });
+
+ it('should unset max_files correctly', () => {
+ expect(cephfsService.quota).toHaveBeenCalledWith(1, '/a/c/b', { max_files: 0 });
+ assert.dirQuotas(2048, 0);
+ });
+
+ it('shows the right texts', () => {
+ assert.quotaUnsetModalTexts(
+ `Unset CephFS files quota for '/a/c/b'`,
+ `Unset files quota 5 from '/a/c/b' in order to inherit files quota 10 from '/a'.`,
+ `Unset CephFS files quota for '/a/c/b'`
+ );
+ });
+ });
+
+ describe('max_bytes', () => {
+ beforeEach(() => {
+ mockLib.updateQuotaThroughModal('max_bytes', 512); // Sets usable quota
+ mockLib.unsetQuotaThroughModal('max_bytes');
+ });
+
+ it('should unset max_files correctly', () => {
+ expect(cephfsService.quota).toHaveBeenCalledWith(1, '/a/c/b', { max_bytes: 0 });
+ assert.dirQuotas(0, 20);
+ });
+
+ it('shows the right texts', () => {
+ assert.quotaUnsetModalTexts(
+ `Unset CephFS size quota for '/a/c/b'`,
+ `Unset size quota 512 B from '/a/c/b' in order to inherit size quota 1 KiB from '/a'.`,
+ `Unset CephFS size quota for '/a/c/b'`
+ );
+ });
+ });
+
+ describe('action behaviour', () => {
+ it('uses different Text if no quota is inherited', () => {
+ mockLib.selectNode('/a');
+ mockLib.unsetQuotaThroughModal('max_bytes');
+ assert.quotaUnsetModalTexts(
+ `Unset CephFS size quota for '/a'`,
+ `Unset size quota 1 KiB from '/a' in order to have no quota on the directory.`,
+ `Unset CephFS size quota for '/a'`
+ );
+ });
+
+ it('uses different Text if quota is already inherited', () => {
+ mockLib.unsetQuotaThroughModal('max_bytes');
+ assert.quotaUnsetModalTexts(
+ `Unset CephFS size quota for '/a/c/b'`,
+ `Unset size quota 2 KiB from '/a/c/b' which isn't used because of the inheritance ` +
+ `of size quota 1 KiB from '/a'.`,
+ `Unset CephFS size quota for '/a/c/b'`
+ );
+ });
+ });
+ });
+ });
+
+ describe('table actions', () => {
+ let actions: CdTableAction[];
+
+ const empty = (): CdTableSelection => new CdTableSelection();
+
+ const select = (value: number): CdTableSelection => {
+ const selection = new CdTableSelection();
+ selection.selected = [{ dirValue: value }];
+ return selection;
+ };
+
+ beforeEach(() => {
+ actions = component.quota.tableActions;
+ });
+
+ it(`shows 'Set' for empty and not set quotas`, () => {
+ const isSetVisible = actions[0].visible;
+ expect(isSetVisible(empty())).toBe(true);
+ expect(isSetVisible(select(0))).toBe(true);
+ expect(isSetVisible(select(1))).toBe(false);
+ });
+
+ it(`shows 'Update' for set quotas only`, () => {
+ const isUpdateVisible = actions[1].visible;
+ expect(isUpdateVisible(empty())).toBeFalsy();
+ expect(isUpdateVisible(select(0))).toBe(false);
+ expect(isUpdateVisible(select(1))).toBe(true);
+ });
+
+ it(`only enables 'Unset' for set quotas only`, () => {
+ const isUnsetDisabled = actions[2].disable;
+ expect(isUnsetDisabled(empty())).toBe(true);
+ expect(isUnsetDisabled(select(0))).toBe(true);
+ expect(isUnsetDisabled(select(1))).toBe(false);
+ });
+
+ it('should test all quota table actions permission combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission, {
+ single: { dirValue: 0 },
+ multiple: [{ dirValue: 0 }, {}]
+ });
+ const tableActions = permissionHelper.setPermissionsAndGetActions(
+ component.quota.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: ['Set', 'Update', 'Unset'],
+ primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
+ },
+ 'create,update': {
+ actions: ['Set', 'Update', 'Unset'],
+ primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
+ },
+ 'create,delete': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ },
+ create: {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ },
+ 'update,delete': {
+ actions: ['Set', 'Update', 'Unset'],
+ primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
+ },
+ update: {
+ actions: ['Set', 'Update', 'Unset'],
+ primary: { multiple: 'Set', executing: 'Set', single: 'Set', no: 'Set' }
+ },
+ delete: {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+ });
+
+ describe('reload all', () => {
+ const calledPaths = ['/', '/a', '/a/c', '/a/c/a', '/a/c/a/b'];
+
+ const dirsByPath = (): string[] => get.dirs().map((d) => d.path);
+
+ beforeEach(() => {
+ mockLib.changeId(1);
+ mockLib.selectNode('/a');
+ mockLib.selectNode('/a/c');
+ mockLib.selectNode('/a/c/a');
+ mockLib.selectNode('/a/c/a/b');
+ });
+
+ it('should reload all requested paths', () => {
+ assert.lsDirHasBeenCalledWith(1, calledPaths);
+ lsDirSpy.calls.reset();
+ assert.lsDirHasBeenCalledWith(1, []);
+ component.refreshAllDirectories();
+ assert.lsDirHasBeenCalledWith(1, calledPaths);
+ });
+
+ it('should reload all requested paths if not selected anything', () => {
+ lsDirSpy.calls.reset();
+ mockLib.changeId(2);
+ assert.lsDirHasBeenCalledWith(2, ['/']);
+ lsDirSpy.calls.reset();
+ component.refreshAllDirectories();
+ assert.lsDirHasBeenCalledWith(2, ['/']);
+ });
+
+ it('should add new directories', () => {
+ // Create two new directories in preparation
+ const dirsBeforeRefresh = dirsByPath();
+ expect(dirsBeforeRefresh.includes('/a/c/has_dir_now')).toBe(false);
+ mockLib.mkDir('/a/c', 'has_dir_now', 0, 0);
+ mockLib.mkDir('/a/c/a/b', 'has_dir_now_too', 0, 0);
+ // Now the new directories will be fetched
+ component.refreshAllDirectories();
+ const dirsAfterRefresh = dirsByPath();
+ expect(dirsAfterRefresh.length - dirsBeforeRefresh.length).toBe(2);
+ expect(dirsAfterRefresh.includes('/a/c/has_dir_now')).toBe(true);
+ expect(dirsAfterRefresh.includes('/a/c/a/b/has_dir_now_too')).toBe(true);
+ });
+
+ it('should remove deleted directories', () => {
+ // Create one new directory and refresh in order to have it added to the directories list
+ mockLib.mkDir('/a/c', 'will_be_removed_shortly', 0, 0);
+ component.refreshAllDirectories();
+ const dirsBeforeRefresh = dirsByPath();
+ expect(dirsBeforeRefresh.includes('/a/c/will_be_removed_shortly')).toBe(true);
+ mockData.createdDirs = []; // Mocks the deletion of the directory
+ // Now the deleted directory will be missing on refresh
+ component.refreshAllDirectories();
+ const dirsAfterRefresh = dirsByPath();
+ expect(dirsAfterRefresh.length - dirsBeforeRefresh.length).toBe(-1);
+ expect(dirsAfterRefresh.includes('/a/c/will_be_removed_shortly')).toBe(false);
+ });
+
+ describe('loading indicator', () => {
+ beforeEach(() => {
+ noAsyncUpdate = true;
+ });
+
+ it('should have set loading indicator to false after refreshing all dirs', fakeAsync(() => {
+ component.refreshAllDirectories();
+ expect(component.loadingIndicator).toBe(true);
+ tick(3000); // To resolve all promises
+ expect(component.loadingIndicator).toBe(false);
+ }));
+
+ it('should only update the tree once and not on every call', fakeAsync(() => {
+ const spy = spyOn(component.treeComponent, 'sizeChanged').and.callThrough();
+ component.refreshAllDirectories();
+ expect(spy).toHaveBeenCalledTimes(0);
+ tick(3000); // To resolve all promises
+ // Called during the interval and at the end of timeout
+ expect(spy).toHaveBeenCalledTimes(2);
+ }));
+
+ it('should have set all loaded dirs as attribute names of "indicators"', () => {
+ noAsyncUpdate = false;
+ component.refreshAllDirectories();
+ expect(Object.keys(component.loading).sort()).toEqual(calledPaths);
+ });
+
+ it('should set an indicator to true during load', () => {
+ lsDirSpy.and.callFake(() => new Observable((): null => null));
+ component.refreshAllDirectories();
+ expect(Object.values(component.loading).every((b) => b)).toBe(true);
+ expect(component.loadingIndicator).toBe(true);
+ });
+ });
+ describe('disable create snapshot', () => {
+ let actions: CdTableAction[];
+ beforeEach(() => {
+ actions = component.snapshot.tableActions;
+ mockLib.mkDir('/', 'volumes', 2, 2);
+ mockLib.mkDir('/volumes', 'group1', 2, 2);
+ mockLib.mkDir('/volumes/group1', 'subvol', 2, 2);
+ mockLib.mkDir('/volumes/group1/subvol', 'subfile', 2, 2);
+ });
+
+ const empty = (): CdTableSelection => new CdTableSelection();
+
+ it('should return a descriptive message to explain why it is disabled', () => {
+ const path = '/volumes/group1/subvol/subfile';
+ const res = 'Cannot create snapshots for files/folders in the subvolume subvol';
+ mockLib.selectNode(path);
+ expect(actions[0].disable(empty())).toContain(res);
+ });
+
+ it('should return false if it is not a subvolume node', () => {
+ const testCases = [
+ '/volumes/group1/subvol',
+ '/volumes/group1',
+ '/volumes',
+ '/',
+ '/a',
+ '/a/b'
+ ];
+ testCases.forEach((testCase) => {
+ mockLib.selectNode(testCase);
+ expect(actions[0].disable(empty())).toBeFalsy();
+ });
+ });
+ });
+ });
+});