mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 18:44:14 +00:00
80141: Add support for legacy bitstream download URLs
This commit is contained in:
@@ -9,6 +9,7 @@ import { ResourcePolicyCreateComponent } from '../shared/resource-policies/creat
|
|||||||
import { ResourcePolicyResolver } from '../shared/resource-policies/resolvers/resource-policy.resolver';
|
import { ResourcePolicyResolver } from '../shared/resource-policies/resolvers/resource-policy.resolver';
|
||||||
import { ResourcePolicyEditComponent } from '../shared/resource-policies/edit/resource-policy-edit.component';
|
import { ResourcePolicyEditComponent } from '../shared/resource-policies/edit/resource-policy-edit.component';
|
||||||
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
|
import { BitstreamAuthorizationsComponent } from './bitstream-authorizations/bitstream-authorizations.component';
|
||||||
|
import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver';
|
||||||
|
|
||||||
const EDIT_BITSTREAM_PATH = ':id/edit';
|
const EDIT_BITSTREAM_PATH = ':id/edit';
|
||||||
const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
|
const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
|
||||||
@@ -20,7 +21,24 @@ const EDIT_BITSTREAM_AUTHORIZATIONS_PATH = ':id/authorizations';
|
|||||||
imports: [
|
imports: [
|
||||||
RouterModule.forChild([
|
RouterModule.forChild([
|
||||||
{
|
{
|
||||||
path:':id/download',
|
// Resolve XMLUI bitstream download URLs
|
||||||
|
path: 'handle/:prefix/:suffix/:filename',
|
||||||
|
component: BitstreamDownloadPageComponent,
|
||||||
|
resolve: {
|
||||||
|
bitstream: LegacyBitstreamUrlResolver
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Resolve JSPUI bitstream download URLs
|
||||||
|
path: ':prefix/:suffix/:sequence_id/:filename',
|
||||||
|
component: BitstreamDownloadPageComponent,
|
||||||
|
resolve: {
|
||||||
|
bitstream: LegacyBitstreamUrlResolver
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Resolve angular bitstream download URLs
|
||||||
|
path: ':id/download',
|
||||||
component: BitstreamDownloadPageComponent,
|
component: BitstreamDownloadPageComponent,
|
||||||
resolve: {
|
resolve: {
|
||||||
bitstream: BitstreamPageResolver
|
bitstream: BitstreamPageResolver
|
||||||
|
145
src/app/+bitstream-page/legacy-bitstream-url.resolver.spec.ts
Normal file
145
src/app/+bitstream-page/legacy-bitstream-url.resolver.spec.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { LegacyBitstreamUrlResolver } from './legacy-bitstream-url.resolver';
|
||||||
|
import { of as observableOf, EMPTY } from 'rxjs';
|
||||||
|
import { BitstreamDataService } from '../core/data/bitstream-data.service';
|
||||||
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
import { RequestEntryState } from '../core/data/request.reducer';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
|
describe(`LegacyBitstreamUrlResolver`, () => {
|
||||||
|
let resolver: LegacyBitstreamUrlResolver;
|
||||||
|
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 = new LegacyBitstreamUrlResolver(bitstreamDataService);
|
||||||
|
});
|
||||||
|
|
||||||
|
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.resolve(route, state);
|
||||||
|
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.resolve(route, state);
|
||||||
|
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.resolve(route, state);
|
||||||
|
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.resolve(route, state)).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.resolve(route, state)).toBe(expected, values);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
48
src/app/+bitstream-page/legacy-bitstream-url.resolver.ts
Normal file
48
src/app/+bitstream-page/legacy-bitstream-url.resolver.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
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';
|
||||||
|
import { BitstreamDataService } from '../core/data/bitstream-data.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class resolves a bitstream based on the DSpace 6 XMLUI or JSPUI bitstream download URLs
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class LegacyBitstreamUrlResolver implements Resolve<RemoteData<Bitstream>> {
|
||||||
|
constructor(protected bitstreamDataService: BitstreamDataService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* @returns Observable<<RemoteData<Item>> Emits the found bitstream based on the parameters in
|
||||||
|
* current route, or an error if something went wrong
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
|
||||||
|
Observable<RemoteData<Bitstream>> {
|
||||||
|
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 this.bitstreamDataService.findByItemHandle(
|
||||||
|
`${prefix}/${suffix}`,
|
||||||
|
sequenceId,
|
||||||
|
filename,
|
||||||
|
).pipe(
|
||||||
|
getFirstCompletedRemoteData()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -10,6 +10,11 @@ import { URLCombiner } from './core/url-combiner/url-combiner';
|
|||||||
|
|
||||||
export const BITSTREAM_MODULE_PATH = 'bitstreams';
|
export const BITSTREAM_MODULE_PATH = 'bitstreams';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The bitstream module path to resolve XMLUI and JSPUI bitstream download URLs
|
||||||
|
*/
|
||||||
|
export const LEGACY_BITSTREAM_MODULE_PATH = 'bitstream';
|
||||||
|
|
||||||
export function getBitstreamModuleRoute() {
|
export function getBitstreamModuleRoute() {
|
||||||
return `/${BITSTREAM_MODULE_PATH}`;
|
return `/${BITSTREAM_MODULE_PATH}`;
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,7 @@ import {
|
|||||||
PROFILE_MODULE_PATH,
|
PROFILE_MODULE_PATH,
|
||||||
REGISTER_PATH,
|
REGISTER_PATH,
|
||||||
WORKFLOW_ITEM_MODULE_PATH,
|
WORKFLOW_ITEM_MODULE_PATH,
|
||||||
|
LEGACY_BITSTREAM_MODULE_PATH,
|
||||||
} from './app-routing-paths';
|
} from './app-routing-paths';
|
||||||
import { COLLECTION_MODULE_PATH } from './+collection-page/collection-page-routing-paths';
|
import { COLLECTION_MODULE_PATH } from './+collection-page/collection-page-routing-paths';
|
||||||
import { COMMUNITY_MODULE_PATH } from './+community-page/community-page-routing-paths';
|
import { COMMUNITY_MODULE_PATH } from './+community-page/community-page-routing-paths';
|
||||||
@@ -93,6 +94,12 @@ import { GroupAdministratorGuard } from './core/data/feature-authorization/featu
|
|||||||
.then((m) => m.ItemPageModule),
|
.then((m) => m.ItemPageModule),
|
||||||
canActivate: [EndUserAgreementCurrentUserGuard]
|
canActivate: [EndUserAgreementCurrentUserGuard]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: LEGACY_BITSTREAM_MODULE_PATH,
|
||||||
|
loadChildren: () => import('./+bitstream-page/bitstream-page.module')
|
||||||
|
.then((m) => m.BitstreamPageModule),
|
||||||
|
canActivate: [EndUserAgreementCurrentUserGuard]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: BITSTREAM_MODULE_PATH,
|
path: BITSTREAM_MODULE_PATH,
|
||||||
loadChildren: () => import('./+bitstream-page/bitstream-page.module')
|
loadChildren: () => import('./+bitstream-page/bitstream-page.module')
|
||||||
|
@@ -28,6 +28,7 @@ import { HttpOptions } from '../dspace-rest/dspace-rest.service';
|
|||||||
import { sendRequest } from '../shared/operators';
|
import { sendRequest } from '../shared/operators';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { RequestParam } from '../cache/models/request-param.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service to retrieve {@link Bitstream}s from the REST API
|
* A service to retrieve {@link Bitstream}s from the REST API
|
||||||
@@ -136,4 +137,50 @@ export class BitstreamDataService extends DataService<Bitstream> {
|
|||||||
return this.rdbService.buildFromRequestUUID(requestId);
|
return this.rdbService.buildFromRequestUUID(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable of {@link RemoteData} of a {@link Bitstream}, based on a handle and an
|
||||||
|
* optional sequenceId or filename, with a list of {@link FollowLinkConfig}, to automatically
|
||||||
|
* resolve {@link HALLink}s of the object
|
||||||
|
*
|
||||||
|
* @param handle The handle of the bitstream we want to retrieve
|
||||||
|
* @param sequenceId The sequence id of the bitstream we want to retrieve
|
||||||
|
* @param filename The filename of the bitstream we want to retrieve
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
findByItemHandle(
|
||||||
|
handle: string,
|
||||||
|
sequenceId?: string,
|
||||||
|
filename?: string,
|
||||||
|
useCachedVersionIfAvailable = true,
|
||||||
|
reRequestOnStale = true,
|
||||||
|
...linksToFollow: FollowLinkConfig<Bitstream>[]
|
||||||
|
): Observable<RemoteData<Bitstream>> {
|
||||||
|
const searchParams = [];
|
||||||
|
searchParams.push(new RequestParam('handle', handle));
|
||||||
|
if (hasValue(sequenceId)) {
|
||||||
|
searchParams.push(new RequestParam('sequenceId', sequenceId));
|
||||||
|
}
|
||||||
|
if (hasValue(filename)) {
|
||||||
|
searchParams.push(new RequestParam('filename', filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
const hrefObs = this.getSearchByHref(
|
||||||
|
'byItemHandle',
|
||||||
|
{ searchParams },
|
||||||
|
...linksToFollow
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.findByHref(
|
||||||
|
hrefObs,
|
||||||
|
useCachedVersionIfAvailable,
|
||||||
|
reRequestOnStale,
|
||||||
|
...linksToFollow
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,12 @@ import {
|
|||||||
redirectOn4xx
|
redirectOn4xx
|
||||||
} from './operators';
|
} from './operators';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
import {
|
||||||
|
createFailedRemoteDataObject,
|
||||||
|
createSuccessfulRemoteDataObject
|
||||||
|
} from '../../shared/remote-data.utils';
|
||||||
|
|
||||||
|
// tslint:disable:no-shadowed-variable
|
||||||
|
|
||||||
describe('Core Module - RxJS Operators', () => {
|
describe('Core Module - RxJS Operators', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
@@ -172,8 +177,12 @@ describe('Core Module - RxJS Operators', () => {
|
|||||||
describe('redirectOn4xx', () => {
|
describe('redirectOn4xx', () => {
|
||||||
let router;
|
let router;
|
||||||
let authService;
|
let authService;
|
||||||
|
let testScheduler;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
testScheduler = new TestScheduler((actual, expected) => {
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
router = jasmine.createSpyObj('router', ['navigateByUrl']);
|
router = jasmine.createSpyObj('router', ['navigateByUrl']);
|
||||||
authService = jasmine.createSpyObj('authService', {
|
authService = jasmine.createSpyObj('authService', {
|
||||||
isAuthenticated: observableOf(true),
|
isAuthenticated: observableOf(true),
|
||||||
@@ -181,32 +190,69 @@ describe('Core Module - RxJS Operators', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call navigateByUrl to a 404 page, when the remote data contains a 404 error', () => {
|
it('should call navigateByUrl to a 404 page, when the remote data contains a 404 error, and not emit anything', () => {
|
||||||
const testRD = createFailedRemoteDataObject('Object was not found', 404);
|
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||||
|
const testRD = createFailedRemoteDataObject('Object was not found', 404);
|
||||||
|
const source = cold('a', { a: testRD });
|
||||||
|
const expected = '-';
|
||||||
|
const values = {};
|
||||||
|
|
||||||
observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
|
expectObservable(source.pipe(redirectOn4xx(router, authService))).toBe(expected, values);
|
||||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/404', { skipLocationChange: true });
|
flush();
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('/404', { skipLocationChange: true });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call navigateByUrl to a 401 page, when the remote data contains a 403 error', () => {
|
it('should call navigateByUrl to a 404 page, when the remote data contains a 422 error, and not emit anything', () => {
|
||||||
const testRD = createFailedRemoteDataObject('Forbidden', 403);
|
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||||
|
const testRD = createFailedRemoteDataObject('Unprocessable Entity', 422);
|
||||||
|
const source = cold('a', { a: testRD });
|
||||||
|
const expected = '-';
|
||||||
|
const values = {};
|
||||||
|
|
||||||
observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
|
expectObservable(source.pipe(redirectOn4xx(router, authService))).toBe(expected, values);
|
||||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/403', { skipLocationChange: true });
|
flush();
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('/404', { skipLocationChange: true });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not call navigateByUrl to a 404, 403 or 401 page, when the remote data contains another error than a 404, 403 or 401', () => {
|
it('should call navigateByUrl to a 401 page, when the remote data contains a 403 error, and not emit anything', () => {
|
||||||
const testRD = createFailedRemoteDataObject('Something went wrong', 500);
|
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||||
|
const testRD = createFailedRemoteDataObject('Forbidden', 403);
|
||||||
|
const source = cold('a', { a: testRD });
|
||||||
|
const expected = '-';
|
||||||
|
const values = {};
|
||||||
|
|
||||||
observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
|
expectObservable(source.pipe(redirectOn4xx(router, authService))).toBe(expected, values);
|
||||||
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
flush();
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('/403', { skipLocationChange: true });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not call navigateByUrl to a 404, 403 or 401 page, when the remote data contains no error', () => {
|
it('should not call navigateByUrl to a 404, 403 or 401 page, when the remote data contains another error than a 404, 422, 403 or 401, and emit the source rd', () => {
|
||||||
const testRD = createSuccessfulRemoteDataObject(undefined);
|
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||||
|
const testRD = createFailedRemoteDataObject('Something went wrong', 500);
|
||||||
|
const source = cold('a', { a: testRD });
|
||||||
|
const expected = 'a';
|
||||||
|
const values = { a: testRD };
|
||||||
|
|
||||||
observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
|
expectObservable(source.pipe(redirectOn4xx(router, authService))).toBe(expected, values);
|
||||||
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
flush();
|
||||||
|
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call navigateByUrl to a 404, 403 or 401 page, when the remote data contains no error, and emit the source rd', () => {
|
||||||
|
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||||
|
const testRD = createSuccessfulRemoteDataObject(undefined);
|
||||||
|
const source = cold('a', { a: testRD });
|
||||||
|
const expected = 'a';
|
||||||
|
const values = { a: testRD };
|
||||||
|
|
||||||
|
expectObservable(source.pipe(redirectOn4xx(router, authService))).toBe(expected, values);
|
||||||
|
flush();
|
||||||
|
expect(router.navigateByUrl).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the user is not authenticated', () => {
|
describe('when the user is not authenticated', () => {
|
||||||
@@ -214,20 +260,32 @@ describe('Core Module - RxJS Operators', () => {
|
|||||||
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(false));
|
(authService.isAuthenticated as jasmine.Spy).and.returnValue(observableOf(false));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the redirect url and navigate to login when the remote data contains a 401 error', () => {
|
it('should set the redirect url and navigate to login when the remote data contains a 401 error, and not emit anything', () => {
|
||||||
const testRD = createFailedRemoteDataObject('The current user is unauthorized', 401);
|
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||||
|
const testRD = createFailedRemoteDataObject('The current user is unauthorized', 401);
|
||||||
|
const source = cold('a', { a: testRD });
|
||||||
|
const expected = '-';
|
||||||
|
const values = {};
|
||||||
|
|
||||||
observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
|
expectObservable(source.pipe(redirectOn4xx(router, authService))).toBe(expected, values);
|
||||||
expect(authService.setRedirectUrl).toHaveBeenCalled();
|
flush();
|
||||||
expect(router.navigateByUrl).toHaveBeenCalledWith('login');
|
expect(authService.setRedirectUrl).toHaveBeenCalled();
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('login');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set the redirect url and navigate to login when the remote data contains a 403 error', () => {
|
it('should set the redirect url and navigate to login when the remote data contains a 403 error, and not emit anything', () => {
|
||||||
const testRD = createFailedRemoteDataObject('Forbidden', 403);
|
testScheduler.run(({ cold, expectObservable, flush }) => {
|
||||||
|
const testRD = createFailedRemoteDataObject('Forbidden', 403);
|
||||||
|
const source = cold('a', { a: testRD });
|
||||||
|
const expected = '-';
|
||||||
|
const values = {};
|
||||||
|
|
||||||
observableOf(testRD).pipe(redirectOn4xx(router, authService)).subscribe();
|
expectObservable(source.pipe(redirectOn4xx(router, authService))).toBe(expected, values);
|
||||||
expect(authService.setRedirectUrl).toHaveBeenCalled();
|
flush();
|
||||||
expect(router.navigateByUrl).toHaveBeenCalledWith('login');
|
expect(authService.setRedirectUrl).toHaveBeenCalled();
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('login');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,6 +1,17 @@
|
|||||||
import { Router, UrlTree } from '@angular/router';
|
import { Router, UrlTree } from '@angular/router';
|
||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
import { debounceTime, filter, find, map, mergeMap, switchMap, take, takeWhile, tap } from 'rxjs/operators';
|
import {
|
||||||
|
debounceTime,
|
||||||
|
filter,
|
||||||
|
find,
|
||||||
|
map,
|
||||||
|
mergeMap,
|
||||||
|
switchMap,
|
||||||
|
take,
|
||||||
|
takeWhile,
|
||||||
|
tap,
|
||||||
|
withLatestFrom
|
||||||
|
} from 'rxjs/operators';
|
||||||
import { hasNoValue, hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util';
|
import { hasNoValue, hasValue, hasValueOperator, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { SearchResult } from '../../shared/search/search-result.model';
|
import { SearchResult } from '../../shared/search/search-result.model';
|
||||||
import { PaginatedList } from '../data/paginated-list.model';
|
import { PaginatedList } from '../data/paginated-list.model';
|
||||||
@@ -22,6 +33,11 @@ export const DEBOUNCE_TIME_OPERATOR = new InjectionToken<<T>(dueTime: number) =>
|
|||||||
factory: () => debounceTime
|
factory: () => debounceTime
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const REDIRECT_ON_4XX = new InjectionToken<<T>(router: Router, authService: AuthService) => (source: Observable<RemoteData<T>>) => Observable<RemoteData<T>>>('redirectOn4xx', {
|
||||||
|
providedIn: 'root',
|
||||||
|
factory: () => redirectOn4xx
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This file contains custom RxJS operators that can be used in multiple places
|
* This file contains custom RxJS operators that can be used in multiple places
|
||||||
*/
|
*/
|
||||||
@@ -175,29 +191,37 @@ export const getAllSucceededRemoteListPayload = <T>() =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Operator that checks if a remote data object returned a 401 or 404 error
|
* Operator that checks if a remote data object returned a 4xx error
|
||||||
* When it does contain such an error, it will redirect the user to the related error page, without altering the current URL
|
* When it does contain such an error, it will redirect the user to the related error page, without
|
||||||
|
* altering the current URL
|
||||||
|
*
|
||||||
* @param router The router used to navigate to a new page
|
* @param router The router used to navigate to a new page
|
||||||
* @param authService Service to check if the user is authenticated
|
* @param authService Service to check if the user is authenticated
|
||||||
*/
|
*/
|
||||||
export const redirectOn4xx = <T>(router: Router, authService: AuthService) =>
|
export const redirectOn4xx = <T>(router: Router, authService: AuthService) =>
|
||||||
(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
|
||||||
observableCombineLatest(source, authService.isAuthenticated()).pipe(
|
source.pipe(
|
||||||
map(([rd, isAuthenticated]: [RemoteData<T>, boolean]) => {
|
withLatestFrom(authService.isAuthenticated()),
|
||||||
|
filter(([rd, isAuthenticated]: [RemoteData<T>, boolean]) => {
|
||||||
if (rd.hasFailed) {
|
if (rd.hasFailed) {
|
||||||
if (rd.statusCode === 404) {
|
if (rd.statusCode === 404 || rd.statusCode === 422) {
|
||||||
router.navigateByUrl(getPageNotFoundRoute(), {skipLocationChange: true});
|
router.navigateByUrl(getPageNotFoundRoute(), { skipLocationChange: true });
|
||||||
|
return false;
|
||||||
} else if (rd.statusCode === 403 || rd.statusCode === 401) {
|
} else if (rd.statusCode === 403 || rd.statusCode === 401) {
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
router.navigateByUrl(getForbiddenRoute(), {skipLocationChange: true});
|
router.navigateByUrl(getForbiddenRoute(), { skipLocationChange: true });
|
||||||
|
return false;
|
||||||
} else {
|
} else {
|
||||||
authService.setRedirectUrl(router.url);
|
authService.setRedirectUrl(router.url);
|
||||||
router.navigateByUrl('login');
|
router.navigateByUrl('login');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rd;
|
return true;
|
||||||
}));
|
}),
|
||||||
|
map(([rd,]: [RemoteData<T>, boolean]) => rd)
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Operator that returns a UrlTree to a forbidden page or the login page when the boolean received is false
|
* Operator that returns a UrlTree to a forbidden page or the login page when the boolean received is false
|
||||||
|
Reference in New Issue
Block a user