Skip to content

Commit 5a07de6

Browse files
author
codeEmpress1
authored
Wd 31509 add cache to webapp/store (#5485)
* add cache to webapp/store * make a base test class for clearing cache * remove user-specific cache * set sitemap cache * add tests for cached explore page
1 parent d40d1b7 commit 5a07de6

7 files changed

Lines changed: 296 additions & 27 deletions

File tree

tests/base_test_cases.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from flask_testing import TestCase as FlaskTestCase
2+
from unittest import TestCase as UnitTestCase
3+
from cache.cache_utility import redis_cache
4+
5+
6+
class BaseFlaskTestCase(FlaskTestCase):
7+
"""Base test class that clears cache before each test."""
8+
9+
def setUp(self):
10+
super().setUp()
11+
# Clear cache before each test
12+
if redis_cache.redis_available:
13+
try:
14+
redis_cache.client.flushdb()
15+
except Exception:
16+
pass
17+
else:
18+
redis_cache.fallback.clear()
19+
20+
21+
class BaseUnitTestCase(UnitTestCase):
22+
"""Base unit test class that clears cache before each test."""
23+
24+
def setUp(self):
25+
super().setUp()
26+
# Clear cache before each test
27+
if redis_cache.redis_available:
28+
try:
29+
redis_cache.client.flushdb()
30+
except Exception:
31+
pass
32+
else:
33+
redis_cache.fallback.clear()

tests/endpoints/endpoint_testing.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
from unittest import TestCase
21
from unittest.mock import patch
32

3+
from tests.base_test_cases import BaseUnitTestCase
44
from webapp.app import create_app
55
from webapp.authentication import get_publishergw_authorization_header
6-
from cache.cache_utility import redis_cache
76

87

9-
class TestEndpoints(TestCase):
8+
class TestEndpoints(BaseUnitTestCase):
109
def _log_in(self, client):
1110
test_macaroon = "test_macaroon"
1211
with client.session_transaction() as s:
@@ -25,15 +24,7 @@ def _log_in(self, client):
2524
return get_publishergw_authorization_header(test_macaroon)
2625

2726
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-
27+
super().setUp()
3728
self.app = create_app(testing=True)
3829
self.client = self.app.test_client()
3930
self._log_in(self.client)

tests/store/tests_details.py

Lines changed: 193 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import responses
22
from urllib.parse import urlencode
3-
from flask_testing import TestCase
43
from webapp.app import create_app
4+
from tests.base_test_cases import BaseFlaskTestCase
5+
from unittest.mock import patch
6+
from cache.cache_utility import redis_cache
7+
8+
POPULAR_PATH = "webapp.store.views.snap_recommendations.get_popular"
9+
RECENT_PATH = "webapp.store.views.snap_recommendations.get_recent"
10+
TREND_PATH = "webapp.store.views.snap_recommendations.get_trending"
11+
TOP_PATH = "webapp.store.views.snap_recommendations.get_top_rated"
12+
CATEGORIES_PATH = "webapp.store.views.device_gateway.get_categories"
513

614

715
EMPTY_EXTRA_DETAILS_PAYLOAD = {"aliases": None, "package_name": "vault"}
@@ -43,8 +51,9 @@
4351
}
4452

4553

46-
class GetDetailsPageTest(TestCase):
54+
class GetDetailsPageTest(BaseFlaskTestCase):
4755
def setUp(self):
56+
super().setUp()
4857
self.snap_name = "toto"
4958
self.api_url = "".join(
5059
[
@@ -388,6 +397,188 @@ def test_extra_details(self):
388397
],
389398
)
390399

