Skip to content

Commit ac12365

Browse files
author
codeEmpress1
authored
Add cache to /endpoints/publisher/listing (#5462)
* add cache utility * add cache to snap listing page * update tests * remove user-id from key
1 parent 49b1802 commit ac12365

7 files changed

Lines changed: 433 additions & 1 deletion

File tree

cache/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Local Redis for Development
2+
3+
This setup provides a local Redis instance for development and testing purposes.
4+
5+
## Getting Started
6+
7+
1. Ensure Docker is installed.
8+
2. ```cd cache``` and run ```docker compose run redis-cli```
9+
10+
3. Then you can interact with the redis server.
11+
To check all saved keys use:
12+
```
13+
KEYS *
14+
```
15+
4. To exit the interactive promt, run
16+
```
17+
exit
18+
```
19+
5. To stop the container, run:
20+
```
21+
docker compose down
22+
```
23+
For more Redis CLI commands, check the docs at https://redis.io/learn/howtos/quick-start/cheat-sheet

cache/cache_utility.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from canonicalwebteam.stores_web_redis.utility import RedisCache
2+
from webapp.config import APP_NAME
3+
4+
redis_cache = RedisCache(
5+
namespace=APP_NAME,
6+
maxsize=1000,
7+
ttl=300,
8+
)

cache/docker-compose.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
services:
2+
redis:
3+
image: redis:7
4+
ports:
5+
- "6379:6379"
6+
volumes:
7+
- redis-data:/data
8+
command: ["redis-server", "--appendonly", "yes"]
9+
healthcheck:
10+
test: ["CMD", "redis-cli", "-h", "localhost", "-p", "6379", "ping"]
11+
interval: 5s
12+
timeout: 2s
13+
retries: 5
14+
15+
redis-cli:
16+
image: redis:7
17+
entrypoint: ["redis-cli", "-h", "redis", "-p", "6379"]
18+
depends_on:
19+
redis:
20+
condition: service_healthy
21+
22+
volumes:
23+
redis-data:

tests/endpoints/endpoint_testing.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from webapp.app import create_app
55
from webapp.authentication import get_publishergw_authorization_header
6+
from cache.cache_utility import redis_cache
67

78

89
class TestEndpoints(TestCase):
@@ -24,6 +25,15 @@ def _log_in(self, client):
2425
return get_publishergw_authorization_header(test_macaroon)
2526

2627
def setUp(self):
28+
# Clear cache before each test
29+
if redis_cache.redis_available:
30+
try:
31+
redis_cache.client.flushdb()
32+
except Exception:
33+
pass
34+
else:
35+
redis_cache.fallback.clear()
36+
2737
self.app = create_app(testing=True)
2838
self.client = self.app.test_client()
2939
self._log_in(self.client)

tests/publisher/snaps/tests_listing.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
import responses
22
from tests.publisher.endpoint_testing import BaseTestCases
3+
from cache.cache_utility import redis_cache
34

45

56
class ListingPageNotAuth(BaseTestCases.EndpointLoggedOut):
67
def setUp(self):
8+
# Clear cache before each test
9+
if redis_cache.redis_available:
10+
try:
11+
redis_cache.client.flushdb()
12+
except Exception:
13+
pass
14+
else:
15+
redis_cache.fallback.clear()
16+
717
snap_name = "test-snap"
818
endpoint_url = "/api/{}/listing".format(snap_name)
919

@@ -12,6 +22,15 @@ def setUp(self):
1222

1323
class GetListingPage(BaseTestCases.EndpointLoggedInErrorHandling):
1424
def setUp(self):
25+
# Clear cache before each test
26+
if redis_cache.redis_available:
27+
try:
28+
redis_cache.client.flushdb()
29+
except Exception:
30+
pass
31+
else:
32+
redis_cache.fallback.clear()
33+
1534
snap_name = "test-snap"
1635

1736
api_url = "https://dashboard.snapcraft.io/dev/api/snaps/info/{}"
@@ -275,3 +294,113 @@ def test_failed_categories_api(self):
275294
self.check_call_by_api_url(responses.calls)
276295

277296
assert response.status_code == 200
297+
298+
@responses.activate
299+
def test_cache_hit_on_second_request(self):
300+
"""Test that second GET request uses cache instead of API"""
301+
payload = {
302+
"snap_id": "id",
303+
"snap_name": self.snap_name,
304+
"title": "Snap title",
305+
"summary": "This is a summary",
306+
"description": "This is a description",
307+
"media": [],
308+
"publisher": {
309+
"display-name": "The publisher",
310+
"username": "toto",
311+
},
312+
"private": True,
313+
"channel_maps_list": [{"map": [{"info": "info"}]}],
314+
"contact": "contact adress",
315+
"website": "website_url",
316+
"public_metrics_enabled": True,
317+
"public_metrics_blacklist": False,
318+
"license": "License",
319+
"video_urls": [],
320+
"categories": {"items": []},
321+
"status": "published",
322+
"update_metadata_on_release": True,
323+
"links": {"website": ["https://example.com"]},
324+
}
325+
326+
responses.add(responses.GET, self.api_url, json=payload, status=200)
327+
responses.add(
328+
responses.GET,
329+
"https://api.snapcraft.io/v2/snaps/categories?type=shared",
330+
json=[],
331+
status=200,
332+
)
333+
334+
# First request should hit API
335+
response1 = self.client.get(self.endpoint_url)
336+
assert response1.status_code == 200
337+
338+
snap_info_calls_before = [
339+
call
340+
for call in responses.calls
341+
if self.api_url in call.request.url
342+
]
343+
assert len(snap_info_calls_before) == 1
344+
345+
# Second request should use cache (no additional snap info API calls)
346+
response2 = self.client.get(self.endpoint_url)
347+
assert response2.status_code == 200
348+
snap_info_calls_after = [
349+
call
350+
for call in responses.calls
351+
if self.api_url in call.request.url
352+
]
353+
# Should have same number of calls (cache hit)
354+
assert len(snap_info_calls_after) == len(snap_info_calls_before)
355+
356+
@responses.activate
357+
def test_cache_stores_data_correctly(self):
358+
"""Test that cached data matches API response"""
359+
payload = {
360+
"snap_id": "cached-id",
361+
"snap_name": self.snap_name,
362+
"title": "Cached Snap Title",
363+
"summary": "This is a cached summary",
364+
"description": "This is a cached description",
365+
"media": [],
366+
"publisher": {
367+
"display-name": "Cached Publisher",
368+
"username": "cached",
369+
},
370+
"private": False,
371+
"channel_maps_list": [{"map": [{"info": "info"}]}],
372+
"contact": "cached@example.com",
373+
"website": "https://cached.example.com",
374+
"public_metrics_enabled": True,
375+
"public_metrics_blacklist": False,
376+
"license": "MIT",
377+
"video_urls": [],
378+
"categories": {"items": []},
379+
"status": "published",
380+
"update_metadata_on_release": False,
381+
"links": {"website": ["https://cached.example.com"]},
382+
}
383+
384+
responses.add(responses.GET, self.api_url, json=payload, status=200)
385+
responses.add(
386+
responses.GET,
387+
"https://api.snapcraft.io/v2/snaps/categories?type=shared",
388+
json=[],
389+
status=200,
390+
)
391+
392+
# First request
393+
response1 = self.client.get(self.endpoint_url)
394+
assert response1.status_code == 200
395+
data1 = response1.get_json()
396+
397+
# Second request (from cache)
398+
response2 = self.client.get(self.endpoint_url)
399+
assert response2.status_code == 200
400+
data2 = response2.get_json()
401+
402+
# Verify data matches
403+
assert data1["data"]["snap_id"] == data2["data"]["snap_id"]
404+
assert data1["data"]["title"] == data2["data"]["title"]
405+
assert data1["data"]["summary"] == data2["data"]["summary"]
406+
assert data1["data"]["description"] == data2["data"]["description"]

0 commit comments

Comments
 (0)