From 370d4d833684821293efe8295635712200a78520 Mon Sep 17 00:00:00 2001 From: Daniel Bauer Date: Thu, 15 Aug 2024 21:44:20 +0200 Subject: [PATCH] add middleware for signposts --- django_signposting/middleware.py | 40 +++++++++++++++++ tests/test_middleware.py | 75 ++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 django_signposting/middleware.py create mode 100644 tests/test_middleware.py diff --git a/django_signposting/middleware.py b/django_signposting/middleware.py new file mode 100644 index 0000000..27fed75 --- /dev/null +++ b/django_signposting/middleware.py @@ -0,0 +1,40 @@ +from typing import Callable +from django.http import HttpRequest, HttpResponse + + +class SignpostingMiddleware: + + def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]): + self.get_response = get_response + + def __call__(self, request: HttpRequest) -> HttpResponse: + response = self.get_response(request) + + # no signposts on errors + if response.status_code >= 400: + return response + + if not hasattr(response, "_signposts"): + return response + + self._add_signposts(response, response._signposts) + + return response + + def _add_signposts(self, response: HttpResponse, typed_links: dict[str, list[str|tuple[str, str]]]): + """ Adds signposting headers to the respones. + params: + response - the response object + typed_links - a map of relation types to a list of corresponding links. Each link can be a link or a tuple of link and media type. + """ + link_snippets = [] + for relation_type in typed_links.keys(): + links = typed_links.get(relation_type, []) + for link in links: + if isinstance(link, tuple) and len(link) > 1: + link_snippets.append(f'<{link[0]}> ; rel="{relation_type}" ; type="{link[1]}"') + else: + link_snippets.append(f'<{link}> ; rel="{relation_type}"') + + response["Link"] = " , ".join(link_snippets) + diff --git a/tests/test_middleware.py b/tests/test_middleware.py new file mode 100644 index 0000000..82e896d --- /dev/null +++ b/tests/test_middleware.py @@ -0,0 +1,75 @@ +from django.http import HttpResponse, HttpRequest +from django.conf import settings +from django_signposting.middleware import SignpostingMiddleware +import pytest + + +@pytest.fixture(scope="module", autouse=True) +def configure_django_settings(): + settings.configure() + + +def test_middleware_no_signposting(): + response = HttpResponse() + response.status_code = 200 + + middleware = SignpostingMiddleware(lambda request: response) + response = middleware(None) + assert "Link" not in response.headers + + +def test_middleware_signposting(): + response = HttpResponse() + response.status_code = 200 + response._signposts = {"author": ["http://example.com"]} + + middleware = SignpostingMiddleware(lambda request: response) + response = middleware(None) + assert response.headers["Link"] == ' ; rel="author"' + + +def test_middleware_multiple_signposts(): + response = HttpResponse() + response.status_code = 200 + response._signposts = { + "author": [ + "http://example.com", + "http://example2.com" + ], + "cite-as": [ + "http://example3.com" + ] + } + + middleware = SignpostingMiddleware(lambda request: response) + response = middleware(None) + links = [x.strip() for x in response.headers["Link"].split(",")] + assert ' ; rel="author"' in links + assert ' ; rel="author"' in links + assert ' ; rel="cite-as"' in links + + +def test_middleware_signpost_with_content_type(): + response = HttpResponse() + response.status_code = 200 + response._signposts = { + "item": [ + ("http://example.com", "test/json"), + ] + } + + middleware = SignpostingMiddleware(lambda request: response) + response = middleware(None) + assert response.headers["Link"] == ' ; rel="item" ; type="test/json"' + + +def test_middleware_ignore_error_responses(): + response = HttpResponse() + response.status_code = 400 + response._signposts = { + "author": ["https://example.com"] + } + + middleware = SignpostingMiddleware(lambda request: response) + response = middleware(None) + assert "Link" not in response.headers