From ee38de488c9726fa02282bb2e856f0b27f164a15 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 15 Apr 2020 16:12:05 +0200 Subject: [PATCH] Added test for ResourcePoliciesComponent --- .../mocks/mock-resource-policy-service.ts | 10 + .../resource-policies.component.spec.ts | 469 ++++++++++++++++++ .../resource-policies.component.ts | 71 ++- src/app/shared/testing/group-mock.ts | 1 + 4 files changed, 529 insertions(+), 22 deletions(-) create mode 100644 src/app/shared/mocks/mock-resource-policy-service.ts create mode 100644 src/app/shared/resource-policies/resource-policies.component.spec.ts diff --git a/src/app/shared/mocks/mock-resource-policy-service.ts b/src/app/shared/mocks/mock-resource-policy-service.ts new file mode 100644 index 0000000000..864cf20730 --- /dev/null +++ b/src/app/shared/mocks/mock-resource-policy-service.ts @@ -0,0 +1,10 @@ +import { ResourcePolicyService } from '../../core/resource-policy/resource-policy.service'; + +export function getMockResourcePolicyService(): ResourcePolicyService { + return jasmine.createSpyObj('resourcePolicyService', { + searchByResource: jasmine.createSpy('searchByResource'), + create: jasmine.createSpy('create'), + delete: jasmine.createSpy('delete'), + update: jasmine.createSpy('update') + }); +} diff --git a/src/app/shared/resource-policies/resource-policies.component.spec.ts b/src/app/shared/resource-policies/resource-policies.component.spec.ts new file mode 100644 index 0000000000..4e318ca630 --- /dev/null +++ b/src/app/shared/resource-policies/resource-policies.component.spec.ts @@ -0,0 +1,469 @@ +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; + +import { of as observableOf } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { cold, getTestScheduler, hot } from 'jasmine-marbles'; + +import { Bitstream } from '../../core/shared/bitstream.model'; +import { Bundle } from '../../core/shared/bundle.model'; +import { createMockRDPaginatedObs } from '../../+item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec'; +import { Item } from '../../core/shared/item.model'; +import { LinkService } from '../../core/cache/builders/link.service'; +import { getMockLinkService } from '../mocks/mock-link-service'; +import { createSuccessfulRemoteDataObject, createTestComponent } from '../testing/utils'; +import { EPersonDataService } from '../../core/eperson/eperson-data.service'; +import { NotificationsService } from '../notifications/notifications.service'; +import { NotificationsServiceStub } from '../testing/notifications-service-stub'; +import { ResourcePolicyService } from '../../core/resource-policy/resource-policy.service'; +import { getMockResourcePolicyService } from '../mocks/mock-resource-policy-service'; +import { GroupDataService } from '../../core/eperson/group-data.service'; +import { RequestService } from '../../core/data/request.service'; +import { getMockRequestService } from '../mocks/mock-request.service'; +import { RouterStub } from '../testing/router-stub'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { ResourcePoliciesComponent } from './resource-policies.component'; +import { PolicyType } from '../../core/resource-policy/models/policy-type.model'; +import { ActionType } from '../../core/resource-policy/models/action-type.model'; +import { EPersonMock } from '../testing/eperson-mock'; +import { GroupMock } from '../testing/group-mock'; + +describe('ResourcePoliciesComponent test suite', () => { + let comp: ResourcePoliciesComponent; + let compAsAny: any; + let fixture: ComponentFixture; + let de; + let routerStub: any; + let scheduler: TestScheduler; + const notificationsServiceStub = new NotificationsServiceStub(); + const resourcePolicyService: any = getMockResourcePolicyService(); + const linkService: any = getMockLinkService(); + + const resourcePolicy: any = { + id: '1', + name: null, + description: null, + policyType: PolicyType.TYPE_SUBMISSION, + action: ActionType.READ, + startDate: null, + endDate: null, + type: 'resourcepolicy', + uuid: 'resource-policy-1', + _links: { + eperson: { + href: 'https://rest.api/rest/api/eperson' + }, + group: { + href: 'https://rest.api/rest/api/group' + }, + self: { + href: 'https://rest.api/rest/api/resourcepolicies/1' + }, + }, + eperson: observableOf(createSuccessfulRemoteDataObject({})), + group: observableOf(createSuccessfulRemoteDataObject(GroupMock)) + }; + + const anotherResourcePolicy: any = { + id: '2', + name: null, + description: null, + policyType: PolicyType.TYPE_SUBMISSION, + action: ActionType.WRITE, + startDate: null, + endDate: null, + type: 'resourcepolicy', + uuid: 'resource-policy-2', + _links: { + eperson: { + href: 'https://rest.api/rest/api/eperson' + }, + group: { + href: 'https://rest.api/rest/api/group' + }, + self: { + href: 'https://rest.api/rest/api/resourcepolicies/1' + }, + }, + eperson: observableOf(createSuccessfulRemoteDataObject(EPersonMock)), + group: observableOf(createSuccessfulRemoteDataObject({})) + }; + + const bitstream1 = Object.assign(new Bitstream(), { + id: 'bitstream1', + uuid: 'bitstream1' + }); + const bitstream2 = Object.assign(new Bitstream(), { + id: 'bitstream2', + uuid: 'bitstream2' + }); + const bitstream3 = Object.assign(new Bitstream(), { + id: 'bitstream3', + uuid: 'bitstream3' + }); + const bitstream4 = Object.assign(new Bitstream(), { + id: 'bitstream4', + uuid: 'bitstream4' + }); + const bundle1 = Object.assign(new Bundle(), { + id: 'bundle1', + uuid: 'bundle1', + _links: { + self: { href: 'bundle1-selflink' } + }, + bitstreams: createMockRDPaginatedObs([bitstream1, bitstream2]) + }); + const bundle2 = Object.assign(new Bundle(), { + id: 'bundle2', + uuid: 'bundle2', + _links: { + self: { href: 'bundle2-selflink' } + }, + bitstreams: createMockRDPaginatedObs([bitstream3, bitstream4]) + }); + + const item = Object.assign(new Item(), { + uuid: 'itemUUID', + id: 'itemUUID', + _links: { + self: { href: 'item-selflink' } + }, + bundles: createMockRDPaginatedObs([bundle1, bundle2]) + }); + + const routeStub = { + data: observableOf({ + item: createSuccessfulRemoteDataObject(item) + }) + }; + + const epersonService = jasmine.createSpyObj('epersonService', { + findByHref: jasmine.createSpy('findByHref'), + }); + + const groupService = jasmine.createSpyObj('groupService', { + findByHref: jasmine.createSpy('findByHref'), + }); + + routerStub = Object.assign(new RouterStub(), { + url: `url/edit` + }); + + const resourcePolicyEntries = [ + { + id: resourcePolicy.id, + policy: resourcePolicy, + checked: false + }, + { + id: anotherResourcePolicy.id, + policy: anotherResourcePolicy, + checked: false + } + ]; + const resourcePolicySelectedEntries = [ + { + id: resourcePolicy.id, + policy: resourcePolicy, + checked: true + }, + { + id: anotherResourcePolicy.id, + policy: anotherResourcePolicy, + checked: false + } + ]; + + const pageInfo = new PageInfo(); + const array = [resourcePolicy, anotherResourcePolicy]; + const paginatedList = new PaginatedList(pageInfo, array); + const resourcePolicyRD = createSuccessfulRemoteDataObject(resourcePolicy); + const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + TranslateModule.forRoot() + ], + declarations: [ + ResourcePoliciesComponent, + TestComponent + ], + providers: [ + { provide: LinkService, useValue: linkService }, + { provide: ActivatedRoute, useValue: routeStub }, + { provide: EPersonDataService, useValue: epersonService }, + { provide: GroupDataService, useValue: groupService }, + { provide: NotificationsService, useValue: notificationsServiceStub }, + { provide: ResourcePolicyService, useValue: resourcePolicyService }, + { provide: RequestService, useValue: getMockRequestService() }, + { provide: Router, useValue: routerStub }, + ChangeDetectorRef, + ResourcePoliciesComponent + ], schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + })); + + describe('', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + + // synchronous beforeEach + beforeEach(() => { + const html = ` + `; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + afterEach(() => { + testFixture.destroy(); + }); + + it('should create ResourcePoliciesComponent', inject([ResourcePoliciesComponent], (app: ResourcePoliciesComponent) => { + + expect(app).toBeDefined(); + + })); + }); + + describe('', () => { + beforeEach(() => { + fixture = TestBed.createComponent(ResourcePoliciesComponent); + comp = fixture.componentInstance; + compAsAny = fixture.componentInstance; + linkService.resolveLink.and.callFake((object, link) => object); + + }); + + afterEach(() => { + comp = null; + compAsAny = null; + de = null; + fixture.destroy(); + }); + + it('should init component properly', () => { + spyOn(comp, 'initResourcePolicyLIst'); + fixture.detectChanges(); + expect(compAsAny.isActive).toBeTruthy(); + expect(comp.initResourcePolicyLIst).toHaveBeenCalled(); + }); + + it('should init resource policies list properly', () => { + compAsAny.isActive = true; + resourcePolicyService.searchByResource.and.returnValue(hot('a|', { + a: paginatedListRD + })); + + scheduler = getTestScheduler(); + scheduler.schedule(() => comp.initResourcePolicyLIst()); + scheduler.flush(); + + expect(compAsAny.resourcePoliciesEntries$.value).toEqual(resourcePolicyEntries); + }); + }); + + describe('', () => { + beforeEach(() => { + fixture = TestBed.createComponent(ResourcePoliciesComponent); + comp = fixture.componentInstance; + compAsAny = fixture.componentInstance; + linkService.resolveLink.and.callFake((object, link) => object); + compAsAny.isActive = true; + compAsAny.resourcePoliciesEntries$.next(resourcePolicyEntries); + resourcePolicyService.searchByResource.and.returnValue(observableOf({})); + spyOn(comp, 'initResourcePolicyLIst').and.callFake(() => ({})); + fixture.detectChanges(); + }); + + afterEach(() => { + comp = null; + compAsAny = null; + de = null; + fixture.destroy(); + }); + + it('should render a table with a row for each policy', () => { + const rows = fixture.debugElement.queryAll(By.css('table > tbody > tr')); + expect(rows.length).toBe(2); + }); + + describe('canDelete', () => { + it('should return false when no row is selected', () => { + expect(comp.canDelete()).toBeObservable(cold('(a|)', { + a: false + })); + }); + + it('should return true when al least is selected', () => { + const checkbox = fixture.debugElement.query(By.css('table > tbody > tr:nth-child(1) input')); + + const event = { target: { checked: true } }; + checkbox.triggerEventHandler('change', event); + expect(comp.canDelete()).toBeObservable(cold('(a|)', { + a: true + })); + }); + }); + + describe('deleteSelectedResourcePolicies', () => { + beforeEach(() => { + compAsAny.resourcePoliciesEntries$.next(resourcePolicySelectedEntries); + fixture.detectChanges(); + }); + + it('should notify success when delete is successful', () => { + + resourcePolicyService.delete.and.returnValue(observableOf(true)); + scheduler = getTestScheduler(); + scheduler.schedule(() => comp.deleteSelectedResourcePolicies()); + scheduler.flush(); + + expect(notificationsServiceStub.success).toHaveBeenCalled(); + expect(comp.initResourcePolicyLIst).toHaveBeenCalled(); + }); + + it('should notify error when delete is not successful', () => { + + resourcePolicyService.delete.and.returnValue(observableOf(false)); + scheduler = getTestScheduler(); + scheduler.schedule(() => comp.deleteSelectedResourcePolicies()); + scheduler.flush(); + + expect(notificationsServiceStub.error).toHaveBeenCalled(); + expect(comp.initResourcePolicyLIst).toHaveBeenCalled(); + }); + }); + + it('should get the resource\'s policy list', () => { + + expect(comp.getResourcePolicies()).toBeObservable(cold('a', { + a: resourcePolicyEntries + })); + + }); + + describe('hasEPerson', () => { + it('should true when policy is link to the eperson', () => { + + expect(comp.hasEPerson(anotherResourcePolicy)).toBeObservable(cold('(ab|)', { + a: false, + b: true + })); + + }); + + it('should false when policy is not link to the eperson', () => { + + expect(comp.hasEPerson(resourcePolicy)).toBeObservable(cold('(aa|)', { + a: false + })); + + }); + }); + + describe('hasGroup', () => { + it('should true when policy is link to the group', () => { + + expect(comp.hasGroup(resourcePolicy)).toBeObservable(cold('(ab|)', { + a: false, + b: true + })); + + }); + + it('should false when policy is not link to the group', () => { + + expect(comp.hasGroup(anotherResourcePolicy)).toBeObservable(cold('(aa|)', { + a: false + })); + + }); + }); + + describe('getEPersonName', () => { + it('should return the eperson name', () => { + + expect(comp.getEPersonName(anotherResourcePolicy)).toBeObservable(cold('(ab|)', { + a: '', + b: 'User Test' + })); + }); + }); + + describe('getGroupName', () => { + it('should return the group name', () => { + + expect(comp.getGroupName(resourcePolicy)).toBeObservable(cold('(ab|)', { + a: '', + b: 'testgroupname' + })); + }); + }); + + it('should format date properly', () => { + expect(comp.formatDate('2020-04-14T12:00:00Z')).toBe('2020-04-14'); + }); + + it('should select All Checkbox', () => { + spyOn(comp, 'selectAllCheckbox').and.callThrough(); + const checkbox = fixture.debugElement.query(By.css('table > thead > tr:nth-child(2) input')); + + const event = { target: { checked: true } }; + checkbox.triggerEventHandler('change', event); + expect(comp.selectAllCheckbox).toHaveBeenCalled(); + }); + + it('should select a Checkbox', () => { + spyOn(comp, 'selectCheckbox').and.callThrough(); + const checkbox = fixture.debugElement.query(By.css('table > tbody > tr:nth-child(1) input')); + + const event = { target: { checked: true } }; + checkbox.triggerEventHandler('change', event); + expect(comp.selectCheckbox).toHaveBeenCalled(); + }); + + it('should redirect to create resource policy page', () => { + + comp.redirectToResourcePolicyCreatePage(); + expect(compAsAny.router.navigate).toHaveBeenCalled(); + }); + + it('should redirect to resource policy edit page', () => { + + comp.redirectToResourcePolicyEditPage(resourcePolicy); + expect(compAsAny.router.navigate).toHaveBeenCalled(); + }); + + it('should redirect to resource policy edit page', () => { + compAsAny.groupService.findByHref.and.returnValue(observableOf(createSuccessfulRemoteDataObject(GroupMock))); + + comp.redirectToGroupEditPage(resourcePolicy); + expect(compAsAny.router.navigate).toHaveBeenCalled(); + }); + }); +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + + resourceUUID = 'itemUUID'; + resourceType = 'item'; +} diff --git a/src/app/shared/resource-policies/resource-policies.component.ts b/src/app/shared/resource-policies/resource-policies.component.ts index 8ad11315a4..76b23c3001 100644 --- a/src/app/shared/resource-policies/resource-policies.component.ts +++ b/src/app/shared/resource-policies/resource-policies.component.ts @@ -21,6 +21,7 @@ import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { RequestService } from '../../core/data/request.service'; import { NotificationsService } from '../notifications/notifications.service'; import { dateToString, stringToNgbDateStruct } from '../date.util'; +import { followLink } from '../utils/follow-link-config.model'; interface ResourcePolicyCheckboxEntry { id: string; @@ -111,6 +112,11 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { this.initResourcePolicyLIst(); } + /** + * Check if there are any selected resource's policies to be deleted + * + * @return {Observable} + */ canDelete(): Observable { return observableFrom(this.resourcePoliciesEntries$.value).pipe( filter((entry: ResourcePolicyCheckboxEntry) => entry.checked), @@ -120,25 +126,30 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { ) } + /** + * Delete the selected resource's policies + */ deleteSelectedResourcePolicies(): void { this.processingDelete$.next(true); const policiesToDelete: ResourcePolicyCheckboxEntry[] = this.resourcePoliciesEntries$.value .filter((entry: ResourcePolicyCheckboxEntry) => entry.checked); - observableFrom(policiesToDelete).pipe( - concatMap((entry: ResourcePolicyCheckboxEntry) => this.resourcePolicyService.delete(entry.policy.id)), - scan((acc: any, value: any) => [...acc, ...value], []), - filter((results: boolean[]) => results.length === policiesToDelete.length), - take(1), - ).subscribe((results: boolean[]) => { - const failureResults = results.filter((result: boolean) => !result); - if (isEmpty(failureResults)) { - this.notificationsService.success(null, this.translate.get('resource-policies.delete.success.content')); - } else { - this.notificationsService.error(null, this.translate.get('resource-policies.delete.failure.content')); - } - this.initResourcePolicyLIst(); - this.processingDelete$.next(false); - }) + this.subs.push( + observableFrom(policiesToDelete).pipe( + concatMap((entry: ResourcePolicyCheckboxEntry) => this.resourcePolicyService.delete(entry.policy.id)), + scan((acc: any, value: any) => [...acc, ...value], []), + filter((results: boolean[]) => results.length === policiesToDelete.length), + take(1), + ).subscribe((results: boolean[]) => { + const failureResults = results.filter((result: boolean) => !result); + if (isEmpty(failureResults)) { + this.notificationsService.success(null, this.translate.get('resource-policies.delete.success.content')); + } else { + this.notificationsService.error(null, this.translate.get('resource-policies.delete.failure.content')); + } + this.initResourcePolicyLIst(); + this.processingDelete$.next(false); + }) + ) } /** @@ -158,7 +169,8 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { */ getEPersonName(policy: ResourcePolicy): Observable { // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved - return this.ePersonService.findByHref(policy._links.eperson.href).pipe( + // return this.ePersonService.findByHref(policy._links.eperson.href).pipe( + return policy.eperson.pipe( filter(() => this.isActive), getFirstSucceededRemoteDataWithNotEmptyPayload(), map((eperson: EPerson) => this.dsoNameService.getName(eperson)), @@ -173,7 +185,8 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { */ getGroupName(policy: ResourcePolicy): Observable { // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved - return this.groupService.findByHref(policy._links.group.href).pipe( + // return this.groupService.findByHref(policy._links.group.href).pipe( + return policy.group.pipe( filter(() => this.isActive), getFirstSucceededRemoteDataWithNotEmptyPayload(), map((group: Group) => this.dsoNameService.getName(group)), @@ -198,10 +211,12 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { */ hasEPerson(policy): Observable { // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved - return this.ePersonService.findByHref(policy._links.eperson.href).pipe( + // return this.ePersonService.findByHref(policy._links.eperson.href).pipe( + return policy.eperson.pipe( filter(() => this.isActive), getFirstSucceededRemoteDataPayload(), - map((eperson: EPerson) => isNotEmpty(eperson)) + map((eperson: EPerson) => isNotEmpty(eperson)), + startWith(false) ) } @@ -213,17 +228,23 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { */ hasGroup(policy): Observable { // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved - return this.groupService.findByHref(policy._links.group.href).pipe( + // return this.groupService.findByHref(policy._links.group.href).pipe( + return policy.group.pipe( filter(() => this.isActive), getFirstSucceededRemoteDataPayload(), - map((group: Group) => isNotEmpty(group)) + map((group: Group) => isNotEmpty(group)), + startWith(false) ) } + /** + * Initialize the resource's policies list + */ initResourcePolicyLIst() { // TODO to be reviewed when https://github.com/DSpace/dspace-angular/issues/644 will be resolved this.requestService.removeByHrefSubstring(this.resourceUUID); - this.resourcePolicyService.searchByResource(this.resourceUUID).pipe( + this.resourcePolicyService.searchByResource(this.resourceUUID, null, + followLink('eperson'), followLink('group')).pipe( filter(() => this.isActive), getSucceededRemoteData(), take(1) @@ -238,6 +259,11 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { }); } + /** + * Return a boolean representing if a delete operation is pending + * + * @return {Observable} + */ isProcessingDelete(): Observable { return this.processingDelete$.asObservable(); } @@ -305,6 +331,7 @@ export class ResourcePoliciesComponent implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.isActive = false; + this.resourcePoliciesEntries$ = null; this.subs .filter((subscription) => hasValue(subscription)) .forEach((subscription) => subscription.unsubscribe()) diff --git a/src/app/shared/testing/group-mock.ts b/src/app/shared/testing/group-mock.ts index 0c9abb4b7d..a0686bc6d0 100644 --- a/src/app/shared/testing/group-mock.ts +++ b/src/app/shared/testing/group-mock.ts @@ -10,6 +10,7 @@ export const GroupMock: Group = Object.assign(new Group(), { }, groups: { href: 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/groups' } }, + _name: 'testgroupname', id: 'testgroupid', uuid: 'testgroupid', type: 'group',