diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts index e84b254eae..71fd74a707 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.spec.ts @@ -11,6 +11,8 @@ import { ActivatedRoute, Router } from '@angular/router'; import { getForbiddenRoute } from '../../app-routing-paths'; import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; +import { ServerResponseService } from '../../core/services/server-response.service'; describe('BitstreamDownloadPageComponent', () => { let component: BitstreamDownloadPageComponent; @@ -24,6 +26,8 @@ describe('BitstreamDownloadPageComponent', () => { let router; let bitstream: Bitstream; + let serverResponseService: jasmine.SpyObj; + let signpostingDataService: jasmine.SpyObj; function init() { authService = jasmine.createSpyObj('authService', { @@ -44,8 +48,8 @@ describe('BitstreamDownloadPageComponent', () => { bitstream = Object.assign(new Bitstream(), { uuid: 'bitstreamUuid', _links: { - content: {href: 'bitstream-content-link'}, - self: {href: 'bitstream-self-link'}, + content: { href: 'bitstream-content-link' }, + self: { href: 'bitstream-self-link' }, } }); @@ -54,10 +58,21 @@ describe('BitstreamDownloadPageComponent', () => { bitstream: createSuccessfulRemoteDataObject( bitstream ) + }), + params: observableOf({ + id: 'testid' }) }; router = jasmine.createSpyObj('router', ['navigateByUrl']); + + serverResponseService = jasmine.createSpyObj('ServerResponseService', { + setLinksetsHeader: jasmine.createSpy('setLinksetsHeader'), + }); + + signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { + getLinksets: observableOf('test'), + }); } function initTestbed() { @@ -65,12 +80,14 @@ describe('BitstreamDownloadPageComponent', () => { imports: [CommonModule, TranslateModule.forRoot()], declarations: [BitstreamDownloadPageComponent], providers: [ - {provide: ActivatedRoute, useValue: activatedRoute}, - {provide: Router, useValue: router}, - {provide: AuthorizationDataService, useValue: authorizationService}, - {provide: AuthService, useValue: authService}, - {provide: FileService, useValue: fileService}, - {provide: HardRedirectService, useValue: hardRedirectService}, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: Router, useValue: router }, + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: AuthService, useValue: authService }, + { provide: FileService, useValue: fileService }, + { provide: HardRedirectService, useValue: hardRedirectService }, + { provide: ServerResponseService, useValue: serverResponseService }, + { provide: SignpostingDataService, useValue: signpostingDataService } ] }) .compileComponents(); @@ -134,7 +151,7 @@ describe('BitstreamDownloadPageComponent', () => { fixture.detectChanges(); }); it('should navigate to the forbidden route', () => { - expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), {skipLocationChange: true}); + expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), { skipLocationChange: true }); }); }); describe('when the user is not authorized and not logged in', () => { diff --git a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts index 38e4d01dd5..4814a9385a 100644 --- a/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts +++ b/src/app/bitstream-page/bitstream-download-page/bitstream-download-page.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { filter, map, switchMap, take } from 'rxjs/operators'; import { ActivatedRoute, Router } from '@angular/router'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { getRemoteDataPayload} from '../../core/shared/operators'; +import { getRemoteDataPayload } from '../../core/shared/operators'; import { Bitstream } from '../../core/shared/bitstream.model'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; @@ -29,7 +29,6 @@ export class BitstreamDownloadPageComponent implements OnInit { bitstream$: Observable; bitstreamRD$: Observable>; - constructor( private route: ActivatedRoute, protected router: Router, @@ -42,8 +41,10 @@ export class BitstreamDownloadPageComponent implements OnInit { private responseService: ServerResponseService ) { this.route.params.subscribe(params => { - this.signpostginDataService.getLinksets(params.id).pipe(take(1)).subscribe(linksets => { - this.responseService.setLinksetsHeader(linksets); + this.signpostginDataService.getLinks(params.id).pipe(take(1)).subscribe(linksets => { + linksets.forEach(link => { + this.responseService.setLinksetsHeader(link.href); + }); }); }); } diff --git a/src/app/core/data/signposting-data.service.spec.ts b/src/app/core/data/signposting-data.service.spec.ts index 45c0abb29b..091f38de2a 100644 --- a/src/app/core/data/signposting-data.service.spec.ts +++ b/src/app/core/data/signposting-data.service.spec.ts @@ -1,16 +1,84 @@ -import { TestBed } from '@angular/core/testing'; - +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; import { SignpostingDataService } from './signposting-data.service'; +import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { of } from 'rxjs'; +import { SignpostingLinks } from './signposting-links.model'; +import { map } from 'rxjs/operators'; describe('SignpostingDataService', () => { let service: SignpostingDataService; + let restServiceSpy: jasmine.SpyObj; + let halServiceSpy: jasmine.SpyObj; beforeEach(() => { - TestBed.configureTestingModule({}); + const restSpy = jasmine.createSpyObj('DspaceRestService', ['get', 'getWithHeaders']); + const halSpy = jasmine.createSpyObj('HALEndpointService', ['getRootHref']); + + TestBed.configureTestingModule({ + providers: [ + SignpostingDataService, + { provide: DspaceRestService, useValue: restSpy }, + { provide: HALEndpointService, useValue: halSpy } + ] + }); + service = TestBed.inject(SignpostingDataService); + restServiceSpy = TestBed.inject(DspaceRestService) as jasmine.SpyObj; + halServiceSpy = TestBed.inject(HALEndpointService) as jasmine.SpyObj; }); it('should be created', () => { expect(service).toBeTruthy(); }); + + it('should return signposting links', fakeAsync(() => { + const uuid = '123'; + const baseUrl = 'http://localhost:8080'; + + halServiceSpy.getRootHref.and.returnValue(`${baseUrl}/api`); + + const mockResponse: any = { + self: { + href: `${baseUrl}/signposting/links/${uuid}` + } + }; + + restServiceSpy.get.and.returnValue(of(mockResponse)); + + let result: SignpostingLinks; + + service.getLinks(uuid).subscribe((links) => { + result = links; + }); + + tick(); + + expect(result).toEqual(mockResponse); + expect(halServiceSpy.getRootHref).toHaveBeenCalled(); + expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`); + })); + + it('should handle error and return false', fakeAsync(() => { + const uuid = '123'; + const baseUrl = 'http://localhost:8080'; + + halServiceSpy.getRootHref.and.returnValue(`${baseUrl}/api`); + + restServiceSpy.get.and.returnValue(of(null).pipe(map(() => { + throw new Error('Error'); + }))); + + let result: any; + + service.getLinks(uuid).subscribe((data) => { + result = data; + }); + + tick(); + + expect(result).toBeFalse(); + expect(halServiceSpy.getRootHref).toHaveBeenCalled(); + expect(restServiceSpy.get).toHaveBeenCalledWith(`${baseUrl}/signposting/links/${uuid}`); + })); }); diff --git a/src/app/core/data/signposting-data.service.ts b/src/app/core/data/signposting-data.service.ts index 5e965d1ab5..efdf80a381 100644 --- a/src/app/core/data/signposting-data.service.ts +++ b/src/app/core/data/signposting-data.service.ts @@ -1,8 +1,6 @@ import { Injectable } from '@angular/core'; import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; -// import { HttpHeaders, HttpResponse } from '@angular/common/http'; import { catchError, map } from 'rxjs/operators'; -// import { throwError } from 'rxjs'; import { Observable, of as observableOf } from 'rxjs'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; @@ -14,9 +12,10 @@ import { SignpostingLinks } from './signposting-links.model'; }) export class SignpostingDataService { - constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { } + constructor(private restService: DspaceRestService, protected halService: HALEndpointService) { + } - getLinks(uuid: string): Observable { + getLinks(uuid: string): Observable { const baseUrl = this.halService.getRootHref().replace('/api', ''); return this.restService.get(`${baseUrl}/signposting/links/${uuid}`).pipe( @@ -24,7 +23,7 @@ export class SignpostingDataService { console.error(err); return observableOf(false); }), - map((res: SignpostingLinks) => res) + map((res: RawRestResponse) => res.payload as SignpostingLinks[]) ); } diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 553b437d71..3c5b4adc0c 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -8,12 +8,7 @@ import { Observable, of as observableOf, of } from 'rxjs'; import { RemoteData } from '../data/remote-data'; import { Item } from '../shared/item.model'; -import { - ItemMock, - MockBitstream1, - MockBitstream3, - MockBitstream2 -} from '../../shared/mocks/item.mock'; +import { ItemMock, MockBitstream1, MockBitstream2, MockBitstream3 } from '../../shared/mocks/item.mock'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { PaginatedList } from '../data/paginated-list.model'; import { Bitstream } from '../shared/bitstream.model'; @@ -30,6 +25,7 @@ import { getMockStore } from '@ngrx/store/testing'; import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions'; import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; import { AppConfig } from '../../../config/app-config.interface'; +import { SignpostingDataService } from '../data/signposting-data.service'; describe('MetadataService', () => { let metadataService: MetadataService; @@ -46,6 +42,7 @@ describe('MetadataService', () => { let translateService: TranslateService; let hardRedirectService: HardRedirectService; let authorizationService: AuthorizationDataService; + let signpostingDataService: SignpostingDataService; let router: Router; let store; @@ -53,7 +50,12 @@ describe('MetadataService', () => { let appConfig: AppConfig; const initialState = { 'core': { metaTag: { tagsInUse: ['title', 'description'] }}}; - + const mocklink = { + href: 'http://test.org', + rel: 'test', + type: 'test' + }; + const document: any = jasmine.createSpyObj('document', ['head', 'createElement']); beforeEach(() => { rootService = jasmine.createSpyObj({ @@ -90,6 +92,10 @@ describe('MetadataService', () => { isAuthorized: observableOf(true) }); + signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { + getLinks: observableOf([mocklink]) + }); + // @ts-ignore store = getMockStore({ initialState }); spyOn(store, 'dispatch'); @@ -115,7 +121,9 @@ describe('MetadataService', () => { store, hardRedirectService, appConfig, - authorizationService + document, + authorizationService, + signpostingDataService ); }); diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 60409f17a1..c22f14b680 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Inject } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; import { Meta, MetaDefinition, Title } from '@angular/platform-browser'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; @@ -8,12 +8,12 @@ import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, combineLatest, - Observable, - of as observableOf, concat as observableConcat, - EMPTY + EMPTY, + Observable, + of as observableOf } from 'rxjs'; -import { filter, map, switchMap, take, mergeMap } from 'rxjs/operators'; +import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators'; import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; import { DSONameService } from '../breadcrumbs/dso-name.service'; @@ -25,10 +25,7 @@ import { BitstreamFormat } from '../shared/bitstream-format.model'; import { Bitstream } from '../shared/bitstream.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { Item } from '../shared/item.model'; -import { - getFirstCompletedRemoteData, - getFirstSucceededRemoteDataPayload -} from '../shared/operators'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../shared/operators'; import { RootDataService } from '../data/root-data.service'; import { getBitstreamDownloadRoute } from '../../app-routing-paths'; import { BundleDataService } from '../data/bundle-data.service'; @@ -46,6 +43,7 @@ import { AuthorizationDataService } from '../data/feature-authorization/authoriz import { getDownloadableBitstream } from '../shared/bitstream.operators'; import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface'; import { SignpostingDataService } from '../data/signposting-data.service'; +import { DOCUMENT } from '@angular/common'; /** * The base selector function to select the metaTag section in the store @@ -102,8 +100,9 @@ export class MetadataService { private store: Store, private hardRedirectService: HardRedirectService, @Inject(APP_CONFIG) private appConfig: AppConfig, + @Inject(DOCUMENT) private _document: Document, private authorizationService: AuthorizationDataService, - private signpostginDataService: SignpostingDataService + private signpostingDataService: SignpostingDataService ) { } @@ -198,9 +197,9 @@ export class MetadataService { */ private setSignpostingLinks() { if (this.currentObject.value instanceof Item){ - const value = this.signpostginDataService.getLinks(this.currentObject.getValue().id); + const value = this.signpostingDataService.getLinks(this.currentObject.getValue().id); value.subscribe(links => { - links.payload.forEach(link => { + links.forEach(link => { this.setLinkTag(link.href, link.rel, link.type); }); }); @@ -208,17 +207,19 @@ export class MetadataService { } setLinkTag(href: string, rel: string, type: string){ - let link: HTMLLinkElement = document.createElement('link'); - link.href = href; - link.rel = rel; - link.type = type; - document.head.appendChild(link); - linkTags.push(link); + let link: HTMLLinkElement = this._document.createElement('link'); + if (link) { + link.href = href; + link.rel = rel; + link.type = type; + this._document.head?.appendChild(link); + linkTags.push(link); + } } public clearLinkTags(){ linkTags.forEach(link => { - link.parentNode.removeChild(link); + link.parentNode?.removeChild(link); }); } diff --git a/src/app/item-page/full/full-item-page.component.spec.ts b/src/app/item-page/full/full-item-page.component.spec.ts index 53e36be1d1..6c59ccbc67 100644 --- a/src/app/item-page/full/full-item-page.component.spec.ts +++ b/src/app/item-page/full/full-item-page.component.spec.ts @@ -11,7 +11,7 @@ import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { VarDirective } from '../../shared/utils/var.directive'; import { RouterTestingModule } from '@angular/router/testing'; import { Item } from '../../core/shared/item.model'; -import { BehaviorSubject, of as observableOf } from 'rxjs'; +import { BehaviorSubject, of as observableOf, of } from 'rxjs'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { By } from '@angular/platform-browser'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; @@ -20,6 +20,8 @@ import { createPaginatedList } from '../../shared/testing/utils.test'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { createRelationshipsObservable } from '../simple/item-types/shared/item.component.spec'; import { RemoteData } from '../../core/data/remote-data'; +import { ServerResponseService } from '../../core/services/server-response.service'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -55,8 +57,8 @@ describe('FullItemPageComponent', () => { let routeStub: ActivatedRouteStub; let routeData; let authorizationDataService: AuthorizationDataService; - - + let serverResponseService: jasmine.SpyObj; + let signpostingDataService: jasmine.SpyObj; beforeEach(waitForAsync(() => { authService = jasmine.createSpyObj('authService', { @@ -76,6 +78,14 @@ describe('FullItemPageComponent', () => { isAuthorized: observableOf(false), }); + serverResponseService = jasmine.createSpyObj('ServerResponseService', { + setLinksetsHeader: jasmine.createSpy('setLinksetsHeader'), + }); + + signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { + getLinksets: of('test'), + }); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot({ loader: { @@ -90,8 +100,9 @@ describe('FullItemPageComponent', () => { { provide: MetadataService, useValue: metadataServiceStub }, { provide: AuthService, useValue: authService }, { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: ServerResponseService, useValue: serverResponseService }, + { provide: SignpostingDataService, useValue: signpostingDataService }, ], - schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(FullItemPageComponent, { set: { changeDetection: ChangeDetectionStrategy.Default } diff --git a/src/app/item-page/full/full-item-page.component.ts b/src/app/item-page/full/full-item-page.component.ts index 44766bac7b..2570bf70fe 100644 --- a/src/app/item-page/full/full-item-page.component.ts +++ b/src/app/item-page/full/full-item-page.component.ts @@ -19,7 +19,6 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/ import { ServerResponseService } from 'src/app/core/services/server-response.service'; import { SignpostingDataService } from 'src/app/core/data/signposting-data.service'; - /** * This component renders a full item page. * The route parameter 'id' is used to request the item it represents. @@ -52,8 +51,8 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit, authorizationService: AuthorizationDataService, private _location: Location, responseService: ServerResponseService, - signpostginDataService: SignpostingDataService) { - super(route, router, items, authService, authorizationService, responseService, signpostginDataService); + signpostingDataService: SignpostingDataService) { + super(route, router, items, authService, authorizationService, responseService, signpostingDataService); } /*** AoT inheritance fix, will hopefully be resolved in the near future **/ diff --git a/src/app/item-page/simple/item-page.component.spec.ts b/src/app/item-page/simple/item-page.component.spec.ts index 9b0e87939d..042cac2724 100644 --- a/src/app/item-page/simple/item-page.component.spec.ts +++ b/src/app/item-page/simple/item-page.component.spec.ts @@ -22,6 +22,9 @@ import { import { AuthService } from '../../core/auth/auth.service'; import { createPaginatedList } from '../../shared/testing/utils.test'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { of } from 'rxjs/internal/observable/of'; +import { ServerResponseService } from '../../core/services/server-response.service'; +import { SignpostingDataService } from '../../core/data/signposting-data.service'; const mockItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), @@ -41,6 +44,8 @@ describe('ItemPageComponent', () => { let fixture: ComponentFixture; let authService: AuthService; let authorizationDataService: AuthorizationDataService; + let serverResponseService: jasmine.SpyObj; + let signpostingDataService: jasmine.SpyObj; const mockMetadataService = { /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ @@ -60,6 +65,13 @@ describe('ItemPageComponent', () => { authorizationDataService = jasmine.createSpyObj('authorizationDataService', { isAuthorized: observableOf(false), }); + serverResponseService = jasmine.createSpyObj('ServerResponseService', { + setLinksetsHeader: jasmine.createSpy('setLinksetsHeader'), + }); + + signpostingDataService = jasmine.createSpyObj('SignpostingDataService', { + getLinksets: of('test'), + }); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot({ @@ -76,6 +88,8 @@ describe('ItemPageComponent', () => { { provide: Router, useValue: {} }, { provide: AuthService, useValue: authService }, { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: ServerResponseService, useValue: serverResponseService }, + { provide: SignpostingDataService, useValue: signpostingDataService }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index 058cbc667a..6483769483 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -66,10 +66,10 @@ export class ItemPageComponent implements OnInit { private authService: AuthService, private authorizationService: AuthorizationDataService, private responseService: ServerResponseService, - private signpostginDataService: SignpostingDataService + private signpostingDataService: SignpostingDataService ) { this.route.params.subscribe(params => { - this.signpostginDataService.getLinksets(params.id).subscribe(linksets => { + this.signpostingDataService.getLinksets(params.id).subscribe(linksets => { this.responseService.setLinksetsHeader(linksets); }); });