Skip to content

Commit 56aee43

Browse files
authored
add comments to test functions in order to better document the intended design (a-luna#48)
Fixes a-luna#41
1 parent e42e4e9 commit 56aee43

File tree

4 files changed

+71
-18
lines changed

4 files changed

+71
-18
lines changed

src/fastapi_redis_cache/cache.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ async def get_api_response_async(func, *args, **kwargs):
8989

9090

9191
def calculate_ttl(expire: Union[int, timedelta]) -> int:
92+
""""Converts expire time to total seconds and ensures that ttl is capped at one year."""
9293
if isinstance(expire, timedelta):
9394
expire = int(expire.total_seconds())
9495
return min(expire, ONE_YEAR_IN_SECONDS)

src/fastapi_redis_cache/client.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,3 @@ def get_etag(cached_data: Union[str, bytes, Dict]) -> str:
158158
def get_log_time():
159159
"""Get a timestamp to include with a log message."""
160160
return datetime.now().strftime(LOG_TIMESTAMP)
161-
162-
@staticmethod
163-
def hasmethod(obj, method_name):
164-
"""Return True if obj.method_name exists and is callable. Otherwise, return False."""
165-
obj_method = getattr(obj, method_name, None)
166-
return callable(obj_method) if obj_method else False

tests/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ def cache_never_expire(request: Request, response: Response):
1616

1717

1818
@app.get("/cache_expires")
19-
@cache(expire=timedelta(seconds=8))
19+
@cache(expire=timedelta(seconds=5))
2020
async def cache_expires():
21-
return {"success": True, "message": "this data should be cached for eight seconds"}
21+
return {"success": True, "message": "this data should be cached for five seconds"}
2222

2323

2424
@app.get("/cache_json_encoder")

tests/test_cache.py

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
from datetime import datetime
55
from decimal import Decimal
66

7+
import pytest
78
from fastapi.testclient import TestClient
9+
from fastapi_redis_cache.client import HTTP_TIME
810

911
from fastapi_redis_cache.util import deserialize_json
1012
from tests.main import app
@@ -14,13 +16,16 @@
1416

1517

1618
def test_cache_never_expire():
19+
# Initial request, X-FastAPI-Cache header field should equal "Miss"
1720
response = client.get("/cache_never_expire")
1821
assert response.status_code == 200
1922
assert response.json() == {"success": True, "message": "this data can be cached indefinitely"}
2023
assert "x-fastapi-cache" in response.headers and response.headers["x-fastapi-cache"] == "Miss"
2124
assert "cache-control" in response.headers
2225
assert "expires" in response.headers
2326
assert "etag" in response.headers
27+
28+
# Send request to same endpoint, X-FastAPI-Cache header field should now equal "Hit"
2429
response = client.get("/cache_never_expire")
2530
assert response.status_code == 200
2631
assert response.json() == {"success": True, "message": "this data can be cached indefinitely"}
@@ -31,38 +36,72 @@ def test_cache_never_expire():
3136

3237

3338
def test_cache_expires():
34-
start = datetime.now()
39+
# Store time when response data was added to cache
40+
added_at_utc = datetime.utcnow()
41+
42+
# Initial request, X-FastAPI-Cache header field should equal "Miss"
3543
response = client.get("/cache_expires")
3644
assert response.status_code == 200
37-
assert response.json() == {"success": True, "message": "this data should be cached for eight seconds"}
45+
assert response.json() == {"success": True, "message": "this data should be cached for five seconds"}
3846
assert "x-fastapi-cache" in response.headers and response.headers["x-fastapi-cache"] == "Miss"
3947
assert "cache-control" in response.headers
4048
assert "expires" in response.headers
4149
assert "etag" in response.headers
50+
51+
# Store eTag value from response header
4252
check_etag = response.headers["etag"]
53+
54+
# Send request, X-FastAPI-Cache header field should now equal "Hit"
4355
response = client.get("/cache_expires")
4456
assert response.status_code == 200
45-
assert response.json() == {"success": True, "message": "this data should be cached for eight seconds"}
57+
assert response.json() == {"success": True, "message": "this data should be cached for five seconds"}
4658
assert "x-fastapi-cache" in response.headers and response.headers["x-fastapi-cache"] == "Hit"
47-
assert "cache-control" in response.headers
48-
assert "expires" in response.headers
59+
60+
# Verify eTag value matches the value stored from the initial response
4961
assert "etag" in response.headers
5062
assert response.headers["etag"] == check_etag
51-
elapsed = (datetime.now() - start).total_seconds()
52-
remaining = 8 - elapsed
53-
if remaining > 0:
54-
time.sleep(remaining)
63+
64+
# Store 'max-age' value of 'cache-control' header field
65+
assert "cache-control" in response.headers
66+
match = MAX_AGE_REGEX.search(response.headers.get("cache-control"))
67+
assert match
68+
ttl = int(match.groupdict()["ttl"])
69+
assert ttl <= 5
70+
71+
# Store value of 'expires' header field
72+
assert "expires" in response.headers
73+
expire_at_utc = datetime.strptime(response.headers["expires"], HTTP_TIME)
74+
75+
# Wait until expire time has passed
76+
now = datetime.utcnow()
77+
while expire_at_utc > now:
78+
time.sleep(1)
79+
now = datetime.utcnow()
80+
81+
# Wait one additional second to ensure redis has deleted the expired response data
82+
time.sleep(1)
83+
second_request_utc = datetime.utcnow()
84+
85+
# Verify that the time elapsed since the data was added to the cache is greater than the ttl value
86+
elapsed = (second_request_utc - added_at_utc).total_seconds()
87+
assert elapsed > ttl
88+
89+
# Send request, X-FastAPI-Cache header field should equal "Miss" since the cached value has been evicted
5590
response = client.get("/cache_expires")
5691
assert response.status_code == 200
57-
assert response.json() == {"success": True, "message": "this data should be cached for eight seconds"}
92+
assert response.json() == {"success": True, "message": "this data should be cached for five seconds"}
5893
assert "x-fastapi-cache" in response.headers and response.headers["x-fastapi-cache"] == "Miss"
5994
assert "cache-control" in response.headers
6095
assert "expires" in response.headers
6196
assert "etag" in response.headers
97+
98+
# Check eTag value again. Since data is the same, the value should still match
6299
assert response.headers["etag"] == check_etag
63100

64101

65102
def test_cache_json_encoder():
103+
# In order to verify that our custom BetterJsonEncoder is working correctly, the /cache_json_encoder
104+
# endpoint returns a dict containing datetime.datetime, datetime.date and decimal.Decimal objects.
66105
response = client.get("/cache_json_encoder")
67106
assert response.status_code == 200
68107
response_json = response.json()
@@ -75,13 +114,18 @@ def test_cache_json_encoder():
75114
"val": "3.140000000000000124344978758017532527446746826171875",
76115
},
77116
}
117+
118+
# To verify that our custom object_hook function which deserializes types that are not typically
119+
# JSON-serializable is working correctly, we test it with the serialized values sent in the response.
78120
json_dict = deserialize_json(json.dumps(response_json))
79121
assert json_dict["start_time"] == datetime(2021, 4, 20, 7, 17, 17)
80122
assert json_dict["finish_by"] == datetime(2021, 4, 21)
81123
assert json_dict["final_calc"] == Decimal(3.14)
82124