400+
@responses.activate
401+
def test_explore_uses_redis_cache(self):
402+
"""When Redis has cached explore data, the recommendation APIs
403+
and device gateway should not be called and the view should
404+
return successfully using the cached values.
405+
"""
406+
# seed redis
407+
popular = [
408+
{
409+
"details": {
410+
"name": "/pop1",
411+
"icon": "",
412+
"title": "Pop 1",
413+
"publisher": "Pub 1",
414+
"developer_validation": None,
415+
"summary": "Popular snap",
416+
},
417+
}
418+
]
419+
recent = [
420+
{
421+
"details": {
422+
"name": "/recent1",
423+
"icon": "",
424+
"title": "Recent 1",
425+
"publisher": "Pub 2",
426+
"developer_validation": None,
427+
"summary": "Recent snap",
428+
},
429+
}
430+
]
431+
trending = [
432+
{
433+
"details": {
434+
"name": "/trend1",
435+
"icon": "",
436+
"title": "Trend 1",
437+
"publisher": "Pub 3",
438+
"developer_validation": None,
439+
"summary": "Trending snap",
440+
},
441+
}
442+
]
443+
top_rated = [
444+
{
445+
"details": {
446+
"name": "/top1",
447+
"icon": "",
448+
"title": "Top 1",
449+
"publisher": "Pub 4",
450+
"developer_validation": None,
451+
"summary": "Top rated snap",
452+
},
453+
}
454+
]
455+
categories = [{"slug": "cat1", "name": "Cat 1"}]
456+
457+
redis_cache.set("explore:popular-snaps", popular, ttl=3600)
458+
redis_cache.set("explore:recent-snaps", recent, ttl=3600)
459+
redis_cache.set("explore:trending-snaps", trending, ttl=3600)
460+
redis_cache.set("explore:top-rated-snaps", top_rated, ttl=3600)
461+
redis_cache.set("explore:categories", categories, ttl=3600)
462+
463+
with patch(POPULAR_PATH) as mock_popular:
464+
with patch(RECENT_PATH) as mock_recent:
465+
with patch(TREND_PATH) as mock_trending:
466+
with patch(TOP_PATH) as mock_top_rated:
467+
with patch(CATEGORIES_PATH) as mock_categories:
468+
response = self.client.get("/explore")
469+
470+
self.assert200(response)
471+
472+
mock_popular.assert_not_called()
473+
mock_recent.assert_not_called()
474+
mock_trending.assert_not_called()
475+
mock_top_rated.assert_not_called()
476+
mock_categories.assert_not_called()
477+
478+
@responses.activate
479+
def test_explore_populates_cache_when_empty(self):
480+
"""When Redis cache is empty, the recommendation/device methods
481+
should be called and their results stored in Redis for subsequent
482+
requests.
483+
"""
484+
485+
with patch(
486+
POPULAR_PATH,
487+
return_value=[
488+
{
489+
"details": {
490+
"name": "/popx",
491+
"icon": "",
492+
"title": "Pop X",
493+
"publisher": "Pub X",
494+
"developer_validation": None,
495+
"summary": "Popular x",
496+
}
497+
}
498+
],
499+
) as mock_popular:
500+
with patch(
501+
RECENT_PATH,
502+
return_value=[
503+
{
504+
"details": {
505+
"name": "/recentx",
506+
"icon": "",
507+
"title": "Recent X",
508+
"publisher": "Pub RX",
509+
"developer_validation": None,
510+
"summary": "Recent x",
511+
}
512+
}
513+
],
514+
) as mock_recent:
515+
with patch(
516+
TREND_PATH,
517+
return_value=[
518+
{
519+
"details": {
520+
"name": "/trendx",
521+
"icon": "",
522+
"title": "Trend X",
523+
"publisher": "Pub TX",
524+
"developer_validation": None,
525+
"summary": "Trend x",
526+
}
527+
}
528+
],
529+
) as mock_trending:
530+
with patch(
531+
TOP_PATH,
532+
return_value=[
533+
{
534+
"details": {
535+
"name": "/topx",
536+
"icon": "",
537+
"title": "Top X",
538+
"publisher": "Pub TX",
539+
"developer_validation": None,
540+
"summary": "Top x",
541+
}
542+
}
543+
],
544+
) as mock_top_rated:
545+
with patch(
546+
CATEGORIES_PATH,
547+
return_value=[{"slug": "c1", "name": "C1"}],
548+
) as mock_categories:
549+
response = self.client.get("/explore")
550+
551+
self.assert200(response)
552+
553+
# ensure the methods were called to populate cache
554+
self.assertTrue(mock_popular.called)
555+
self.assertTrue(mock_recent.called)
556+
self.assertTrue(mock_trending.called)
557+
self.assertTrue(mock_top_rated.called)
558+
self.assertTrue(mock_categories.called)
559+
# cached values should now exist
560+
pop_cached = redis_cache.get(
561+
"explore:popular-snaps"
562+
)
563+
recent_cached = redis_cache.get(
564+
"explore:recent-snaps"
565+
)
566+
trend_cached = redis_cache.get(
567+
"explore:trending-snaps"
568+
)
569+
top_cached = redis_cache.get(
570+
"explore:top-rated-snaps"
571+
)
572+
categories_cached = redis_cache.get(
573+
"explore:categories"
574+
)
575+
576+
assert pop_cached is not None
577+
assert recent_cached is not None
578+
assert trend_cached is not None
579+
assert top_cached is not None
580+
assert categories_cached is not None
581+
391582

