Refactor to use HardRedirectService. Update its redirect method to support optional statusCode

This commit is contained in:
Tim Donohue
2023-06-22 13:37:14 -05:00
parent 0fe33eecd1
commit d4a5308d0c
5 changed files with 44 additions and 57 deletions

View File

@@ -10,6 +10,7 @@ import { RequestService } from './request.service';
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
import { Item } from '../shared/item.model'; import { Item } from '../shared/item.model';
import { EMBED_SEPARATOR } from './base/base-data.service'; import { EMBED_SEPARATOR } from './base/base-data.service';
import { HardRedirectService } from '../services/hard-redirect.service';
describe('DsoRedirectService', () => { describe('DsoRedirectService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
@@ -17,7 +18,7 @@ describe('DsoRedirectService', () => {
let halService: HALEndpointService; let halService: HALEndpointService;
let requestService: RequestService; let requestService: RequestService;
let rdbService: RemoteDataBuildService; let rdbService: RemoteDataBuildService;
let router; let redirectService: HardRedirectService;
let remoteData; let remoteData;
const dsoUUID = '9b4f22f4-164a-49db-8817-3316b6ee5746'; const dsoUUID = '9b4f22f4-164a-49db-8817-3316b6ee5746';
const dsoHandle = '1234567789/22'; const dsoHandle = '1234567789/22';
@@ -27,9 +28,6 @@ describe('DsoRedirectService', () => {
const requestUUIDURL = `https://rest.api/rest/api/pid/find?id=${dsoUUID}`; const requestUUIDURL = `https://rest.api/rest/api/pid/find?id=${dsoUUID}`;
const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2'; const requestUUID = '34cfed7c-f597-49ef-9cbe-ea351f0023c2';
const objectCache = {} as ObjectCacheService; const objectCache = {} as ObjectCacheService;
const mockResponse = jasmine.createSpyObj(['redirect']);
const mockPlatformBrowser = 'browser';
const mockPlatformServer = 'server';
beforeEach(() => { beforeEach(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
@@ -41,9 +39,6 @@ describe('DsoRedirectService', () => {
generateRequestId: requestUUID, generateRequestId: requestUUID,
send: true send: true
}); });
router = {
navigate: jasmine.createSpy('navigate')
};
remoteData = createSuccessfulRemoteDataObject(Object.assign(new Item(), { remoteData = createSuccessfulRemoteDataObject(Object.assign(new Item(), {
type: 'item', type: 'item',
@@ -55,14 +50,17 @@ describe('DsoRedirectService', () => {
a: remoteData a: remoteData
}) })
}); });
redirectService = jasmine.createSpyObj('redirectService', {
redirect: {}
});
service = new DsoRedirectService( service = new DsoRedirectService(
requestService, requestService,
rdbService, rdbService,
objectCache, objectCache,
halService, halService,
router, redirectService
mockResponse,
mockPlatformBrowser // default to CSR except where explicitly SSR below
); );
}); });
@@ -109,7 +107,7 @@ describe('DsoRedirectService', () => {
redir.subscribe(); redir.subscribe();
scheduler.schedule(() => redir); scheduler.schedule(() => redir);
scheduler.flush(); scheduler.flush();
expect(router.navigate).toHaveBeenCalledWith(['/items/' + remoteData.payload.uuid]); expect(redirectService.redirect).toHaveBeenCalledWith('/items/' + remoteData.payload.uuid, 301);
}); });
it('should navigate to entities route with the corresponding entity type', () => { it('should navigate to entities route with the corresponding entity type', () => {
remoteData.payload.type = 'item'; remoteData.payload.type = 'item';
@@ -126,7 +124,7 @@ describe('DsoRedirectService', () => {
redir.subscribe(); redir.subscribe();
scheduler.schedule(() => redir); scheduler.schedule(() => redir);
scheduler.flush(); scheduler.flush();
expect(router.navigate).toHaveBeenCalledWith(['/entities/publication/' + remoteData.payload.uuid]); expect(redirectService.redirect).toHaveBeenCalledWith('/entities/publication/' + remoteData.payload.uuid, 301);
}); });
it('should navigate to collections route', () => { it('should navigate to collections route', () => {
@@ -135,7 +133,7 @@ describe('DsoRedirectService', () => {
redir.subscribe(); redir.subscribe();
scheduler.schedule(() => redir); scheduler.schedule(() => redir);
scheduler.flush(); scheduler.flush();
expect(router.navigate).toHaveBeenCalledWith(['/collections/' + remoteData.payload.uuid]); expect(redirectService.redirect).toHaveBeenCalledWith('/collections/' + remoteData.payload.uuid, 301);
}); });
it('should navigate to communities route', () => { it('should navigate to communities route', () => {
@@ -144,26 +142,7 @@ describe('DsoRedirectService', () => {
redir.subscribe(); redir.subscribe();
scheduler.schedule(() => redir); scheduler.schedule(() => redir);
scheduler.flush(); scheduler.flush();
expect(router.navigate).toHaveBeenCalledWith(['/communities/' + remoteData.payload.uuid]); expect(redirectService.redirect).toHaveBeenCalledWith('/communities/' + remoteData.payload.uuid, 301);
});
it('should return 301 redirect when SSR is used', () => {
service = new DsoRedirectService(
requestService,
rdbService,
objectCache,
halService,
router,
mockResponse,
mockPlatformServer // explicitly SSR mode
);
remoteData.payload.type = 'item';
const redir = service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE);
// The framework would normally subscribe but do it here so we can test navigation.
redir.subscribe();
scheduler.schedule(() => redir);
scheduler.flush();
expect(mockResponse.redirect).toHaveBeenCalledWith(301, '/items/' + remoteData.payload.uuid);
}); });
}); });

View File

@@ -6,8 +6,7 @@
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
/* eslint-disable max-classes-per-file */ /* eslint-disable max-classes-per-file */
import { Inject, Injectable, Optional, PLATFORM_ID } from '@angular/core'; import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators'; import { tap } from 'rxjs/operators';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
@@ -21,8 +20,7 @@ import { getFirstCompletedRemoteData } from '../shared/operators';
import { DSpaceObject } from '../shared/dspace-object.model'; import { DSpaceObject } from '../shared/dspace-object.model';
import { IdentifiableDataService } from './base/identifiable-data.service'; import { IdentifiableDataService } from './base/identifiable-data.service';
import { getDSORoute } from '../../app-routing-paths'; import { getDSORoute } from '../../app-routing-paths';
import { RESPONSE } from '@nguniversal/express-engine/tokens'; import { HardRedirectService } from '../services/hard-redirect.service';
import { isPlatformServer } from '@angular/common';
const ID_ENDPOINT = 'pid'; const ID_ENDPOINT = 'pid';
const UUID_ENDPOINT = 'dso'; const UUID_ENDPOINT = 'dso';
@@ -76,9 +74,7 @@ export class DsoRedirectService {
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService, protected objectCache: ObjectCacheService,
protected halService: HALEndpointService, protected halService: HALEndpointService,
private router: Router, private hardRedirectService: HardRedirectService
@Optional() @Inject(RESPONSE) private response: any,
@Inject(PLATFORM_ID) private platformId: any
) { ) {
this.dataService = new DsoByIdOrUUIDDataService(requestService, rdbService, objectCache, halService); this.dataService = new DsoByIdOrUUIDDataService(requestService, rdbService, objectCache, halService);
} }
@@ -88,9 +84,6 @@ export class DsoRedirectService {
* This is used to redirect paths like "/handle/[prefix]/[suffix]" to the object's path (e.g. /items/[uuid]). * This is used to redirect paths like "/handle/[prefix]/[suffix]" to the object's path (e.g. /items/[uuid]).
* See LookupGuard for more examples. * See LookupGuard for more examples.
* *
* If this is called server side (via SSR), it performs a 301 Redirect.
* If this is called client side (via CSR), it simply uses the Angular router to do the redirect.
*
* @param id the identifier of the object to retrieve * @param id the identifier of the object to retrieve
* @param identifierType the type of the given identifier (defaults to UUID) * @param identifierType the type of the given identifier (defaults to UUID)
*/ */
@@ -104,12 +97,8 @@ export class DsoRedirectService {
if (hasValue(dso.uuid)) { if (hasValue(dso.uuid)) {
let newRoute = getDSORoute(dso); let newRoute = getDSORoute(dso);
if (hasValue(newRoute)) { if (hasValue(newRoute)) {
// If running via SSR, perform a "301 Moved Permanently" redirect for SEO purposes. // Use a "301 Moved Permanently" redirect for SEO purposes
if (isPlatformServer(this.platformId)) { this.hardRedirectService.redirect(newRoute, 301);
this.response.redirect(301, newRoute);
} else {
this.router.navigate([newRoute]);
}
} }
} }
} }

View File

@@ -11,8 +11,10 @@ export abstract class HardRedirectService {
* *
* @param url * @param url
* the page to redirect to * the page to redirect to
* @param statusCode
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
*/ */
abstract redirect(url: string); abstract redirect(url: string, statusCode?: number);
/** /**
* Get the current route, with query params included * Get the current route, with query params included

View File

@@ -22,20 +22,33 @@ describe('ServerHardRedirectService', () => {
expect(service).toBeTruthy(); expect(service).toBeTruthy();
}); });
describe('when performing a redirect', () => { describe('when performing a default redirect', () => {
const redirect = 'test redirect'; const redirect = 'test redirect';
beforeEach(() => { beforeEach(() => {
service.redirect(redirect); service.redirect(redirect);
}); });
it('should update the response object', () => { it('should perform a 302 redirect', () => {
expect(mockResponse.redirect).toHaveBeenCalledWith(302, redirect); expect(mockResponse.redirect).toHaveBeenCalledWith(302, redirect);
expect(mockResponse.end).toHaveBeenCalled(); expect(mockResponse.end).toHaveBeenCalled();
}); });
}); });
describe('when performing a 301 redirect', () => {
const redirect = 'test 301 redirect';
const redirectStatusCode = 301;
beforeEach(() => {
service.redirect(redirect, redirectStatusCode);
});
it('should redirect with passed in status code', () => {
expect(mockResponse.redirect).toHaveBeenCalledWith(redirectStatusCode, redirect);
expect(mockResponse.end).toHaveBeenCalled();
});
});
describe('when requesting the current route', () => { describe('when requesting the current route', () => {
beforeEach(() => { beforeEach(() => {

View File

@@ -17,10 +17,14 @@ export class ServerHardRedirectService extends HardRedirectService {
} }
/** /**
* Perform a hard redirect to URL * Perform a hard redirect to a given location.
*
* @param url * @param url
* the page to redirect to
* @param statusCode
* optional HTTP status code to use for redirect (default = 302, which is a temporary redirect)
*/ */
redirect(url: string) { redirect(url: string, statusCode?: number) {
if (url === this.req.url) { if (url === this.req.url) {
return; return;
@@ -38,8 +42,8 @@ export class ServerHardRedirectService extends HardRedirectService {
process.exit(1); process.exit(1);
} }
} else { } else {
// attempt to use the already set status // attempt to use passed in statusCode or the already set status (in request)
let status = this.res.statusCode || 0; let status = statusCode || this.res.statusCode || 0;
if (status < 300 || status >= 400) { if (status < 300 || status >= 400) {
// temporary redirect // temporary redirect
status = 302; status = 302;