Skip to content

Commit

Permalink
Update StaticFilesWrapper to ASGI 3 single-callable. (#1567)
Browse files Browse the repository at this point in the history
* Added a suite of tests covering the basic wrapper functionality. 
* An additional constructor argument has been added to facilitate testing.
  • Loading branch information
davidmarquis committed Nov 9, 2020
1 parent a32b513 commit 5462419
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 5 deletions.
11 changes: 6 additions & 5 deletions channels/staticfiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ class StaticFilesWrapper:
files, passing them off to Django's static file serving.
"""

def __init__(self, application):
def __init__(self, application, staticfiles_handler=None):
self.application = application
self.staticfiles_handler_class = staticfiles_handler or StaticFilesHandler
self.base_url = urlparse(self.get_base_url())

def get_base_url(self):
Expand All @@ -28,19 +29,19 @@ def _should_handle(self, path):
Checks if the path should be handled. Ignores the path if:
* the host is provided as part of the base_url
* the request's path isn't under the media path (or equal)
* the request's path isn't under the static files path (or equal)
"""
return path.startswith(self.base_url[2]) and not self.base_url[1]

def __call__(self, scope, receive, send):
async def __call__(self, scope, receive, send):
# Only even look at HTTP requests
if scope["type"] == "http" and self._should_handle(scope["path"]):
# Serve static content
return StaticFilesHandler()(
return await self.staticfiles_handler_class()(
dict(scope, static_base_url=self.base_url), receive, send
)
# Hand off to the main app
return self.application(scope, receive, send)
return await self.application(scope, receive, send)


class StaticFilesHandler(AsgiHandler):
Expand Down
101 changes: 101 additions & 0 deletions tests/test_staticfiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import pytest

from channels.staticfiles import StaticFilesWrapper


@pytest.fixture(autouse=True)
def configure_static_files(settings):
settings.STATIC_URL = "/static"
settings.MEDIA_URL = "/media"


class MockApplication:
def __init__(self, return_value=None):
self.return_value = return_value
self.was_called = False

async def __call__(self, scope, receive, send):
self.was_called = True
return self.return_value


class MockStaticHandler:
async def __call__(self, scope, receive, send):
return scope["path"]


def request_for_path(path, type="http"):
return {
"type": type,
"path": path,
}


@pytest.mark.asyncio
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
async def test_staticfiles_wrapper_serves_static_http_requests(settings):
settings.STATIC_URL = "/mystatic/"

application = MockApplication("application")

wrapper = StaticFilesWrapper(application, staticfiles_handler=MockStaticHandler)

scope = request_for_path("/mystatic/image.png")
assert (
await wrapper(scope, None, None) == "/mystatic/image.png"
), "StaticFilesWrapper should serve paths under the STATIC_URL path"
assert (
not application.was_called
), "The inner application should not be called when serving static files"


@pytest.mark.asyncio
async def test_staticfiles_wrapper_calls_application_for_non_static_http_requests():
wrapper = StaticFilesWrapper(MockApplication("application"))

non_static_path = request_for_path("/path/to/non/static/resource")
assert (
await wrapper(non_static_path, None, None) == "application"
), "StaticFilesWrapper should call inner application for non-static HTTP paths"

non_http_path = request_for_path("/path/to/websocket", type="websocket")
assert (
await wrapper(non_http_path, None, None) == "application"
), "StaticFilesWrapper should call inner application for non-HTTP paths"


@pytest.mark.asyncio
async def test_staticfiles_wrapper_calls_application_for_non_http_paths(settings):
settings.STATIC_URL = "/mystatic/"

wrapper = StaticFilesWrapper(MockApplication("application"))

non_http_static_path = request_for_path("/mystatic/match", type="websocket")
assert await wrapper(non_http_static_path, None, None) == "application", (
"StaticFilesWrapper should call inner application if path matches "
"but type is not HTTP"
)


@pytest.mark.asyncio
async def test_staticfiles_wrapper_calls_application_if_static_url_has_host(settings):
settings.STATIC_URL = "http://hostname.com/mystatic/"

wrapper = StaticFilesWrapper(MockApplication("application"))

scope = request_for_path("/mystatic/match")
assert await wrapper(scope, None, None) == "application", (
"StaticFilesWrapper should call inner application if STATIC_URL contains a "
"host, even if path matches"
)


def test_is_single_callable():
from asgiref.compatibility import is_double_callable

wrapper = StaticFilesWrapper(None)

assert not is_double_callable(wrapper), (
"StaticFilesWrapper should be recognized as a single callable by "
"asgiref compatibility tools"
)

0 comments on commit 5462419

Please sign in to comment.