392583
if __name__ == "__main__":
393584
import unittest

tests/store/tests_embedded_card.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import responses
22
from urllib.parse import urlencode
3-
from flask_testing import TestCase
43
from webapp.app import create_app
4+
from tests.base_test_cases import BaseFlaskTestCase
55

66

7-
class GetEmbeddedCardTest(TestCase):
7+
class GetEmbeddedCardTest(BaseFlaskTestCase):
88
snap_payload = {
99
"snap-id": "id",
1010
"name": "snapName",
@@ -43,6 +43,7 @@ class GetEmbeddedCardTest(TestCase):
4343
}
4444

4545
def setUp(self):
46+
super().setUp()
4647
self.snap_name = "toto"
4748
self.api_url = "".join(
4849
[

tests/store/tests_github_badge.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import responses
22
from urllib.parse import urlencode
3-
from flask_testing import TestCase
43
from webapp.app import create_app
4+
from tests.base_test_cases import BaseFlaskTestCase
55

66

7-
class GetGitHubBadgeTest(TestCase):
7+
class GetGitHubBadgeTest(BaseFlaskTestCase):
88
snap_payload = {
99
"snap-id": "id",
1010
"name": "snapName",
@@ -56,6 +56,7 @@ class GetGitHubBadgeTest(TestCase):
5656
}
5757

5858
def setUp(self):
59+
super().setUp()
5960
self.snap_name = "toto"
6061
self.api_url = "".join(
6162
[

webapp/store/snap_details_views.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import webapp.store.logic as logic
1212
from webapp import authentication
1313
from webapp.markdown import parse_markdown_description
14+
from cache.cache_utility import redis_cache
1415

1516
from canonicalwebteam.flask_base.decorators import (
1617
exclude_xframe_options_header,
@@ -197,7 +198,6 @@ def _get_context_snap_details(snap_name, supported_architectures=None):
197198
"links": details["snap"].get("links"),
198199
"updates": updates,
199200
}
200-
201201
return context
202202

203203
@store.route('/<regex("' + snap_regex + '"):snap_name>')
@@ -465,15 +465,21 @@ def snap_distro_install(snap_name, distro):
465465
"distro_install_steps": distro_data["install"],
466466
}
467467
)
468-
468+
cached_featured_snaps = redis_cache.get(
469+
"featured_snaps_install_pages", expected_type=list
470+
)
471+
if cached_featured_snaps:
472+
context.update({"featured_snaps": cached_featured_snaps})
473+
return flask.render_template(
474+
"store/snap-distro-install.html", **context
475+
)
469476
try:
470477
featured_snaps_results = device_gateway.get_featured_items(
471478
size=13, page=1
472479
).get("results", [])
473480

474481
except StoreApiError:
475482
featured_snaps_results = []
476-
477483
featured_snaps = [
478484
snap
479485
for snap in featured_snaps_results
@@ -482,7 +488,9 @@ def snap_distro_install(snap_name, distro):
482488

483489
for snap in featured_snaps:
484490
snap["icon_url"] = helpers.get_icon(snap["media"])
485-
491+
redis_cache.set(
492+
"featured_snaps_install_pages", featured_snaps, ttl=3600
493+
)
486494
context.update({"featured_snaps": featured_snaps})
487495
return flask.render_template(
488496
"store/snap-distro-install.html", **context

0 commit comments

Comments
 (0)