Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Made legacy bitstream URLs redirect with 301 status code #3062

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions src/app/bitstream-page/bitstream-page-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,17 +24,13 @@ 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
Expand Down
154 changes: 154 additions & 0 deletions src/app/bitstream-page/legacy-bitstream-url-redirect.guard.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { fakeAsync } from '@angular/core/testing';
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 { Bitstream } from '../core/shared/bitstream.model';
import { RouterStub } from '../shared/testing/router.stub';
import { ServerResponseServiceStub } from '../shared/testing/server-response-service.stub';
import { legacyBitstreamURLRedirectGuard } from './legacy-bitstream-url-redirect.guard';

describe('legacyBitstreamURLRedirectGuard', () => {
let resolver: any;
let bitstreamDataService: BitstreamDataService;
let remoteDataMocks: { [type: string]: RemoteData<Bitstream> };
let route;
let state;
let serverResponseService: ServerResponseServiceStub;
let router: RouterStub;

beforeEach(() => {
route = {
params: {},
queryParams: {},
};
router = new RouterStub();
serverResponseService = new ServerResponseServiceStub();
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, new 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, serverResponseService, 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, serverResponseService, 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, serverResponseService, 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', fakeAsync(() => {
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, serverResponseService, router).subscribe(() => {
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalled();
expect(router.createUrlTree).toHaveBeenCalledWith([PAGE_NOT_FOUND_PATH]);
});
}));

it('...succeeded without content', fakeAsync(() => {
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, serverResponseService, router).subscribe(() => {
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalled();
expect(router.createUrlTree).toHaveBeenCalledWith([PAGE_NOT_FOUND_PATH]);
});
}));

it('...succeeded', fakeAsync(() => {
spyOn(serverResponseService, 'setStatus').and.callThrough();
spyOn(bitstreamDataService, 'findByItemHandle').and.returnValue(cold('a-b-c', {
a: remoteDataMocks.RequestPending,
b: remoteDataMocks.ResponsePending,
c: remoteDataMocks.Success,
}));
resolver(route, state, bitstreamDataService, serverResponseService, router).subscribe(() => {
expect(bitstreamDataService.findByItemHandle).toHaveBeenCalled();
expect(serverResponseService.setStatus).toHaveBeenCalledWith(301);
expect(router.parseUrl).toHaveBeenCalled();
});
}));
});
});
});
63 changes: 63 additions & 0 deletions src/app/bitstream-page/legacy-bitstream-url-redirect.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { inject } from '@angular/core';
import {
ActivatedRouteSnapshot,
CanActivateFn,
Router,
RouterStateSnapshot,
UrlTree,
} from '@angular/router';
import { Observable } from 'rxjs';
import {
map,
tap,
} 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 { ServerResponseService } from '../core/services/server-response.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.
*
* @returns Observable<UrlTree> Returns a URL to redirect the user to the new URL format
*/
export const legacyBitstreamURLRedirectGuard: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
bitstreamDataService: BitstreamDataService = inject(BitstreamDataService),
serverResponseService: ServerResponseService = inject(ServerResponseService),
router: Router = inject(Router),
): Observable<UrlTree> => {
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(),
tap((rd: RemoteData<Bitstream>) => {
if (rd.hasSucceeded && !rd.hasNoContent) {
serverResponseService.setStatus(301);
}
}),
map((rd: RemoteData<Bitstream>) => {
if (rd.hasSucceeded && !rd.hasNoContent) {
return router.parseUrl(`/bitstreams/${rd.payload.uuid}/download`);
} else {
return router.createUrlTree([PAGE_NOT_FOUND_PATH]);
}
}),
);
};
146 changes: 0 additions & 146 deletions src/app/bitstream-page/legacy-bitstream-url.resolver.spec.ts

This file was deleted.

Loading
Loading