[CST-5307] Refactoring and adding missing unit tests

This commit is contained in:
Giuseppe Digilio
2022-05-10 18:14:00 +02:00
parent 0b664431af
commit 853dcecfb8
10 changed files with 876 additions and 286 deletions

View File

@@ -78,6 +78,7 @@ describe(`DSONameService`, () => {
});
describe(`factories.Person`, () => {
describe(`with person.familyName and person.givenName`, () => {
beforeEach(() => {
spyOn(mockPerson, 'firstMetadataValue').and.returnValues(...mockPersonName.split(', '));
});
@@ -87,6 +88,22 @@ describe(`DSONameService`, () => {
expect(result).toBe(mockPersonName);
expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.familyName');
expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.givenName');
expect(mockPerson.firstMetadataValue).not.toHaveBeenCalledWith('dc.title');
});
});
describe(`without person.familyName and person.givenName`, () => {
beforeEach(() => {
spyOn(mockPerson, 'firstMetadataValue').and.returnValues(undefined, undefined, mockPersonName);
});
it(`should return dc.title`, () => {
const result = (service as any).factories.Person(mockPerson);
expect(result).toBe(mockPersonName);
expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.familyName');
expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.givenName');
expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('dc.title');
});
});
});

View File

@@ -0,0 +1,290 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RequestService } from '../data/request.service';
import { PageInfo } from '../shared/page-info.model';
import { buildPaginatedList } from '../data/paginated-list.model';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { RestResponse } from '../cache/response.models';
import { RequestEntry } from '../data/request-entry.model';
import { ResearcherProfileService } from './researcher-profile.service';
import { RouterMock } from '../../shared/mocks/router.mock';
import { ResearcherProfile } from './model/researcher-profile.model';
import { Item } from '../shared/item.model';
import { ReplaceOperation } from 'fast-json-patch';
import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { PostRequest } from '../data/request.models';
describe('ResearcherProfileService', () => {
let scheduler: TestScheduler;
let service: ResearcherProfileService;
let serviceAsAny: any;
let requestService: RequestService;
let rdbService: RemoteDataBuildService;
let objectCache: ObjectCacheService;
let halService: HALEndpointService;
let responseCacheEntry: RequestEntry;
const researcherProfileId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
const itemId = 'beef9946-rt56-479e-8f11-b90cbe9f7241';
const researcherProfileItem: Item = Object.assign(new Item(), {
id: itemId,
_links: {
self: {
href: `https://rest.api/rest/api/items/${itemId}`
},
}
});
const researcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), {
id: researcherProfileId,
visible: false,
type: 'profile',
_links: {
item: {
href: `https://rest.api/rest/api/profiles/${researcherProfileId}/item`
},
self: {
href: `https://rest.api/rest/api/profiles/${researcherProfileId}`
},
}
});
const researcherProfilePatched: ResearcherProfile = Object.assign(new ResearcherProfile(), {
id: researcherProfileId,
visible: true,
type: 'profile',
_links: {
item: {
href: `https://rest.api/rest/api/profiles/${researcherProfileId}/item`
},
self: {
href: `https://rest.api/rest/api/profiles/${researcherProfileId}`
},
}
});
const researcherProfileId2 = 'agbf9946-f4ce-479e-8f11-b90cbe9f7241';
const anotherResearcherProfile: ResearcherProfile = Object.assign(new ResearcherProfile(), {
id: researcherProfileId2,
visible: false,
type: 'profile',
_links: {
self: {
href: `https://rest.api/rest/api/profiles/${researcherProfileId2}`
},
}
});
const endpointURL = `https://rest.api/rest/api/profiles`;
const sourceUri = `https://rest.api/rest/api/external-source/profile`;
const requestURL = `https://rest.api/rest/api/profiles/${researcherProfileId}`;
const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
const pageInfo = new PageInfo();
const array = [researcherProfile, anotherResearcherProfile];
const paginatedList = buildPaginatedList(pageInfo, array);
const researcherProfileRD = createSuccessfulRemoteDataObject(researcherProfile);
const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList);
beforeEach(() => {
scheduler = getTestScheduler();
halService = jasmine.createSpyObj('halService', {
getEndpoint: cold('a', { a: endpointURL })
});
responseCacheEntry = new RequestEntry();
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
requestService = jasmine.createSpyObj('requestService', {
generateRequestId: requestUUID,
send: true,
removeByHrefSubstring: {},
getByHref: observableOf(responseCacheEntry),
getByUUID: observableOf(responseCacheEntry),
setStaleByHrefSubstring: jasmine.createSpy('setStaleByHrefSubstring')
});
rdbService = jasmine.createSpyObj('rdbService', {
buildSingle: hot('a|', {
a: researcherProfileRD
}),
buildList: hot('a|', {
a: paginatedListRD
}),
buildFromRequestUUID: hot('a|', {
a: researcherProfileRD
})
});
objectCache = {} as ObjectCacheService;
const notificationsService = {} as NotificationsService;
const http = {} as HttpClient;
const comparator = {} as any;
const routerStub: any = new RouterMock();
const itemService = jasmine.createSpyObj('ItemService', {
findByHref: jasmine.createSpy('findByHref')
});
service = new ResearcherProfileService(
requestService,
rdbService,
objectCache,
halService,
notificationsService,
http,
routerStub,
comparator,
itemService
);
serviceAsAny = service;
spyOn((service as any).dataService, 'create').and.callThrough();
spyOn((service as any).dataService, 'delete').and.callThrough();
spyOn((service as any).dataService, 'update').and.callThrough();
spyOn((service as any).dataService, 'findById').and.callThrough();
spyOn((service as any).dataService, 'findByHref').and.callThrough();
spyOn((service as any).dataService, 'searchBy').and.callThrough();
spyOn((service as any).dataService, 'getLinkPath').and.returnValue(observableOf(endpointURL));
});
describe('findById', () => {
it('should proxy the call to dataservice.findById with eperson UUID', () => {
scheduler.schedule(() => service.findById(researcherProfileId));
scheduler.flush();
expect((service as any).dataService.findById).toHaveBeenCalledWith(researcherProfileId, true, true);
});
it('should return a ResearcherProfile object with the given id', () => {
const result = service.findById(researcherProfileId);
const expected = cold('a|', {
a: researcherProfileRD
});
expect(result).toBeObservable(expected);
});
});
describe('create', () => {
it('should proxy the call to dataservice.create with eperson UUID', () => {
scheduler.schedule(() => service.create());
scheduler.flush();
expect((service as any).dataService.create).toHaveBeenCalled();
});
it('should return the RemoteData<ResearcherProfile> created', () => {
const result = service.create();
const expected = cold('a|', {
a: researcherProfileRD
});
expect(result).toBeObservable(expected);
});
});
describe('delete', () => {
it('should proxy the call to dataservice.delete', () => {
scheduler.schedule(() => service.delete(researcherProfile));
scheduler.flush();
expect((service as any).dataService.delete).toHaveBeenCalledWith(researcherProfile.id);
});
});
describe('findRelatedItemId', () => {
describe('with a related item', () => {
beforeEach(() => {
(service as any).itemService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(researcherProfileItem));
});
it('should proxy the call to dataservice.findById with eperson UUID', () => {
scheduler.schedule(() => service.findRelatedItemId(researcherProfile));
scheduler.flush();
expect((service as any).itemService.findByHref).toHaveBeenCalledWith(researcherProfile._links.item.href, false);
});
it('should return a ResearcherProfile object with the given id', () => {
const result = service.findRelatedItemId(researcherProfile);
const expected = cold('(a|)', {
a: itemId
});
expect(result).toBeObservable(expected);
});
});
describe('without a related item', () => {
beforeEach(() => {
(service as any).itemService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(null));
});
it('should proxy the call to dataservice.findById with eperson UUID', () => {
scheduler.schedule(() => service.findRelatedItemId(researcherProfile));
scheduler.flush();
expect((service as any).itemService.findByHref).toHaveBeenCalledWith(researcherProfile._links.item.href, false);
});
it('should return a ResearcherProfile object with the given id', () => {
const result = service.findRelatedItemId(researcherProfile);
const expected = cold('(a|)', {
a: undefined
});
expect(result).toBeObservable(expected);
});
});
});
describe('setVisibility', () => {
let patchSpy;
beforeEach(() => {
spyOn((service as any), 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
spyOn((service as any), 'findById').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
});
it('should proxy the call to dataservice.patch', () => {
const replaceOperation: ReplaceOperation<boolean> = {
path: '/visible',
op: 'replace',
value: true
};
scheduler.schedule(() => service.setVisibility(researcherProfile, true));
scheduler.flush();
expect((service as any).patch).toHaveBeenCalledWith(researcherProfile, [replaceOperation]);
});
});
describe('createFromExternalSource', () => {
let patchSpy;
beforeEach(() => {
spyOn((service as any), 'patch').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
spyOn((service as any), 'findById').and.returnValue(createSuccessfulRemoteDataObject$(researcherProfilePatched));
});
it('should proxy the call to dataservice.patch', () => {
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'text/uri-list');
options.headers = headers;
const request = new PostRequest(requestUUID, endpointURL, sourceUri, options);
scheduler.schedule(() => service.createFromExternalSource(sourceUri));
scheduler.flush();
expect((service as any).requestService.send).toHaveBeenCalledWith(request);
expect((service as any).rdbService.buildFromRequestUUID).toHaveBeenCalledWith(requestUUID);
});
});
});

