async def test_decorator_starlette_endpoint() -> None: cache = Cache("locmem://null", ttl=2 * 60) @cached(cache) class CachedHome(HTTPEndpoint): async def get(self, request: Request) -> Response: return PlainTextResponse("Hello, world!") class UncachedUsers(HTTPEndpoint): async def get(self, request: Request) -> Response: return PlainTextResponse("Hello, users!") assert isinstance(CachedHome, CacheMiddleware) spy = CachedHome.app = CacheSpy(CachedHome.app) users_spy = CacheSpy(UncachedUsers) app = Starlette(routes=[Route("/", CachedHome), Route("/users", users_spy)]) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: assert spy.misses == 0 r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert "Expires" in r.headers assert "Cache-Control" in r.headers assert spy.misses == 1 r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert "Expires" in r.headers assert "Cache-Control" in r.headers assert spy.misses == 1 assert users_spy.misses == 0 r = await client.get("/users") assert r.status_code == 200 assert r.text == "Hello, users!" assert "Expires" not in r.headers assert "Cache-Control" not in r.headers assert users_spy.misses == 1 r = await client.get("/users") assert r.status_code == 200 assert r.text == "Hello, users!" assert "Expires" not in r.headers assert "Cache-Control" not in r.headers assert users_spy.misses == 2
async def test_cookies_in_response_and_cookieless_request() -> None: """ Responses that set cookies shouldn't be cached if the request doesn't have cookies. """ cache = Cache("locmem://null") async def app(scope: Scope, receive: Receive, send: Send) -> None: response = PlainTextResponse("Hello, world!") response.set_cookie("session_id", "1234") await response(scope, receive, send) spy = CacheSpy(app) app = CacheMiddleware(spy, cache=cache) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert spy.misses == 1 r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert spy.misses == 2
async def test_decorator_raw_asgi() -> None: cache = Cache("locmem://null", ttl=2 * 60) @cached(cache) async def app(scope: Scope, receive: Receive, send: Send) -> None: response = PlainTextResponse("Hello, world!") await response(scope, receive, send) spy = app.app = CacheSpy(app.app) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: assert spy.misses == 0 r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert "Expires" in r.headers assert "Cache-Control" in r.headers assert spy.misses == 1 r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert "Expires" in r.headers assert "Cache-Control" in r.headers assert spy.misses == 1
async def test_use_cached_head_response_on_get() -> None: """ Making a HEAD request should use the cached response for future GET requests. """ cache = Cache("locmem://null") spy = CacheSpy(PlainTextResponse("Hello, world!")) app = CacheMiddleware(spy, cache=cache) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: assert spy.misses == 0 r = await client.head("/") assert not r.text assert r.status_code == 200 assert "Expires" in r.headers assert "Cache-Control" in r.headers assert spy.misses == 1 r1 = await client.get("/") assert r1.text == "Hello, world!" assert r1.status_code == 200 assert "Expires" in r.headers assert "Cache-Control" in r.headers assert spy.misses == 1
async def test_cache_response() -> None: cache = Cache("locmem://null", ttl=2 * 60) spy = CacheSpy(PlainTextResponse("Hello, world!")) app = CacheMiddleware(spy, cache=cache) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: assert spy.misses == 0 r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert spy.misses == 1 assert "Expires" in r.headers expires_fmt = "%a, %d %b %Y %H:%M:%S GMT" expires = dt.datetime.strptime(r.headers["Expires"], expires_fmt) delta: dt.timedelta = expires - dt.datetime.utcnow() assert delta.total_seconds() == pytest.approx(120, rel=1e-2) assert "Cache-Control" in r.headers assert r.headers["Cache-Control"] == "max-age=120" r1 = await client.get("/") assert spy.misses == 1 assert ComparableHTTPXResponse(r1) == r r2 = await client.get("/") assert spy.misses == 1 assert ComparableHTTPXResponse(r2) == r
async def test_streaming_response() -> None: """Streaming responses should not be cached.""" cache = Cache("locmem://null") async def body() -> typing.AsyncIterator[str]: yield "Hello, " yield "world!" async def app(scope: Scope, receive: Receive, send: Send) -> None: response = StreamingResponse(body()) await response(scope, receive, send) spy = CacheSpy(app) app = CacheMiddleware(spy, cache=cache) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: assert spy.misses == 0 r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert spy.misses == 1 r = await client.get("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert spy.misses == 2
async def test_vary() -> None: """ Sending different values for request headers registered as varying should result in different cache entries. """ cache = Cache("locmem://null") async def gzippable_app(scope: Scope, receive: Receive, send: Send) -> None: headers = Headers(scope=scope) if "gzip" in headers.getlist("accept-encoding"): body = gzip.compress(b"Hello, world!") response = PlainTextResponse( content=body, headers={ "Content-Encoding": "gzip", "Content-Length": str(len(body)) }, ) else: response = PlainTextResponse("Hello, world!") response.headers["Vary"] = "Accept-Encoding" await response(scope, receive, send) spy = CacheSpy(gzippable_app) app = CacheMiddleware(spy, cache=cache) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: r = await client.get("/", headers={"accept-encoding": "gzip"}) assert spy.misses == 1 assert r.status_code == 200 assert r.text == "Hello, world!" assert r.headers["content-encoding"] == "gzip" assert "Expires" in r.headers assert "Cache-Control" in r.headers # Different "Accept-Encoding" header => the cached result # for "Accept-Encoding: gzip" should not be used. r1 = await client.get("/", headers={"accept-encoding": "identity"}) assert spy.misses == 2 assert r1.status_code == 200 assert r1.text == "Hello, world!" assert "Expires" in r.headers assert "Cache-Control" in r.headers # This "Accept-Encoding" header has already been seen => we should # get a cached response. r2 = await client.get("/", headers={"accept-encoding": "gzip"}) assert spy.misses == 2 assert r.status_code == 200 assert r.text == "Hello, world!" assert r2.headers["Content-Encoding"] == "gzip" assert "Expires" in r.headers assert "Cache-Control" in r.headers
async def test_not_200_ok(status_code: int) -> None: """Responses that don't have status code 200 should not be cached.""" cache = Cache("locmem://null") spy = CacheSpy(PlainTextResponse("Hello, world!", status_code=status_code)) app = CacheMiddleware(spy, cache=cache) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: r = await client.get("/") assert r.status_code == status_code assert r.text == "Hello, world!" assert "Expires" not in r.headers assert "Cache-Control" not in r.headers assert spy.misses == 1 r1 = await client.get("/") assert ComparableHTTPXResponse(r1) == r assert spy.misses == 2
async def test_non_cachable_request() -> None: cache = Cache("locmem://null") spy = CacheSpy(PlainTextResponse("Hello, world!")) app = CacheMiddleware(spy, cache=cache) client = httpx.AsyncClient(app=app, base_url="http://testserver") async with cache, client: assert spy.misses == 0 r = await client.post("/") assert r.status_code == 200 assert r.text == "Hello, world!" assert "Expires" not in r.headers assert "Cache-Control" not in r.headers assert spy.misses == 1 r1 = await client.post("/") assert ComparableHTTPXResponse(r1) == r assert spy.misses == 2
from starlette.requests import Request from starlette.responses import JSONResponse, PlainTextResponse, Response from starlette.routing import Mount, Route, request_response from asgi_caches.decorators import cache_control from asgi_caches.middleware import CacheMiddleware from tests.utils import CacheSpy from .resources import cache, special_cache async def _home(request: Request) -> Response: return PlainTextResponse("Hello, world!") home = CacheSpy(request_response(_home)) @cache_control(max_age=30, must_revalidate=True) class Pi(HTTPEndpoint): async def get(self, request: Request) -> Response: return JSONResponse({"value": math.pi}) pi = CacheSpy(Pi) class Exp(HTTPEndpoint): async def get(self, request: Request) -> Response: return JSONResponse({"value": math.e})