diff --git a/src/app/bitstream-page/bitstream-page-routes.ts b/src/app/bitstream-page/bitstream-page-routes.ts index 73848a7f4e8..1a6e3ebc3df 100644 --- a/src/app/bitstream-page/bitstream-page-routes.ts +++ b/src/app/bitstream-page/bitstream-page-routes.ts @@ -11,7 +11,7 @@ import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bit import { BitstreamDownloadPageComponent } from './bitstream-download-page/bitstream-download-page.component'; import { bitstreamPageResolver } from './bitstream-page.resolver'; import { ThemedEditBitstreamPageComponent } from './edit-bitstream-page/themed-edit-bitstream-page.component'; -import { legacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver'; +import { legacyBitstreamURLRedirectGuard } from './legacy-bitstream-url-redirect.guard'; const EDIT_BITSTREAM_PATH = ':id/edit'; const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations'; @@ -23,18 +23,12 @@ export const ROUTES: Route[] = [ { // Resolve XMLUI bitstream download URLs path: 'handle/:prefix/:suffix/:filename', - component: BitstreamDownloadPageComponent, - resolve: { - bitstream: legacyBitstreamUrlResolver, - }, + canActivate: [legacyBitstreamURLRedirectGuard], }, { // Resolve JSPUI bitstream download URLs path: ':prefix/:suffix/:sequence_id/:filename', - component: BitstreamDownloadPageComponent, - resolve: { - bitstream: legacyBitstreamUrlResolver, - }, + canActivate: [legacyBitstreamURLRedirectGuard], }, { // Resolve angular bitstream download URLs diff --git a/src/app/bitstream-page/legacy-bitstream-url-redirect.guard.spec.ts b/src/app/bitstream-page/legacy-bitstream-url-redirect.guard.spec.ts new file mode 100644 index 00000000000..1eb0e00b85f --- /dev/null +++ b/src/app/bitstream-page/legacy-bitstream-url-redirect.guard.spec.ts @@ -0,0 +1,158 @@ +import { cold } from 'jasmine-marbles'; +import { EMPTY } from 'rxjs'; + +import { PAGE_NOT_FOUND_PATH } from '../app-routing-paths'; +import { BitstreamDataService } from '../core/data/bitstream-data.service'; +import { RemoteData } from '../core/data/remote-data'; +import { RequestEntryState } from '../core/data/request-entry-state.model'; +import { BrowserHardRedirectService } from '../core/services/browser-hard-redirect.service'; +import { HardRedirectService } from '../core/services/hard-redirect.service'; +import { Bitstream } from '../core/shared/bitstream.model'; +import { RouterStub } from '../shared/testing/router.stub'; +import { legacyBitstreamURLRedirectGuard } from './legacy-bitstream-url-redirect.guard'; + +describe('legacyBitstreamURLRedirectGuard', () => { + let resolver: any; + let bitstreamDataService: BitstreamDataService; + let remoteDataMocks: { [type: string]: RemoteData }; + let route; + let state; + let hardRedirectService: HardRedirectService; + let router: RouterStub; + + let bitstream: Bitstream; + + beforeEach(() => { + route = { + params: {}, + queryParams: {}, + }; + router = new RouterStub(); + hardRedirectService = new BrowserHardRedirectService(window.location); + state = {}; + bitstream = Object.assign(new Bitstream(), { + uuid: 'bitstream-id', + }); + remoteDataMocks = { + RequestPending: new RemoteData(undefined, 0, 0, RequestEntryState.RequestPending, undefined, undefined, undefined), + ResponsePending: new RemoteData(undefined, 0, 0, RequestEntryState.ResponsePending, undefined, undefined, undefined), + Success: new RemoteData(0, 0, 0, RequestEntryState.Success, undefined, bitstream, 200), + NoContent: new RemoteData(0, 0, 0, RequestEntryState.Success, undefined, undefined, 204), + Error: new RemoteData(0, 0, 0, RequestEntryState.Error, 'Internal server error', undefined, 500), + }; + bitstreamDataService = { + findByItemHandle: () => undefined, + } as any; + resolver = legacyBitstreamURLRedirectGuard; + }); + + describe(`resolve`, () => { + describe(`For JSPUI-style URLs`, () => { + beforeEach(() => { + spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(EMPTY); + route = Object.assign({}, route, { + params: { + prefix: '123456789', + suffix: '1234', + filename: 'some-file.pdf', + sequence_id: '5', + }, + }); + }); + it(`should call findByItemHandle with the handle, sequence id, and filename from the route`, () => { + resolver(route, state, bitstreamDataService, hardRedirectService, router); + expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( + `${route.params.prefix}/${route.params.suffix}`, + route.params.sequence_id, + route.params.filename, + ); + }); + }); + + describe(`For XMLUI-style URLs`, () => { + describe(`when there is a sequenceId query parameter`, () => { + beforeEach(() => { + spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(EMPTY); + route = Object.assign({}, route, { + params: { + prefix: '123456789', + suffix: '1234', + filename: 'some-file.pdf', + }, + queryParams: { + sequenceId: '5', + }, + }); + }); + it(`should call findByItemHandle with the handle and filename from the route, and the sequence ID from the queryParams`, () => { + resolver(route, state, bitstreamDataService, hardRedirectService, router); + expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( + `${route.params.prefix}/${route.params.suffix}`, + route.queryParams.sequenceId, + route.params.filename, + ); + }); + }); + describe(`when there's no sequenceId query parameter`, () => { + beforeEach(() => { + spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(EMPTY); + route = Object.assign({}, route, { + params: { + prefix: '123456789', + suffix: '1234', + filename: 'some-file.pdf', + }, + }); + }); + it(`should call findByItemHandle with the handle, and filename from the route`, () => { + resolver(route, state, bitstreamDataService, hardRedirectService, router); + expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( + `${route.params.prefix}/${route.params.suffix}`, + undefined, + route.params.filename, + ); + }); + }); + }); + describe('should return and complete after the RemoteData has...', () => { + it('...failed', () => { + spyOn(router, 'createUrlTree').and.callThrough(); + spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(cold('a-b-c', { + a: remoteDataMocks.RequestPending, + b: remoteDataMocks.ResponsePending, + c: remoteDataMocks.Error, + })); + resolver(route, state, bitstreamDataService, hardRedirectService, router).subscribe(() => { + expect(bitstreamDataService.findByItemHandle).toHaveBeenCalled(); + expect(router.createUrlTree).toHaveBeenCalledWith([PAGE_NOT_FOUND_PATH]); + }); + }); + + it('...succeeded without content', () => { + spyOn(router, 'createUrlTree').and.callThrough(); + spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(cold('a-b-c', { + a: remoteDataMocks.RequestPending, + b: remoteDataMocks.ResponsePending, + c: remoteDataMocks.NoContent, + })); + resolver(route, state, bitstreamDataService, hardRedirectService, router).subscribe(() => { + expect(bitstreamDataService.findByItemHandle).toHaveBeenCalled(); + expect(router.createUrlTree).toHaveBeenCalledWith([PAGE_NOT_FOUND_PATH]); + }); + }); + + it('...succeeded', () => { + spyOn(hardRedirectService, 'redirect'); + spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(cold('a-b-c', { + a: remoteDataMocks.RequestPending, + b: remoteDataMocks.ResponsePending, + c: remoteDataMocks.Success, + })); + resolver(route, state, bitstreamDataService, hardRedirectService, router).subscribe(() => { + expect(bitstreamDataService.findByItemHandle).toHaveBeenCalled(); + expect(hardRedirectService.redirect).toHaveBeenCalledWith(new URL(`/bitstreams/${bitstream.uuid}/download`, window.location.origin).href, 301); + }); + }); + }); + }); +}); diff --git a/src/app/bitstream-page/legacy-bitstream-url-redirect.guard.ts b/src/app/bitstream-page/legacy-bitstream-url-redirect.guard.ts new file mode 100644 index 00000000000..78403ed7e3f --- /dev/null +++ b/src/app/bitstream-page/legacy-bitstream-url-redirect.guard.ts @@ -0,0 +1,57 @@ +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivateFn, + Router, + RouterStateSnapshot, + UrlTree, +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { PAGE_NOT_FOUND_PATH } from '../app-routing-paths'; +import { BitstreamDataService } from '../core/data/bitstream-data.service'; +import { RemoteData } from '../core/data/remote-data'; +import { HardRedirectService } from '../core/services/hard-redirect.service'; +import { Bitstream } from '../core/shared/bitstream.model'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; +import { hasNoValue } from '../shared/empty.util'; + +/** + * Redirects to a bitstream based on the handle of the item, and the sequence id or the filename of the + * bitstream. In production mode the status code will also be set the status code to 301 marking it as a permanent URL + * redirect for bots to the regular bitstream download Page. + * + * @returns Either a {@link UrlTree} to the 404 page when the url isn't a valid format or false in order to make the + * user wait until the {@link HardRedirectService#redirect} was performed + */ +export const legacyBitstreamURLRedirectGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + bitstreamDataService: BitstreamDataService = inject(BitstreamDataService), + serverHardRedirectService: HardRedirectService = inject(HardRedirectService), + router: Router = inject(Router), +): Observable => { + const prefix = route.params.prefix; + const suffix = route.params.suffix; + const filename = route.params.filename; + let sequenceId = route.params.sequence_id; + if (hasNoValue(sequenceId)) { + sequenceId = route.queryParams.sequenceId; + } + return bitstreamDataService.findByItemHandle( + `${prefix}/${suffix}`, + sequenceId, + filename, + ).pipe( + getFirstCompletedRemoteData(), + map((rd: RemoteData) => { + if (rd.hasSucceeded && !rd.hasNoContent) { + serverHardRedirectService.redirect(new URL(`/bitstreams/${rd.payload.uuid}/download`, serverHardRedirectService.getCurrentOrigin()).href, 301); + return false; + } else { + return router.createUrlTree([PAGE_NOT_FOUND_PATH]); + } + }), + ); +}; diff --git a/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts b/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts deleted file mode 100644 index ec67d530df7..00000000000 --- a/src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { EMPTY } from 'rxjs'; -import { TestScheduler } from 'rxjs/testing'; - -import { BitstreamDataService } from '../core/data/bitstream-data.service'; -import { RemoteData } from '../core/data/remote-data'; -import { RequestEntryState } from '../core/data/request-entry-state.model'; -import { legacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver'; - -describe(`legacyBitstreamUrlResolver`, () => { - let resolver: any; - let bitstreamDataService: BitstreamDataService; - let testScheduler; - let remoteDataMocks; - let route; - let state; - - beforeEach(() => { - testScheduler = new TestScheduler((actual, expected) => { - expect(actual).toEqual(expected); - }); - - route = { - params: {}, - queryParams: {}, - }; - state = {}; - remoteDataMocks = { - RequestPending: new RemoteData(undefined, 0, 0, RequestEntryState.RequestPending, undefined, undefined, undefined), - ResponsePending: new RemoteData(undefined, 0, 0, RequestEntryState.ResponsePending, undefined, undefined, undefined), - Success: new RemoteData(0, 0, 0, RequestEntryState.Success, undefined, {}, 200), - Error: new RemoteData(0, 0, 0, RequestEntryState.Error, 'Internal server error', undefined, 500), - }; - bitstreamDataService = { - findByItemHandle: () => undefined, - } as any; - resolver = legacyBitstreamUrlResolver; - }); - - describe(`resolve`, () => { - describe(`For JSPUI-style URLs`, () => { - beforeEach(() => { - spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(EMPTY); - route = Object.assign({}, route, { - params: { - prefix: '123456789', - suffix: '1234', - filename: 'some-file.pdf', - sequence_id: '5', - }, - }); - }); - it(`should call findByItemHandle with the handle, sequence id, and filename from the route`, () => { - testScheduler.run(() => { - resolver(route, state, bitstreamDataService); - expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( - `${route.params.prefix}/${route.params.suffix}`, - route.params.sequence_id, - route.params.filename, - ); - }); - }); - }); - - describe(`For XMLUI-style URLs`, () => { - describe(`when there is a sequenceId query parameter`, () => { - beforeEach(() => { - spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(EMPTY); - route = Object.assign({}, route, { - params: { - prefix: '123456789', - suffix: '1234', - filename: 'some-file.pdf', - }, - queryParams: { - sequenceId: '5', - }, - }); - }); - it(`should call findByItemHandle with the handle and filename from the route, and the sequence ID from the queryParams`, () => { - testScheduler.run(() => { - resolver(route, state, bitstreamDataService); - expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( - `${route.params.prefix}/${route.params.suffix}`, - route.queryParams.sequenceId, - route.params.filename, - ); - }); - }); - }); - describe(`when there's no sequenceId query parameter`, () => { - beforeEach(() => { - spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(EMPTY); - route = Object.assign({}, route, { - params: { - prefix: '123456789', - suffix: '1234', - filename: 'some-file.pdf', - }, - }); - }); - it(`should call findByItemHandle with the handle, and filename from the route`, () => { - testScheduler.run(() => { - resolver(route, state, bitstreamDataService); - expect(bitstreamDataService.findByItemHandle).toHaveBeenCalledWith( - `${route.params.prefix}/${route.params.suffix}`, - undefined, - route.params.filename, - ); - }); - }); - }); - }); - describe(`should return and complete after the remotedata has...`, () => { - it(`...failed`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(cold('a-b-c', { - a: remoteDataMocks.RequestPending, - b: remoteDataMocks.ResponsePending, - c: remoteDataMocks.Error, - })); - const expected = '----(c|)'; - const values = { - c: remoteDataMocks.Error, - }; - - expectObservable(resolver(route, state, bitstreamDataService)).toBe(expected, values); - }); - }); - it(`...succeeded`, () => { - testScheduler.run(({ cold, expectObservable }) => { - spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(cold('a-b-c', { - a: remoteDataMocks.RequestPending, - b: remoteDataMocks.ResponsePending, - c: remoteDataMocks.Success, - })); - const expected = '----(c|)'; - const values = { - c: remoteDataMocks.Success, - }; - - expectObservable(resolver(route, state, bitstreamDataService)).toBe(expected, values); - }); - }); - }); - }); -}); diff --git a/src/app/bitstream-page/legacy-bitstream-url.resolver.ts b/src/app/bitstream-page/legacy-bitstream-url.resolver.ts deleted file mode 100644 index 8b9b1127b13..00000000000 --- a/src/app/bitstream-page/legacy-bitstream-url.resolver.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { inject } from '@angular/core'; -import { - ActivatedRouteSnapshot, - ResolveFn, - RouterStateSnapshot, -} from '@angular/router'; -import { Observable } from 'rxjs'; - -import { BitstreamDataService } from '../core/data/bitstream-data.service'; -import { RemoteData } from '../core/data/remote-data'; -import { Bitstream } from '../core/shared/bitstream.model'; -import { getFirstCompletedRemoteData } from '../core/shared/operators'; -import { hasNoValue } from '../shared/empty.util'; - -/** - * Resolve a bitstream based on the handle of the item, and the sequence id or the filename of the - * bitstream - * - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @param {BitstreamDataService} bitstreamDataService - * @returns Observable<> Emits the found bitstream based on the parameters in - * current route, or an error if something went wrong - */ -export const legacyBitstreamUrlResolver: ResolveFn> = ( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot, - bitstreamDataService: BitstreamDataService = inject(BitstreamDataService), -): Observable> => { - const prefix = route.params.prefix; - const suffix = route.params.suffix; - const filename = route.params.filename; - - let sequenceId = route.params.sequence_id; - if (hasNoValue(sequenceId)) { - sequenceId = route.queryParams.sequenceId; - } - - return bitstreamDataService.findByItemHandle( - `${prefix}/${suffix}`, - sequenceId, - filename, - ).pipe( - getFirstCompletedRemoteData(), - ); -}; diff --git a/src/app/shared/testing/server-response-service.stub.ts b/src/app/shared/testing/server-response-service.stub.ts new file mode 100644 index 00000000000..fd6e4a0d240 --- /dev/null +++ b/src/app/shared/testing/server-response-service.stub.ts @@ -0,0 +1,30 @@ +/* eslint-disable no-empty,@typescript-eslint/no-empty-function */ +/** + * Stub class of {@link ServerResponseService} + */ +export class ServerResponseServiceStub { + + setStatus(_code: number, _message?: string): this { + return this; + } + + setUnauthorized(_message = 'Unauthorized'): this { + return this; + } + + setForbidden(_message = 'Forbidden'): this { + return this; + } + + setNotFound(_message = 'Not found'): this { + return this; + } + + setInternalServerError(_message = 'Internal Server Error'): this { + return this; + } + + setHeader(_header: string, _content: string): void { + } + +}