View File

@@ -2,27 +2,25 @@
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { Operation, RemoveOperation, ReplaceOperation } from 'fast-json-patch';
import { combineLatest, Observable, of as observableOf } from 'rxjs';
import { Operation, ReplaceOperation } from 'fast-json-patch';
import { Observable, of as observableOf } from 'rxjs';
import { catchError, find, map, switchMap, tap } from 'rxjs/operators';
import { environment } from '../../../environments/environment';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { dataService } from '../cache/builders/build-decorators';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { ConfigurationDataService } from '../data/configuration-data.service';
import { DataService } from '../data/data.service';
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
import { ItemDataService } from '../data/item-data.service';
import { RemoteData } from '../data/remote-data';
import { RequestService } from '../data/request.service';
import { ConfigurationProperty } from '../shared/configuration-property.model';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Item } from '../shared/item.model';
import { NoContent } from '../shared/NoContent.model';
import {
getFinishedRemoteData,
getAllCompletedRemoteData,
getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload
} from '../shared/operators';
@@ -32,6 +30,7 @@ import { HttpOptions } from '../dspace-rest/dspace-rest.service';
import { PostRequest } from '../data/request.models';
import { hasValue } from '../../shared/empty.util';
import { CoreState } from '../core-state.model';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
/**
* A private DataService implementation to delegate specific methods to.
@@ -67,17 +66,15 @@ export class ResearcherProfileService {
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected router: Router,
protected comparator: DefaultChangeAnalyzer<ResearcherProfile>,
protected itemService: ItemDataService,
protected configurationService: ConfigurationDataService ) {
protected itemService: ItemDataService) {
this.dataService = new ResearcherProfileServiceImpl(requestService, rdbService, store, objectCache, halService,
this.dataService = new ResearcherProfileServiceImpl(requestService, rdbService, null, objectCache, halService,
notificationsService, http, comparator);
}
@@ -86,17 +83,23 @@ export class ResearcherProfileService {
* Find the researcher profile with the given uuid.
*
* @param uuid the profile uuid
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/
findById(uuid: string): Observable<ResearcherProfile> {
return this.dataService.findById(uuid, false)
.pipe ( getFinishedRemoteData(),
map((remoteData) => remoteData.payload));
public findById(uuid: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<ResearcherProfile>[]): Observable<RemoteData<ResearcherProfile>> {
return this.dataService.findById(uuid, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
getAllCompletedRemoteData(),
);
}
/**
* Create a new researcher profile for the current user.
*/
create(): Observable<RemoteData<ResearcherProfile>> {
public create(): Observable<RemoteData<ResearcherProfile>> {
return this.dataService.create(new ResearcherProfile());
}
@@ -105,7 +108,7 @@ export class ResearcherProfileService {
*
* @param researcherProfile the profile to delete
*/
delete(researcherProfile: ResearcherProfile): Observable<boolean> {
public delete(researcherProfile: ResearcherProfile): Observable<boolean> {
return this.dataService.delete(researcherProfile.id).pipe(
getFirstCompletedRemoteData(),
tap((response: RemoteData<NoContent>) => {
@@ -122,14 +125,15 @@ export class ResearcherProfileService {
*
* @param researcherProfile the profile to find for
*/
findRelatedItemId( researcherProfile: ResearcherProfile ): Observable<string> {
public findRelatedItemId(researcherProfile: ResearcherProfile): Observable<string> {
return this.itemService.findByHref(researcherProfile._links.item.href, false)
.pipe(getFirstSucceededRemoteDataPayload(),
catchError((error) => {
console.debug(error);
return observableOf(null);
}),
map((item) => item != null ? item.id : null ));
map((item) => item?.id)
);
}
/**
@@ -138,7 +142,7 @@ export class ResearcherProfileService {
* @param researcherProfile the profile to update
* @param visible the visibility value to set
*/
setVisibility(researcherProfile: ResearcherProfile, visible: boolean): Observable<ResearcherProfile> {
public setVisibility(researcherProfile: ResearcherProfile, visible: boolean): Observable<ResearcherProfile> {
const replaceOperation: ReplaceOperation<boolean> = {
path: '/visible',
@@ -147,14 +151,11 @@ export class ResearcherProfileService {
};
return this.patch(researcherProfile, [replaceOperation]).pipe(
switchMap( ( ) => this.findById(researcherProfile.id))
switchMap(() => this.findById(researcherProfile.id)),
getFirstSucceededRemoteDataPayload()
);
}
patch(researcherProfile: ResearcherProfile, operations: Operation[]): Observable<RemoteData<ResearcherProfile>> {
return this.dataService.patch(researcherProfile, operations);
}
/**
* Creates a researcher profile starting from an external source URI
* @param sourceUri URI of source item of researcher profile.
@@ -179,4 +180,7 @@ export class ResearcherProfileService {
return this.rdbService.buildFromRequestUUID(requestId);
}
private patch(researcherProfile: ResearcherProfile, operations: Operation[]): Observable<RemoteData<ResearcherProfile>> {
return this.dataService.patch(researcherProfile, operations);
}
}

View File

@@ -0,0 +1,215 @@
import { cold, getTestScheduler } from 'jasmine-marbles';
import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { ProfileClaimService } from './profile-claim.service';
import { SearchService } from '../../core/shared/search/search.service';
import { ItemSearchResult } from '../../shared/object-collection/shared/item-search-result.model';
import { SearchObjects } from '../../shared/search/models/search-objects.model';
import { Item } from '../../core/shared/item.model';
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
import { EPerson } from '../../core/eperson/models/eperson.model';
describe('ProfileClaimService', () => {
let scheduler: TestScheduler;
let service: ProfileClaimService;
let serviceAsAny: any;
let searchService: jasmine.SpyObj<SearchService>;
const eperson: EPerson = Object.assign(new EPerson(), {
id: 'id',
metadata: {
'eperson.firstname': [
{
value: 'John'
}
],
'eperson.lastname': [
{
value: 'Doe'
},
],
},
email: 'fake@email.com'
});
const item1: Item = Object.assign(new Item(), {
uuid: 'e1c51c69-896d-42dc-8221-1d5f2ad5516e',
metadata: {
'person.email': [
{
value: 'fake@email.com'
}
],
'person.familyName': [
{
value: 'Doe'
}
],
'person.givenName': [
{
value: 'John'
}
]
},
_links: {
self: {
href: 'item-href'
}
}
});
const item2: Item = Object.assign(new Item(), {
uuid: 'c8279647-1acc-41ae-b036-951d5f65649b',
metadata: {
'person.email': [
{
value: 'fake2@email.com'
}
],
'dc.title': [
{
value: 'John, Doe'
}
]
},
_links: {
self: {
href: 'item-href'
}
}
});
const item3: Item = Object.assign(new Item(), {
uuid: 'c8279647-1acc-41ae-b036-951d5f65649b',
metadata: {
'person.email': [
{
value: 'fake3@email.com'
}
],
'dc.title': [
{
value: 'John, Doe'
}
]
},
_links: {
self: {
href: 'item-href'
}
}
});
const searchResult1 = Object.assign(new ItemSearchResult(), { indexableObject: item1 });
const searchResult2 = Object.assign(new ItemSearchResult(), { indexableObject: item2 });
const searchResult3 = Object.assign(new ItemSearchResult(), { indexableObject: item3 });
const searchResult = Object.assign(new SearchObjects(), {
page: [searchResult1, searchResult2, searchResult3]
});
const emptySearchResult = Object.assign(new SearchObjects(), {
page: []
});
const searchResultRD = createSuccessfulRemoteDataObject(searchResult);
const emptyRearchResultRD = createSuccessfulRemoteDataObject(emptySearchResult);
beforeEach(() => {
scheduler = getTestScheduler();
searchService = jasmine.createSpyObj('SearchService', {
search: jasmine.createSpy('search')
});
service = new ProfileClaimService(searchService);
serviceAsAny = service;
});
describe('hasProfilesToSuggest', () => {
describe('when has suggestions', () => {
beforeEach(() => {
spyOn(service, 'search').and.returnValue(observableOf(searchResultRD));
});
it('should return true', () => {
const result = service.hasProfilesToSuggest(eperson);
const expected = cold('(a|)', {
a: true
});
expect(result).toBeObservable(expected);
});
});
describe('when has not suggestions', () => {
beforeEach(() => {
spyOn(service, 'search').and.returnValue(observableOf(emptyRearchResultRD));
});
it('should return false', () => {
const result = service.hasProfilesToSuggest(eperson);
const expected = cold('(a|)', {
a: false
});
expect(result).toBeObservable(expected);
});
});
describe('when has not valid eperson', () => {
it('should return false', () => {
const result = service.hasProfilesToSuggest(null);
const expected = cold('(a|)', {
a: false
});
expect(result).toBeObservable(expected);
});
});
});
describe('search', () => {
describe('when has search results', () => {
beforeEach(() => {
searchService.search.and.returnValue(observableOf(searchResultRD));
});
it('should return the proper search object', () => {
const result = service.search(eperson);
const expected = cold('(a|)', {
a: searchResultRD
});
expect(result).toBeObservable(expected);
});
});
describe('when has not suggestions', () => {
beforeEach(() => {
searchService.search.and.returnValue(observableOf(emptyRearchResultRD));
});
it('should return null', () => {
const result = service.search(eperson);
const expected = cold('(a|)', {
a: emptyRearchResultRD
});
expect(result).toBeObservable(expected);
});
});
describe('when has not valid eperson', () => {
it('should return null', () => {
const result = service.search(null);
const expected = cold('(a|)', {
a: null
});
expect(result).toBeObservable(expected);
});
});
});
});

View File

@@ -1,16 +1,16 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { mergeMap, take } from 'rxjs/operators';
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { map } from 'rxjs/operators';
import { RemoteData } from '../../core/data/remote-data';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { SearchService } from '../../core/shared/search/search.service';
import { hasValue } from '../../shared/empty.util';
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
import { SearchResult } from '../../shared/search/models/search-result.model';
import { getFirstSucceededRemoteData } from './../../core/shared/operators';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { SearchObjects } from '../../shared/search/models/search-objects.model';
/**
* Service that handle profiles claim.
@@ -18,8 +18,7 @@ import { getFirstSucceededRemoteData } from './../../core/shared/operators';
@Injectable()
export class ProfileClaimService {
constructor(private searchService: SearchService,
private configurationService: ConfigurationDataService) {
constructor(private searchService: SearchService) {
}
/**
@@ -27,27 +26,21 @@ export class ProfileClaimService {
*
* @param eperson the eperson
*/
canClaimProfiles(eperson: EPerson): Observable<boolean> {
const query = this.personQueryData(eperson);
if (!hasValue(query) || query.length === 0) {
return of(false);
}
return this.lookup(query).pipe(
mergeMap((rd: RemoteData<PaginatedList<SearchResult<DSpaceObject>>>) => of(rd.payload.totalElements > 0))
hasProfilesToSuggest(eperson: EPerson): Observable<boolean> {
return this.search(eperson).pipe(
map((rd: RemoteData<SearchObjects<DSpaceObject>>) => {
return isNotEmpty(rd) && rd.hasSucceeded && rd.payload?.page?.length > 0;
})
);
}
/**
* Returns profiles that could be associated with the given user.
* @param eperson the user
*/
search(eperson: EPerson): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
search(eperson: EPerson): Observable<RemoteData<SearchObjects<DSpaceObject>>> {
const query = this.personQueryData(eperson);
if (!hasValue(query) || query.length === 0) {
if (isEmpty(query)) {
return of(null);
}
return this.lookup(query);
@@ -57,21 +50,31 @@ export class ProfileClaimService {
* Search object by the given query.
* @param query the query for the search
*/
private lookup(query: string): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
if (!hasValue(query)) {
private lookup(query: string): Observable<RemoteData<SearchObjects<DSpaceObject>>> {
if (isEmpty(query)) {
return of(null);
}
return this.searchService.search(new PaginatedSearchOptions({
configuration: 'eperson_claims',
query: query
}))
.pipe(
getFirstSucceededRemoteData(),
take(1));
})).pipe(
getFirstCompletedRemoteData()
);
}
/**
* Return the search query for person lookup, from the given eperson
*
* @param eperson The eperson to use for the lookup
*/
private personQueryData(eperson: EPerson): string {
return 'dc.title:' + eperson.name;
if (eperson) {
const firstname = eperson.firstMetadataValue('eperson.firstname');
const lastname = eperson.firstMetadataValue('eperson.lastname');
return 'dc.title:' + eperson.name + ' OR (person.familyName:' + lastname + ' AND person.givenName:' + firstname + ')';
} else {
return null;
}
}
}

View File

@@ -16,6 +16,7 @@ import { ProfilePageResearcherFormComponent } from './profile-page-researcher-fo
import { ProfileClaimService } from '../profile-claim/profile-claim.service';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { AuthService } from 'src/app/core/auth/auth.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
describe('ProfilePageResearcherFormComponent', () => {
@@ -51,7 +52,7 @@ describe('ProfilePageResearcherFormComponent', () => {
});
researcherProfileService = jasmine.createSpyObj('researcherProfileService', {
findById: observableOf(profile),
findById: createSuccessfulRemoteDataObject$(profile),
create: observableOf(profile),
setVisibility: observableOf(profile),
delete: observableOf(true),
@@ -61,7 +62,7 @@ describe('ProfilePageResearcherFormComponent', () => {
notificationsServiceStub = new NotificationsServiceStub();
profileClaimService = jasmine.createSpyObj('profileClaimService', {
canClaimProfiles: observableOf(false),
hasProfilesToSuggest: observableOf(false),
});
}
@@ -91,7 +92,7 @@ describe('ProfilePageResearcherFormComponent', () => {
});
it('should search the researcher profile for the current user', () => {
expect(researcherProfileService.findById).toHaveBeenCalledWith(user.id);
expect(researcherProfileService.findById).toHaveBeenCalledWith(user.id, false);
});
describe('createProfile', () => {

View File

@@ -4,17 +4,18 @@ import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { filter, mergeMap, switchMap, take, tap } from 'rxjs/operators';
import { mergeMap, switchMap, take, tap } from 'rxjs/operators';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { ClaimItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component';
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
import {
ClaimItemSelectorComponent
} from '../../shared/dso-selector/modal-wrappers/claim-item-selector/claim-item-selector.component';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { AuthService } from '../../core/auth/auth.service';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { ResearcherProfile } from '../../core/profile/model/researcher-profile.model';
import { ResearcherProfileService } from '../../core/profile/researcher-profile.service';
import { ProfileClaimService } from '../profile-claim/profile-claim.service';
import { isNotEmpty } from '../../shared/empty.util';
@Component({
selector: 'ds-profile-page-researcher-form',
@@ -77,10 +78,10 @@ export class ProfilePageResearcherFormComponent implements OnInit {
this.processingCreate$.next(true);
this.authService.getAuthenticatedUserFromStore().pipe(
switchMap((currentUser) => this.profileClaimService.canClaimProfiles(currentUser)))
.subscribe((canClaimProfiles) => {
switchMap((currentUser) => this.profileClaimService.hasProfilesToSuggest(currentUser)))
.subscribe((hasProfilesToSuggest) => {
if (canClaimProfiles) {
if (hasProfilesToSuggest) {
this.processingCreate$.next(false);
const modal = this.modalService.open(ClaimItemSelectorComponent);
modal.componentInstance.dso = this.user;
@@ -174,9 +175,8 @@ export class ProfilePageResearcherFormComponent implements OnInit {
* Initializes the researcherProfile and researcherProfileItemId attributes using the profile of the current user.
*/
private initResearchProfile(): void {
this.researcherProfileService.findById(this.user.id).pipe(
take(1),
filter((researcherProfile) => isNotEmpty(researcherProfile)),
this.researcherProfileService.findById(this.user.id, false).pipe(
getFirstSucceededRemoteDataPayload(),
tap((researcherProfile) => this.researcherProfile$.next(researcherProfile)),
mergeMap((researcherProfile) => this.researcherProfileService.findRelatedItemId(researcherProfile)),
).subscribe((itemId: string) => {

View File

@@ -11,14 +11,14 @@ import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model';
import { EPersonDataService } from '../core/eperson/eperson-data.service';
import { NotificationsService } from '../shared/notifications/notifications.service';
import { authReducer } from '../core/auth/auth.reducer';
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
import { createPaginatedList } from '../shared/testing/utils.test';
import { BehaviorSubject, of as observableOf } from 'rxjs';
import { AuthService } from '../core/auth/auth.service';
import { RestResponse } from '../core/cache/response.models';
import { provideMockStore } from '@ngrx/store/testing';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { getTestScheduler } from 'jasmine-marbles';
import { cold, getTestScheduler } from 'jasmine-marbles';
import { By } from '@angular/platform-browser';
import { ConfigurationDataService } from '../core/data/configuration-data.service';
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
@@ -30,10 +30,22 @@ describe('ProfilePageComponent', () => {
let initialState: any;
let authService;
let authorizationService;
let epersonService;
let notificationsService;
let configurationService;
const canChangePassword = new BehaviorSubject(true);
const validConfiguration = Object.assign(new ConfigurationProperty(), {
name: 'researcher-profile.entity-type',
values: [
'Person'
]
});
const emptyConfiguration = Object.assign(new ConfigurationProperty(), {
name: 'researcher-profile.entity-type',
values: []
});
function init() {
user = Object.assign(new EPerson(), {
@@ -54,7 +66,7 @@ describe('ProfilePageComponent', () => {
}
}
};
authorizationService = jasmine.createSpyObj('authorizationService', { isAuthorized: canChangePassword });
authService = jasmine.createSpyObj('authService', {
getAuthenticatedUserFromStore: observableOf(user)
});
@@ -67,6 +79,9 @@ describe('ProfilePageComponent', () => {
error: {},
warning: {}
});
configurationService = jasmine.createSpyObj('configurationDataService', {
findByPropertyName: jasmine.createSpy('findByPropertyName')
});
}
beforeEach(waitForAsync(() => {
@@ -82,15 +97,8 @@ describe('ProfilePageComponent', () => {
{ provide: EPersonDataService, useValue: epersonService },
{ provide: NotificationsService, useValue: notificationsService },
{ provide: AuthService, useValue: authService },
{ provide: ConfigurationDataService, useValue: jasmine.createSpyObj('configurationDataService', {
findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), {
name: 'researcher-profile.entity-type',
values: [
'Person'
]
}))
})},
{ provide: AuthorizationDataService, useValue: jasmine.createSpyObj('authorizationService', { isAuthorized: canChangePassword }) },
{ provide: ConfigurationDataService, useValue: configurationService },
{ provide: AuthorizationDataService, useValue: authorizationService },
provideMockStore({ initialState }),
],
schemas: [NO_ERRORS_SCHEMA]
@@ -100,6 +108,12 @@ describe('ProfilePageComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(ProfilePageComponent);
component = fixture.componentInstance;
});
describe('', () => {
beforeEach(() => {
configurationService.findByPropertyName.and.returnValue(createSuccessfulRemoteDataObject$(validConfiguration));
fixture.detectChanges();
});
@@ -246,3 +260,55 @@ describe('ProfilePageComponent', () => {
});
});
});
describe('isResearcherProfileEnabled', () => {
describe('when configuration service return values', () => {
beforeEach(() => {
configurationService.findByPropertyName.and.returnValue(createSuccessfulRemoteDataObject$(validConfiguration));
fixture.detectChanges();
});
it('should return true', () => {
const result = component.isResearcherProfileEnabled();
const expected = cold('a', {
a: true
});
expect(result).toBeObservable(expected);
});
});
describe('when configuration service return no values', () => {
beforeEach(() => {
configurationService.findByPropertyName.and.returnValue(createSuccessfulRemoteDataObject$(emptyConfiguration));
fixture.detectChanges();
});
it('should return false', () => {
const result = component.isResearcherProfileEnabled();
const expected = cold('a', {
a: false
});
expect(result).toBeObservable(expected);
});
});
describe('when configuration service return an error', () => {
beforeEach(() => {
configurationService.findByPropertyName.and.returnValue(createFailedRemoteDataObject$());
fixture.detectChanges();
});
it('should return false', () => {
const result = component.isResearcherProfileEnabled();
const expected = cold('a', {
a: false
});
expect(result).toBeObservable(expected);
});
});
});
});

View File

@@ -9,11 +9,7 @@ import { RemoteData } from '../core/data/remote-data';
import { PaginatedList } from '../core/data/paginated-list.model';
import { filter, switchMap, tap } from 'rxjs/operators';
import { EPersonDataService } from '../core/eperson/eperson-data.service';
import {
getAllSucceededRemoteData,
getRemoteDataPayload,
getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload
} from '../core/shared/operators';
import { getAllSucceededRemoteData, getFirstCompletedRemoteData, getRemoteDataPayload } from '../core/shared/operators';
import { hasValue, isNotEmpty } from '../shared/empty.util';
import { followLink } from '../shared/utils/follow-link-config.model';
import { AuthService } from '../core/auth/auth.service';
@@ -21,6 +17,7 @@ import { Operation } from 'fast-json-patch';
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../core/data/feature-authorization/feature-id';
import { ConfigurationDataService } from '../core/data/configuration-data.service';
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
@Component({
selector: 'ds-profile-page',
@@ -94,8 +91,10 @@ export class ProfilePageComponent implements OnInit {
this.canChangePassword$ = this.user$.pipe(switchMap((user: EPerson) => this.authorizationService.isAuthorized(FeatureID.CanChangePassword, user._links.self.href)));
this.configurationService.findByPropertyName('researcher-profile.entity-type').pipe(
getFirstSucceededRemoteDataPayload()
).subscribe(() => this.isResearcherProfileEnabled$.next(true));
getFirstCompletedRemoteData()
).subscribe((configRD: RemoteData<ConfigurationProperty>) => {
this.isResearcherProfileEnabled$.next(configRD.hasSucceeded && configRD.payload.values.length > 0);
});
}
/**
@@ -175,8 +174,8 @@ export class ProfilePageComponent implements OnInit {
/**
* Returns true if the researcher profile feature is enabled, false otherwise.
*/
isResearcherProfileEnabled(){
return this.isResearcherProfileEnabled$;
isResearcherProfileEnabled(): Observable<boolean> {
return this.isResearcherProfileEnabled$.asObservable();
}
}

View File

@@ -8884,11 +8884,6 @@ ngx-ui-switch@^11.0.1:
resolved "https://registry.yarnpkg.com/ngx-ui-switch/-/ngx-ui-switch-11.0.1.tgz#c7f1e97ebe698f827a26f49951b50492b22c7839"
integrity sha512-N8QYT/wW+xJdyh/aeebTSLPA6Sgrwp69H6KAcW0XZueg/LF+FKiqyG6Po/gFHq2gDhLikwyJEMpny8sudTI08w==
nice-try@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
nice-napi@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/nice-napi/-/nice-napi-1.0.2.tgz#dc0ab5a1eac20ce548802fc5686eaa6bc654927b"