Skip to content

Commit 9384f2a

Browse files
committed
add tests for async gzip middleware
1 parent b965f73 commit 9384f2a

File tree

2 files changed

+281
-0
lines changed

2 files changed

+281
-0
lines changed

tests/test_middlewares/test_gzip.py

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import gzip
2+
import random
3+
import struct
4+
from io import BytesIO
5+
6+
import pytest
7+
8+
from django.http import FileResponse, HttpResponse, StreamingHttpResponse
9+
from django.test import AsyncRequestFactory
10+
11+
from django_async_extensions.middleware.gzip import AsyncGZipMiddleware
12+
from django_async_extensions.middleware.http import AsyncConditionalGetMiddleware
13+
14+
int2byte = struct.Struct(">B").pack
15+
16+
17+
class TestGZipMiddleware:
18+
"""
19+
Tests the GZipMiddleware.
20+
"""
21+
22+
short_string = b"This string is too short to be worth compressing."
23+
compressible_string = b"a" * 500
24+
incompressible_string = b"".join(
25+
int2byte(random.randint(0, 255)) for _ in range(500) # noqa: S311
26+
)
27+
sequence = [b"a" * 500, b"b" * 200, b"a" * 300]
28+
sequence_unicode = ["a" * 500, "é" * 200, "a" * 300]
29+
request_factory = AsyncRequestFactory()
30+
31+
@pytest.fixture(autouse=True)
32+
def setup(self):
33+
self.req = self.request_factory.get("/")
34+
self.req.META["HTTP_ACCEPT_ENCODING"] = "gzip, deflate"
35+
self.req.META["HTTP_USER_AGENT"] = (
36+
"Mozilla/5.0 (Windows NT 5.1; rv:9.0.1) Gecko/20100101 Firefox/9.0.1"
37+
)
38+
self.resp = HttpResponse()
39+
self.resp.status_code = 200
40+
self.resp.content = self.compressible_string
41+
self.resp["Content-Type"] = "text/html; charset=UTF-8"
42+
43+
async def get_response(self, request):
44+
return self.resp
45+
46+
@staticmethod
47+
def decompress(gzipped_string):
48+
with gzip.GzipFile(mode="rb", fileobj=BytesIO(gzipped_string)) as f:
49+
return f.read()
50+
51+
@staticmethod
52+
def get_mtime(gzipped_string):
53+
with gzip.GzipFile(mode="rb", fileobj=BytesIO(gzipped_string)) as f:
54+
f.read() # must read the data before accessing the header
55+
return f.mtime
56+
57+
async def test_compress_response(self):
58+
"""
59+
Compression is performed on responses with compressible content.
60+
"""
61+
r = await AsyncGZipMiddleware(self.get_response)(self.req)
62+
assert self.decompress(r.content) == self.compressible_string
63+
assert r.get("Content-Encoding") == "gzip"
64+
assert r.get("Content-Length") == str(len(r.content))
65+
66+
async def test_compress_streaming_response(self):
67+
"""
68+
Compression is performed on responses with streaming content.
69+
"""
70+
71+
async def get_stream_response(request):
72+
resp = StreamingHttpResponse(self.sequence)
73+
resp["Content-Type"] = "text/html; charset=UTF-8"
74+
return resp
75+
76+
r = await AsyncGZipMiddleware(get_stream_response)(self.req)
77+
assert self.decompress(b"".join(r)) == b"".join(self.sequence)
78+
assert r.get("Content-Encoding") == "gzip"
79+
assert r.has_header("Content-Length") is False
80+
81+
async def test_compress_async_streaming_response(self):
82+
"""
83+
Compression is performed on responses with async streaming content.
84+
"""
85+
86+
async def get_stream_response(request):
87+
async def iterator():
88+
for chunk in self.sequence:
89+
yield chunk
90+
91+
resp = StreamingHttpResponse(iterator())
92+
resp["Content-Type"] = "text/html; charset=UTF-8"
93+
return resp
94+
95+
r = await AsyncGZipMiddleware(get_stream_response)(self.req)
96+
assert self.decompress(b"".join([chunk async for chunk in r])) == b"".join(
97+
self.sequence
98+
)
99+
assert r.get("Content-Encoding") == "gzip"
100+
assert r.has_header("Content-Length") is False
101+
102+
async def test_compress_streaming_response_unicode(self):
103+
"""
104+
Compression is performed on responses with streaming Unicode content.
105+
"""
106+
107+
async def get_stream_response_unicode(request):
108+
resp = StreamingHttpResponse(self.sequence_unicode)
109+
resp["Content-Type"] = "text/html; charset=UTF-8"
110+
return resp
111+
112+
r = await AsyncGZipMiddleware(get_stream_response_unicode)(self.req)
113+
114+
assert self.decompress(b"".join(r)) == b"".join(
115+
x.encode() for x in self.sequence_unicode
116+
)
117+
assert r.get("Content-Encoding") == "gzip"
118+
assert r.has_header("Content-Length") is False
119+
120+
async def test_compress_file_response(self):
121+
"""
122+
Compression is performed on FileResponse.
123+
"""
124+
with open(__file__, "rb") as file1:
125+
126+
async def get_response(req):
127+
file_resp = FileResponse(file1)
128+
file_resp["Content-Type"] = "text/html; charset=UTF-8"
129+
return file_resp
130+
131+
r = await AsyncGZipMiddleware(get_response)(self.req)
132+
with open(__file__, "rb") as file2:
133+
assert self.decompress(b"".join(r)) == file2.read()
134+
assert r.get("Content-Encoding") == "gzip"
135+
assert r.file_to_stream is not file1
136+
137+
async def test_compress_non_200_response(self):
138+
"""
139+
Compression is performed on responses with a status other than 200
140+
(#10762).
141+
"""
142+
self.resp.status_code = 404
143+
r = await AsyncGZipMiddleware(self.get_response)(self.req)
144+
assert self.decompress(r.content) == self.compressible_string
145+
assert r.get("Content-Encoding") == "gzip"
146+
147+
async def test_no_compress_short_response(self):
148+
"""
149+
Compression isn't performed on responses with short content.
150+
"""
151+
self.resp.content = self.short_string
152+
r = await AsyncGZipMiddleware(self.get_response)(self.req)
153+
assert r.content == self.short_string
154+
assert r.get("Content-Encoding") is None
155+
156+
async def test_no_compress_compressed_response(self):
157+
"""
158+
Compression isn't performed on responses that are already compressed.
159+
"""
160+
self.resp["Content-Encoding"] = "deflate"
161+
r = await AsyncGZipMiddleware(self.get_response)(self.req)
162+
assert r.content == self.compressible_string
163+
assert r.get("Content-Encoding") == "deflate"
164+
165+
async def test_no_compress_incompressible_response(self):
166+
"""
167+
Compression isn't performed on responses with incompressible content.
168+
"""
169+
self.resp.content = self.incompressible_string
170+
r = await AsyncGZipMiddleware(self.get_response)(self.req)
171+
assert r.content == self.incompressible_string
172+
assert r.get("Content-Encoding") is None
173+
174+
async def test_compress_deterministic(self):
175+
"""
176+
Compression results are the same for the same content and don't
177+
include a modification time (since that would make the results
178+
of compression non-deterministic and prevent
179+
ConditionalGetMiddleware from recognizing conditional matches
180+
on gzipped content).
181+
"""
182+
183+
class DeterministicGZipMiddleware(AsyncGZipMiddleware):
184+
max_random_bytes = 0
185+
186+
r1 = await DeterministicGZipMiddleware(self.get_response)(self.req)
187+
r2 = await DeterministicGZipMiddleware(self.get_response)(self.req)
188+
assert r1.content == r2.content
189+
assert self.get_mtime(r1.content) == 0
190+
assert self.get_mtime(r2.content) == 0
191+
192+
async def test_random_bytes(self, mocker):
193+
"""A random number of bytes is added to mitigate the BREACH attack."""
194+
mocker.patch(
195+
"django.utils.text.secrets.randbelow", autospec=True, return_value=3
196+
)
197+
r = await AsyncGZipMiddleware(self.get_response)(self.req)
198+
# The fourth byte of a gzip stream contains flags.
199+
assert r.content[3] == gzip.FNAME
200+
# A 3 byte filename "aaa" and a null byte are added.
201+
assert r.content[10:14] == b"aaa\x00"
202+
assert self.decompress(r.content) == self.compressible_string
203+
204+
async def test_random_bytes_streaming_response(self, mocker):
205+
"""A random number of bytes is added to mitigate the BREACH attack."""
206+
207+
async def get_stream_response(request):
208+
resp = StreamingHttpResponse(self.sequence)
209+
resp["Content-Type"] = "text/html; charset=UTF-8"
210+
return resp
211+
212+
mocker.patch(
213+
"django.utils.text.secrets.randbelow", autospec=True, return_value=3
214+
)
215+
r = await AsyncGZipMiddleware(get_stream_response)(self.req)
216+
content = b"".join(r)
217+
# The fourth byte of a gzip stream contains flags.
218+
assert content[3] == gzip.FNAME
219+
# A 3 byte filename "aaa" and a null byte are added.
220+
assert content[10:14] == b"aaa\x00"
221+
assert self.decompress(content) == b"".join(self.sequence)
222+
223+
224+
class TestETagGZipMiddleware:
225+
"""
226+
ETags are handled properly by GZipMiddleware.
227+
"""
228+
229+
rf = AsyncRequestFactory()
230+
compressible_string = b"a" * 500
231+
232+
async def test_strong_etag_modified(self):
233+
"""
234+
GZipMiddleware makes a strong ETag weak.
235+
"""
236+
237+
async def get_response(req):
238+
response = HttpResponse(self.compressible_string)
239+
response.headers["ETag"] = '"eggs"'
240+
return response
241+
242+
request = self.rf.get("/", headers={"accept-encoding": "gzip, deflate"})
243+
gzip_response = await AsyncGZipMiddleware(get_response)(request)
244+
assert gzip_response.headers["ETag"] == 'W/"eggs"'
245+
246+
async def test_weak_etag_not_modified(self):
247+
"""
248+
GZipMiddleware doesn't modify a weak ETag.
249+
"""
250+
251+
async def get_response(req):
252+
response = HttpResponse(self.compressible_string)
253+
response.headers["ETag"] = 'W/"eggs"'
254+
return response
255+
256+
request = self.rf.get("/", headers={"accept-encoding": "gzip, deflate"})
257+
gzip_response = await AsyncGZipMiddleware(get_response)(request)
258+
assert gzip_response.headers["ETag"] == 'W/"eggs"'
259+
260+
async def test_etag_match(self):
261+
"""
262+
GZipMiddleware allows 304 Not Modified responses.
263+
"""
264+
265+
async def get_response(req):
266+
return HttpResponse(self.compressible_string)
267+
268+
async def get_cond_response(req):
269+
return await AsyncConditionalGetMiddleware(get_response)(req)
270+
271+
request = self.rf.get("/", headers={"accept-encoding": "gzip, deflate"})
272+
response = await AsyncGZipMiddleware(get_cond_response)(request)
273+
gzip_etag = response.headers["ETag"]
274+
next_request = self.rf.get(
275+
"/",
276+
headers={"accept-encoding": "gzip, deflate", "if-none-match": gzip_etag},
277+
)
278+
next_response = await AsyncConditionalGetMiddleware(get_response)(next_request)
279+
assert next_response.status_code == 304

tests/test_middlewares/test_middleware_mixin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.http.response import HttpResponse
77

88
from django_async_extensions.middleware.base import AsyncMiddlewareMixin
9+
from django_async_extensions.middleware.gzip import AsyncGZipMiddleware
910
from django_async_extensions.middleware.http import AsyncConditionalGetMiddleware
1011
from django_async_extensions.middleware.locale import AsyncLocaleMiddleware
1112
from django_async_extensions.middleware.security import AsyncSecurityMiddleware
@@ -37,6 +38,7 @@ class TestMiddlewareMixin:
3738
AsyncSecurityMiddleware,
3839
AsyncLocaleMiddleware,
3940
AsyncConditionalGetMiddleware,
41+
AsyncGZipMiddleware,
4042
]
4143

4244
def test_repr(self):

0 commit comments

Comments
 (0)