summaryrefslogtreecommitdiffstats
path: root/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts')
-rw-r--r--src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts438
1 files changed, 438 insertions, 0 deletions
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts
new file mode 100644
index 000000000..fa7a772f0
--- /dev/null
+++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.spec.ts
@@ -0,0 +1,438 @@
+import { HttpHeaders } from '@angular/common/http';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbNavModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { BehaviorSubject, of } from 'rxjs';
+
+import { RbdService } from '~/app/shared/api/rbd.service';
+import { TableActionsComponent } from '~/app/shared/datatable/table-actions/table-actions.component';
+import { ExecutingTask } from '~/app/shared/models/executing-task';
+import { SummaryService } from '~/app/shared/services/summary.service';
+import { TaskListService } from '~/app/shared/services/task-list.service';
+import { SharedModule } from '~/app/shared/shared.module';
+import { configureTestBed, expectItemTasks, PermissionHelper } from '~/testing/unit-test-helper';
+import { RbdConfigurationListComponent } from '../rbd-configuration-list/rbd-configuration-list.component';
+import { RbdDetailsComponent } from '../rbd-details/rbd-details.component';
+import { RbdSnapshotListComponent } from '../rbd-snapshot-list/rbd-snapshot-list.component';
+import { RbdTabsComponent } from '../rbd-tabs/rbd-tabs.component';
+import { RbdListComponent } from './rbd-list.component';
+import { RbdModel } from './rbd-model';
+
+describe('RbdListComponent', () => {
+ let fixture: ComponentFixture<RbdListComponent>;
+ let component: RbdListComponent;
+ let summaryService: SummaryService;
+ let rbdService: RbdService;
+ let headers: HttpHeaders;
+
+ const refresh = (data: any) => {
+ summaryService['summaryDataSource'].next(data);
+ };
+
+ configureTestBed({
+ imports: [
+ BrowserAnimationsModule,
+ SharedModule,
+ NgbNavModule,
+ NgbTooltipModule,
+ ToastrModule.forRoot(),
+ RouterTestingModule,
+ HttpClientTestingModule
+ ],
+ declarations: [
+ RbdListComponent,
+ RbdDetailsComponent,
+ RbdSnapshotListComponent,
+ RbdConfigurationListComponent,
+ RbdTabsComponent
+ ],
+ providers: [TaskListService]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RbdListComponent);
+ component = fixture.componentInstance;
+ summaryService = TestBed.inject(SummaryService);
+ rbdService = TestBed.inject(RbdService);
+ headers = new HttpHeaders().set('X-Total-Count', '10');
+
+ // this is needed because summaryService isn't being reset after each test.
+ summaryService['summaryDataSource'] = new BehaviorSubject(null);
+ summaryService['summaryData$'] = summaryService['summaryDataSource'].asObservable();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('after ngOnInit', () => {
+ beforeEach(() => {
+ fixture.detectChanges();
+ spyOn(rbdService, 'list').and.callThrough();
+ });
+
+ it('should load images on init', () => {
+ refresh({});
+ expect(rbdService.list).toHaveBeenCalled();
+ });
+
+ it('should not load images on init because no data', () => {
+ refresh(undefined);
+ expect(rbdService.list).not.toHaveBeenCalled();
+ });
+
+ it('should call error function on init when summary service fails', () => {
+ spyOn(component.table, 'reset');
+ summaryService['summaryDataSource'].error(undefined);
+ expect(component.table.reset).toHaveBeenCalled();
+ });
+ });
+
+ describe('handling of provisioned columns', () => {
+ let rbdServiceListSpy: jasmine.Spy;
+
+ const images = [
+ {
+ name: 'img1',
+ pool_name: 'rbd',
+ features_name: ['layering', 'exclusive-lock'],
+ disk_usage: null,
+ total_disk_usage: null
+ },
+ {
+ name: 'img2',
+ pool_name: 'rbd',
+ features_name: ['layering', 'exclusive-lock', 'object-map', 'fast-diff'],
+ disk_usage: 1024,
+ total_disk_usage: 1024
+ }
+ ];
+
+ beforeEach(() => {
+ component.images = images;
+ refresh({ executing_tasks: [], finished_tasks: [] });
+ rbdServiceListSpy = spyOn(rbdService, 'list');
+ });
+
+ it('should display N/A for Provisioned & Total Provisioned columns if disk usage is null', () => {
+ rbdServiceListSpy.and.callFake(() =>
+ of([{ pool_name: 'rbd', value: images, headers: headers }])
+ );
+ fixture.detectChanges();
+ const spanWithoutFastDiff = fixture.debugElement.nativeElement.querySelectorAll(
+ '.datatable-body-cell-label span'
+ );
+ // check image with disk usage = null & fast-diff disabled
+ expect(spanWithoutFastDiff[6].textContent).toBe('N/A');
+
+ images[0]['features_name'] = ['layering', 'exclusive-lock', 'object-map', 'fast-diff'];
+ component.images = images;
+ refresh({ executing_tasks: [], finished_tasks: [] });
+
+ rbdServiceListSpy.and.callFake(() =>
+ of([{ pool_name: 'rbd', value: images, headers: headers }])
+ );
+ fixture.detectChanges();
+
+ const spanWithFastDiff = fixture.debugElement.nativeElement.querySelectorAll(
+ '.datatable-body-cell-label span'
+ );
+ // check image with disk usage = null & fast-diff changed to enabled
+ expect(spanWithFastDiff[6].textContent).toBe('-');
+ });
+ });
+
+ describe('handling of deletion', () => {
+ beforeEach(() => {
+ fixture.detectChanges();
+ });
+
+ it('should check if there are no snapshots', () => {
+ component.selection.add({
+ id: '-1',
+ name: 'rbd1',
+ pool_name: 'rbd'
+ });
+ expect(component.hasSnapshots()).toBeFalsy();
+ });
+
+ it('should check if there are snapshots', () => {
+ component.selection.add({
+ id: '-1',
+ name: 'rbd1',
+ pool_name: 'rbd',
+ snapshots: [{}, {}]
+ });
+ expect(component.hasSnapshots()).toBeTruthy();
+ });
+
+ it('should get delete disable description', () => {
+ component.selection.add({
+ id: '-1',
+ name: 'rbd1',
+ pool_name: 'rbd',
+ snapshots: [
+ {
+ children: [{}]
+ }
+ ]
+ });
+ expect(component.getDeleteDisableDesc(component.selection)).toBe(
+ 'This RBD has cloned snapshots. Please delete related RBDs before deleting this RBD.'
+ );
+ });
+
+ it('should list all protected snapshots', () => {
+ component.selection.add({
+ id: '-1',
+ name: 'rbd1',
+ pool_name: 'rbd',
+ snapshots: [
+ {
+ name: 'snap1',
+ is_protected: false
+ },
+ {
+ name: 'snap2',
+ is_protected: true
+ }
+ ]
+ });
+
+ expect(component.listProtectedSnapshots()).toEqual(['snap2']);
+ });
+ });
+
+ describe('handling of executing tasks', () => {
+ let images: RbdModel[];
+
+ const addImage = (name: string) => {
+ const model = new RbdModel();
+ model.id = '-1';
+ model.name = name;
+ model.pool_name = 'rbd';
+ images.push(model);
+ };
+
+ const addTask = (name: string, image_name: string) => {
+ const task = new ExecutingTask();
+ task.name = name;
+ switch (task.name) {
+ case 'rbd/copy':
+ task.metadata = {
+ dest_pool_name: 'rbd',
+ dest_namespace: null,
+ dest_image_name: 'd'
+ };
+ break;
+ case 'rbd/clone':
+ task.metadata = {
+ child_pool_name: 'rbd',
+ child_namespace: null,
+ child_image_name: 'd'
+ };
+ break;
+ case 'rbd/create':
+ task.metadata = {
+ pool_name: 'rbd',
+ namespace: null,
+ image_name: image_name
+ };
+ break;
+ default:
+ task.metadata = {
+ image_spec: `rbd/${image_name}`
+ };
+ break;
+ }
+ summaryService.addRunningTask(task);
+ };
+
+ beforeEach(() => {
+ images = [];
+ addImage('a');
+ addImage('b');
+ addImage('c');
+ component.images = images;
+ refresh({ executing_tasks: [], finished_tasks: [] });
+ spyOn(rbdService, 'list').and.callFake(() =>
+ of([{ pool_name: 'rbd', value: images, headers: headers }])
+ );
+ fixture.detectChanges();
+ });
+
+ it('should gets all images without tasks', () => {
+ expect(component.images.length).toBe(3);
+ expect(component.images.every((image: any) => !image.cdExecuting)).toBeTruthy();
+ });
+
+ it('should add a new image from a task', () => {
+ addTask('rbd/create', 'd');
+ expect(component.images.length).toBe(4);
+ expectItemTasks(component.images[0], undefined);
+ expectItemTasks(component.images[1], undefined);
+ expectItemTasks(component.images[2], undefined);
+ expectItemTasks(component.images[3], 'Creating');
+ });
+
+ it('should show when a image is being cloned', () => {
+ addTask('rbd/clone', 'd');
+ expect(component.images.length).toBe(4);
+ expectItemTasks(component.images[0], undefined);
+ expectItemTasks(component.images[1], undefined);
+ expectItemTasks(component.images[2], undefined);
+ expectItemTasks(component.images[3], 'Cloning');
+ });
+
+ it('should show when a image is being copied', () => {
+ addTask('rbd/copy', 'd');
+ expect(component.images.length).toBe(4);
+ expectItemTasks(component.images[0], undefined);
+ expectItemTasks(component.images[1], undefined);
+ expectItemTasks(component.images[2], undefined);
+ expectItemTasks(component.images[3], 'Copying');
+ });
+
+ it('should show when an existing image is being modified', () => {
+ addTask('rbd/edit', 'a');
+ expectItemTasks(component.images[0], 'Updating');
+ addTask('rbd/delete', 'b');
+ expectItemTasks(component.images[1], 'Deleting');
+ addTask('rbd/flatten', 'c');
+ expectItemTasks(component.images[2], 'Flattening');
+ expect(component.images.length).toBe(3);
+ });
+ });
+
+ it('should test all TableActions combinations', () => {
+ const permissionHelper: PermissionHelper = new PermissionHelper(component.permission);
+ const tableActions: TableActionsComponent = permissionHelper.setPermissionsAndGetActions(
+ component.tableActions
+ );
+
+ expect(tableActions).toEqual({
+ 'create,update,delete': {
+ actions: [
+ 'Create',
+ 'Edit',
+ 'Copy',
+ 'Flatten',
+ 'Resync',
+ 'Delete',
+ 'Move to Trash',
+ 'Remove Scheduling',
+ 'Promote',
+ 'Demote'
+ ],
+ primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,update': {
+ actions: [
+ 'Create',
+ 'Edit',
+ 'Copy',
+ 'Flatten',
+ 'Resync',
+ 'Remove Scheduling',
+ 'Promote',
+ 'Demote'
+ ],
+ primary: { multiple: 'Create', executing: 'Edit', single: 'Edit', no: 'Create' }
+ },
+ 'create,delete': {
+ actions: ['Create', 'Copy', 'Delete', 'Move to Trash'],
+ primary: { multiple: 'Create', executing: 'Copy', single: 'Copy', no: 'Create' }
+ },
+ create: {
+ actions: ['Create', 'Copy'],
+ primary: { multiple: 'Create', executing: 'Copy', single: 'Copy', no: 'Create' }
+ },
+ 'update,delete': {
+ actions: [
+ 'Edit',
+ 'Flatten',
+ 'Resync',
+ 'Delete',
+ 'Move to Trash',
+ 'Remove Scheduling',
+ 'Promote',
+ 'Demote'
+ ],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ update: {
+ actions: ['Edit', 'Flatten', 'Resync', 'Remove Scheduling', 'Promote', 'Demote'],
+ primary: { multiple: 'Edit', executing: 'Edit', single: 'Edit', no: 'Edit' }
+ },
+ delete: {
+ actions: ['Delete', 'Move to Trash'],
+ primary: { multiple: 'Delete', executing: 'Delete', single: 'Delete', no: 'Delete' }
+ },
+ 'no-permissions': {
+ actions: [],
+ primary: { multiple: '', executing: '', single: '', no: '' }
+ }
+ });
+ });
+
+ const getActionDisable = (name: string) =>
+ component.tableActions.find((o) => o.name === name).disable;
+
+ const testActions = (selection: any, expected: { [action: string]: string | boolean }) => {
+ expect(getActionDisable('Edit')(selection)).toBe(expected.edit || false);
+ expect(getActionDisable('Delete')(selection)).toBe(expected.delete || false);
+ expect(getActionDisable('Copy')(selection)).toBe(expected.copy || false);
+ expect(getActionDisable('Flatten')(selection)).toBeTruthy();
+ expect(getActionDisable('Move to Trash')(selection)).toBe(expected.moveTrash || false);
+ };
+
+ it('should test TableActions with valid/invalid image name', () => {
+ component.selection.selected = [
+ {
+ name: 'foobar',
+ pool_name: 'rbd',
+ snapshots: []
+ }
+ ];
+ testActions(component.selection, {});
+
+ component.selection.selected = [
+ {
+ name: 'foo/bar',
+ pool_name: 'rbd',
+ snapshots: []
+ }
+ ];
+ const message = `This RBD image has an invalid name and can't be managed by ceph.`;
+ const expected = {
+ edit: message,
+ delete: message,
+ copy: message,
+ moveTrash: message
+ };
+ testActions(component.selection, expected);
+ });
+
+ it('should disable edit, copy, flatten and move action if RBD is in status `Removing`', () => {
+ component.selection.selected = [
+ {
+ name: 'foobar',
+ pool_name: 'rbd',
+ snapshots: [],
+ source: 'REMOVING'
+ }
+ ];
+
+ const message = `Action not possible for an RBD in status 'Removing'`;
+ const expected = {
+ edit: message,
+ copy: message,
+ moveTrash: message
+ };
+ testActions(component.selection, expected);
+ });
+});