83125

84126
def test_cache_control_no_cache():
127+
# Simple test that verifies if a request is recieved with the cache-control header field containing "no-cache",
128+
# no caching behavior is performed
85129
response = client.get("/cache_never_expire", headers={"cache-control": "no-cache"})
86130
assert response.status_code == 200
87131
assert response.json() == {"success": True, "message": "this data can be cached indefinitely"}
@@ -92,6 +136,8 @@ def test_cache_control_no_cache():
92136

93137

94138
def test_cache_control_no_store():
139+
# Simple test that verifies if a request is recieved with the cache-control header field containing "no-store",
140+
# no caching behavior is performed
95141
response = client.get("/cache_never_expire", headers={"cache-control": "no-store"})
96142
assert response.status_code == 200
97143
assert response.json() == {"success": True, "message": "this data can be cached indefinitely"}
@@ -102,29 +148,39 @@ def test_cache_control_no_store():
102148

103149

104150
def test_if_none_match():
151+
# Initial request, response data is added to cache
105152
response = client.get("/cache_never_expire")
106153
assert response.status_code == 200
107154
assert response.json() == {"success": True, "message": "this data can be cached indefinitely"}
108155
assert "x-fastapi-cache" in response.headers and response.headers["x-fastapi-cache"] == "Miss"
109156
assert "cache-control" in response.headers
110157
assert "expires" in response.headers
111158
assert "etag" in response.headers
159+
160+
# Store correct eTag value from response header
112161
etag = response.headers["etag"]
162+
# Create another eTag value that is different from the correct value
113163
invalid_etag = "W/-5480454928453453778"
164+
165+
# Send request to same endpoint where If-None-Match header contains both valid and invalid eTag values
114166
response = client.get("/cache_never_expire", headers={"if-none-match": f"{etag}, {invalid_etag}"})
115167
assert response.status_code == 304
116168
assert not response.content
117169
assert "x-fastapi-cache" in response.headers and response.headers["x-fastapi-cache"] == "Hit"
118170
assert "cache-control" in response.headers
119171
assert "expires" in response.headers
120172
assert "etag" in response.headers
173+
174+
# Send request to same endpoint where If-None-Match header contains just the wildcard (*) character
121175
response = client.get("/cache_never_expire", headers={"if-none-match": "*"})
122176
assert response.status_code == 304
123177
assert not response.content
124178
assert "x-fastapi-cache" in response.headers and response.headers["x-fastapi-cache"] == "Hit"
125179
assert "cache-control" in response.headers
126180
assert "expires" in response.headers
127181
assert "etag" in response.headers
182+
183+
# Send request to same endpoint where If-None-Match header contains only the invalid eTag value
128184
response = client.get("/cache_never_expire", headers={"if-none-match": invalid_etag})
129185
assert response.status_code == 200
130186
assert response.json() == {"success": True, "message": "this data can be cached indefinitely"}
@@ -135,6 +191,8 @@ def test_if_none_match():
135191

136192

137193
def test_partial_cache_one_hour():
194+
# Simple test that verifies that the @cache_for_one_hour partial function version of the @cache decorator
195+
# is working correctly.
138196
response = client.get("/cache_one_hour")
139197
assert response.status_code == 200
140198
assert response.json() == {"success": True, "message": "this data should be cached for one hour"}

0 commit comments

Comments
 (0)