Skip to content

Commit

Permalink
Prepare release (#142)
Browse files Browse the repository at this point in the history
* Fix Session middleware duplicate
* Add tests
  • Loading branch information
tarsil authored Feb 12, 2025
1 parent d356cc6 commit 4c375fc
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 66 deletions.
4 changes: 4 additions & 0 deletions docs/en/docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ hide:

- Declaring `DefinePermission` became optional as Lilya automatically wraps if not provided.
- Declaring `DefineMiddleware` became optional as Lilya automatically wraps if not provided.
-
### Fixed

- `SessionMiddleware` was creating duplicates because it was called on every lifecycle.

## 0.12.6

Expand Down
2 changes: 1 addition & 1 deletion lilya/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.12.6"
__version__ = "0.12.7"
91 changes: 27 additions & 64 deletions lilya/middleware/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,6 @@ def __init__(
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
"""
ASGI application callable.
Args:
scope (Scope): ASGI scope.
receive (Receive): ASGI receive channel.
send (Send): ASGI send channel.
"""
if scope["type"] not in ("http", "websocket"):
await self.app(scope, receive, send)
Expand All @@ -89,13 +84,6 @@ def encode_session(self, session: Any) -> bytes:
async def load_session_data(self, scope: Scope, connection: Connection) -> bool:
"""
Load session data from the session cookie.
Args:
scope (Scope): ASGI scope.
connection (Connection): HTTP connection object.
Returns:
bool: True if the initial session was empty, False otherwise.
"""
if self.session_cookie in connection.cookies:
data = connection.cookies[self.session_cookie].encode("utf-8")
Expand All @@ -117,59 +105,34 @@ async def process_response(
send: Send,
) -> None:
"""
Process the response and set the session cookie.
Args:
message (Message): ASGI message.
scope (Scope): ASGI scope.
initial_session_was_empty (bool): True if the initial session was empty, False otherwise.
send (Send): ASGI send channel.
Process the response and set the session cookie. Handles multiple messages.
"""
if message["type"] == "http.response.start":
if scope["session"]:
message = await self.set_session_cookie(scope, message)
elif not initial_session_was_empty:
message = await self.clear_session_cookie(scope, message)
headers = Header.ensure_header_instance(scope=message)
if "set-cookie" not in (h.lower() for h in headers.keys()): # Check if already set
if scope["session"]:
data = self.encode_session(scope["session"])
data = self.signer.sign(data)
header_value = (
"{session_cookie}={data}; path={path}; {max_age}{security_flags}".format(
session_cookie=self.session_cookie,
data=data.decode("utf-8"),
path=self.path,
max_age=f"Max-Age={self.max_age}; " if self.max_age else "",
security_flags=self.security_flags,
)
)
headers.add("Set-Cookie", header_value)
elif not initial_session_was_empty:
header_value = (
"{session_cookie}={data}; path={path}; {expires}{security_flags}".format(
session_cookie=self.session_cookie,
data="null",
path=self.path,
expires="expires=Thu, 01 Jan 1970 00:00:00 GMT; ",
security_flags=self.security_flags,
)
)
headers.add("Set-Cookie", header_value)

await send(message)

async def set_session_cookie(self, scope: Scope, message: Message) -> Message:
"""
Set the session cookie in the response headers.
Args:
scope (Scope): ASGI scope.
message (Message): ASGI message
"""
data = self.encode_session(scope["session"])
data = self.signer.sign(data)
# we need to update the message
headers = Header.ensure_header_instance(scope=message)
header_value = "{session_cookie}={data}; path={path}; {max_age}{security_flags}".format(
session_cookie=self.session_cookie,
data=data.decode("utf-8"),
path=self.path,
max_age=f"Max-Age={self.max_age}; " if self.max_age else "",
security_flags=self.security_flags,
)
headers.add("Set-Cookie", header_value)
return message

async def clear_session_cookie(self, scope: Scope, message: Message) -> Message:
"""
Clear the session cookie in the response headers.
Args:
scope (Scope): ASGI scope.
"""
# we need to update the message
headers = Header.ensure_header_instance(scope=message)
header_value = "{session_cookie}={data}; path={path}; {expires}{security_flags}".format(
session_cookie=self.session_cookie,
data="null",
path=self.path,
expires="expires=Thu, 01 Jan 1970 00:00:00 GMT; ",
security_flags=self.security_flags,
)
headers.add("Set-Cookie", header_value)
return message
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ source = ["tests", "lilya"]
# omit = []

[[tool.mypy.overrides]]
module = ["multipart.*", "mako.*", "uvicorn.*"]
module = ["multipart.*", "mako.*", "uvicorn.*"]
ignore_missing_imports = true
ignore_errors = true

Expand Down
106 changes: 106 additions & 0 deletions tests/test_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
import pytest

from lilya import status
from lilya.apps import Lilya
from lilya.background import Task
from lilya.encoders import Encoder
from lilya.middleware import DefineMiddleware
from lilya.middleware.sessions import SessionMiddleware
from lilya.requests import Request
from lilya.responses import (
Error,
Expand All @@ -21,6 +24,7 @@
StreamingResponse,
redirect,
)
from lilya.routing import Path
from lilya.testclient import TestClient


Expand Down Expand Up @@ -382,6 +386,108 @@ async def app(scope, receive, send):
)


def test_set_cookie_multiple(test_client_factory, monkeypatch):
# Mock time used as a reference for `Expires` by stdlib `SimpleCookie`.
mocked_now = dt.datetime(2037, 1, 22, 12, 0, 0, tzinfo=dt.timezone.utc)
monkeypatch.setattr(time, "time", lambda: mocked_now.timestamp())

async def app(scope, receive, send):
response = Response("Hello, world!", media_type="text/plain", encoders=[FooEncoder])
assert isinstance(response.encoders[0], FooEncoder)
response.set_cookie(
"access_cookie",
"myvalue",
max_age=10,
expires=10,
path="/",
domain="localhost",
secure=True,
httponly=True,
samesite="none",
)
response.set_cookie(
"refresh_cookie",
"myvalue",
max_age=10,
expires=10,
path="/",
domain="localhost",
secure=True,
httponly=True,
samesite="none",
)
await response(scope, receive, send)

client = test_client_factory(app)
response = client.get("/")
assert response.text == "Hello, world!"

assert (
response.headers["set-cookie"]
== "access_cookie=myvalue; Domain=localhost; expires=Thu, 22 Jan 2037 12:00:10 GMT; HttpOnly; Max-Age=10; Path=/; SameSite=none; Secure, refresh_cookie=myvalue; Domain=localhost; expires=Thu, 22 Jan 2037 12:00:10 GMT; HttpOnly; Max-Age=10; Path=/; SameSite=none; Secure"
)


def test_set_cookie_multiple_with_session(test_client_factory, monkeypatch):
# Mock time used as a reference for `Expires` by stdlib `SimpleCookie`.
mocked_now = dt.datetime(2037, 1, 22, 12, 0, 0, tzinfo=dt.timezone.utc)
monkeypatch.setattr(time, "time", lambda: mocked_now.timestamp())

async def home():
response = Response("Hello, world!", media_type="text/plain", encoders=[FooEncoder])
response.set_cookie(
"access_cookie",
"myvalue",
max_age=10,
expires=10,
path="/",
domain="localhost",
secure=True,
httponly=True,
samesite="none",
)
response.set_cookie(
"refresh_cookie",
"myvalue",
max_age=10,
expires=10,
path="/",
domain="localhost",
secure=True,
httponly=True,
samesite="none",
)
return response

async def update_session(request: Request) -> JSONResponse:
data = await request.json()
request.session.update(data)
return JSONResponse({"session": request.session})

lilya_app = Lilya(
routes=[
Path("/session", update_session, methods=["POST"]),
Path("/", home),
],
middleware=[
DefineMiddleware(SessionMiddleware, secret_key="your-secret-key"),
],
)

client = test_client_factory(lilya_app)

response = client.post("/session", json={"some": "data"})
assert response.json() == {"session": {"some": "data"}}

response = client.get("/")
assert response.text == "Hello, world!"

assert (
response.headers["set-cookie"]
== "access_cookie=myvalue; Domain=localhost; expires=Thu, 22 Jan 2037 12:00:10 GMT; HttpOnly; Max-Age=10; Path=/; SameSite=none; Secure, refresh_cookie=myvalue; Domain=localhost; expires=Thu, 22 Jan 2037 12:00:10 GMT; HttpOnly; Max-Age=10; Path=/; SameSite=none; Secure"
)


@pytest.mark.parametrize(
"expires",
[
Expand Down

0 comments on commit 4c375fc

Please sign in to comment.