diff --git a/src/index.ts b/src/index.ts index e8cd7b879..964b9dbb7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -216,6 +216,7 @@ export * from './server/middleware/StaticAssetHandler'; export * from './server/middleware/WebSocketAdvertiser'; // Server/Util +export * from './server/util/RedirectAllHttpHandler'; export * from './server/util/RouterHandler'; // Storage/Accessors diff --git a/src/server/util/RedirectAllHttpHandler.ts b/src/server/util/RedirectAllHttpHandler.ts new file mode 100644 index 000000000..f515185a6 --- /dev/null +++ b/src/server/util/RedirectAllHttpHandler.ts @@ -0,0 +1,46 @@ +import { RedirectResponseDescription } from '../../ldp/http/response/RedirectResponseDescription'; +import type { ResponseWriter } from '../../ldp/http/ResponseWriter'; +import type { TargetExtractor } from '../../ldp/http/TargetExtractor'; +import { NotImplementedHttpError } from '../../util/errors/NotImplementedHttpError'; +import { getRelativeUrl, joinUrl } from '../../util/PathUtil'; +import type { HttpHandlerInput } from '../HttpHandler'; +import { HttpHandler } from '../HttpHandler'; + +export interface RedirectAllHttpHandlerArgs { + baseUrl: string; + target: string; + targetExtractor: TargetExtractor; + responseWriter: ResponseWriter; +} + +/** + * Will redirect all incoming requests to the given target. + * In case the incoming request already has the correct target, + * the `canHandle` call will reject the input. + */ +export class RedirectAllHttpHandler extends HttpHandler { + private readonly baseUrl: string; + private readonly target: string; + private readonly targetExtractor: TargetExtractor; + private readonly responseWriter: ResponseWriter; + + public constructor(args: RedirectAllHttpHandlerArgs) { + super(); + this.baseUrl = args.baseUrl; + this.target = args.target; + this.targetExtractor = args.targetExtractor; + this.responseWriter = args.responseWriter; + } + + public async canHandle({ request }: HttpHandlerInput): Promise { + const target = await getRelativeUrl(this.baseUrl, request, this.targetExtractor); + if (target === this.target) { + throw new NotImplementedHttpError('Target is already correct.'); + } + } + + public async handle({ response }: HttpHandlerInput): Promise { + const result = new RedirectResponseDescription(joinUrl(this.baseUrl, this.target)); + await this.responseWriter.handleSafe({ response, result }); + } +} diff --git a/test/unit/server/util/RedirectAllHttpHandler.test.ts b/test/unit/server/util/RedirectAllHttpHandler.test.ts new file mode 100644 index 000000000..23e4da039 --- /dev/null +++ b/test/unit/server/util/RedirectAllHttpHandler.test.ts @@ -0,0 +1,53 @@ +import type { ResponseWriter } from '../../../../src/ldp/http/ResponseWriter'; +import type { TargetExtractor } from '../../../../src/ldp/http/TargetExtractor'; +import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier'; +import type { HttpRequest } from '../../../../src/server/HttpRequest'; +import type { HttpResponse } from '../../../../src/server/HttpResponse'; +import { RedirectAllHttpHandler } from '../../../../src/server/util/RedirectAllHttpHandler'; +import { NotImplementedHttpError } from '../../../../src/util/errors/NotImplementedHttpError'; +import { joinUrl } from '../../../../src/util/PathUtil'; +import { SOLID_HTTP } from '../../../../src/util/Vocabularies'; + +describe('A RedirectAllHttpHandler', (): void => { + const baseUrl = 'http://test.com/'; + const target = '/foo'; + const absoluteTarget = 'http://test.com/foo'; + let request: HttpRequest; + const response: HttpResponse = {} as any; + let targetExtractor: jest.Mocked; + let responseWriter: jest.Mocked; + let handler: RedirectAllHttpHandler; + + beforeEach(async(): Promise => { + request = { url: '/foo' } as any; + + targetExtractor = { + handleSafe: jest.fn(({ request: req }): ResourceIdentifier => ({ path: joinUrl(baseUrl, req.url!) })), + } as any; + + responseWriter = { handleSafe: jest.fn() } as any; + + handler = new RedirectAllHttpHandler({ baseUrl, target, targetExtractor, responseWriter }); + }); + + it('rejects requests for the target.', async(): Promise => { + request.url = target; + await expect(handler.canHandle({ request, response })).rejects.toThrow(NotImplementedHttpError); + }); + + it('accepts all other requests.', async(): Promise => { + request.url = '/otherPath'; + await expect(handler.canHandle({ request, response })).resolves.toBeUndefined(); + }); + + it('writes out a redirect response.', async(): Promise => { + await expect(handler.handle({ request, response })).resolves.toBeUndefined(); + expect(responseWriter.handleSafe).toHaveBeenCalledTimes(1); + expect(responseWriter.handleSafe).toHaveBeenLastCalledWith({ + response, + result: expect.objectContaining({ statusCode: 302 }), + }); + const { metadata } = responseWriter.handleSafe.mock.calls[0][0].result; + expect(metadata?.get(SOLID_HTTP.terms.location)?.value).toBe(absoluteTarget); + }); +});