44from datetime import datetime
55from decimal import Decimal
66
7+ import pytest
78from fastapi .testclient import TestClient
9+ from fastapi_redis_cache .client import HTTP_TIME
810
911from fastapi_redis_cache .util import deserialize_json
1012from tests .main import app
1416
1517
1618def 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
3338def 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
65102def 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
84126def 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
94138def 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
104150def 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
137193def 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