From bab6f6df1676b65f891ace94527d4596f176dcbf Mon Sep 17 00:00:00 2001 From: Karl Dubost Date: Wed, 8 Feb 2017 16:26:40 +0900 Subject: [PATCH] Issue #609 - Implement Cache-Policy decorator Issue #609 - Removes code dust --- tests/test_http_caching.py | 69 ++++++++++++++++++++++++++++++++++++++ webcompat/api/endpoints.py | 1 - webcompat/helpers.py | 33 +++++++++++++++++- webcompat/views.py | 13 +++++-- 4 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 tests/test_http_caching.py diff --git a/tests/test_http_caching.py b/tests/test_http_caching.py new file mode 100644 index 000000000..27e7727da --- /dev/null +++ b/tests/test_http_caching.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +'''Tests for HTTP Caching on webcompat resources.''' + +import os.path +import sys +import unittest + +# Add webcompat module to import path +sys.path.append(os.path.realpath(os.pardir)) +import webcompat # nopep8 + +# Any request that depends on parsing HTTP Headers (basically anything +# on the index route, will need to include the following: environ_base=headers +html_headers = { + 'HTTP_USER_AGENT': ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; ' + 'rv:53.0) Gecko/20100101 Firefox/53.0'), + 'HTTP_ACCEPT': 'text/html'} + + +class TestHTTPCaching(unittest.TestCase): + def setUp(self): + webcompat.app.config['TESTING'] = True + self.app = webcompat.app.test_client() + + def tearDown(self): + pass + + def test_issue_has_etag(self): + '''Check ETAG for issues.''' + rv = self.app.get('/issues/100', environ_base=html_headers) + response_headers = rv.headers + self.assertIn('etag', response_headers) + self.assertIsNotNone(response_headers['etag']) + + def test_cache_control(self): + '''Check Cache-Control for issues.''' + rv = self.app.get('/issues/100', environ_base=html_headers) + response_headers = rv.headers + self.assertIn('cache-control', response_headers) + self.assertEqual(response_headers['cache-control'], + 'private, max-age=86400') + + def test_not_modified_status(self): + '''Checks if we receive a 304 Not Modified.''' + for uri in ['/about', + '/contributors', + '/issues', + '/issues/100', + '/privacy']: + rv = self.app.get(uri, environ_base=html_headers) + response_headers = rv.headers + etag = response_headers['etag'] + rv2 = self.app.get(uri, + environ_base=html_headers, + headers={'If-None-Match': etag}) + self.assertEqual(rv2.status_code, 304) + self.assertEqual(rv2.data, '') + self.assertIn('cache-control', response_headers) + self.assertEqual(response_headers['cache-control'], + 'private, max-age=86400') + + +if __name__ == '__main__': + unittest.main() diff --git a/webcompat/api/endpoints.py b/webcompat/api/endpoints.py index e6eb0c1c6..4f43588db 100644 --- a/webcompat/api/endpoints.py +++ b/webcompat/api/endpoints.py @@ -9,7 +9,6 @@ This is used to make API calls to GitHub, either via a logged-in users credentials or as a proxy on behalf of anonymous or unauthenticated users.''' -import json from flask import abort from flask import Blueprint diff --git a/webcompat/helpers.py b/webcompat/helpers.py index d3dc59a59..e2a94f2eb 100644 --- a/webcompat/helpers.py +++ b/webcompat/helpers.py @@ -5,6 +5,8 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. from datetime import datetime +from functools import wraps +from functools import update_wrapper import hashlib import json import math @@ -16,10 +18,10 @@ from babel.dates import format_timedelta from flask import abort from flask import g +from flask import make_response from flask import request from flask import session from form import IssueForm -from functools import wraps from ua_parser import user_agent_parser from webcompat import app @@ -456,3 +458,32 @@ def api_request(method, path, params=None, data=None): get_response_headers(resource)) else: abort(404) + + +def cache_policy(private=True, uri_max_age=86400): + '''Implements a HTTP Cache Decorator. + + Adds Cache-Control headers. + * max-age has a 1 day default (86400s) + * and makes it private by default + Adds Etag based on HTTP Body. + Sends a 304 Not Modified in case of If-None-Match. + ''' + def set_policy(view): + @wraps(view) + def policy(*args, **kwargs): + response = make_response(view(*args, **kwargs)) + # we choose if the resource is private or public + if private: + response.cache_control.private = True + else: + response.cache_control.public = True + # Instructs how long the Cache should keep the resource + response.cache_control.max_age = uri_max_age + # Etag is based on the HTTP body + response.add_etag(response.data) + # to send a 304 Not Modified instead of a full HTTP response + response.make_conditional(request) + return response + return update_wrapper(policy, view) + return set_policy diff --git a/webcompat/views.py b/webcompat/views.py index 456abcfb7..e6e876f1c 100644 --- a/webcompat/views.py +++ b/webcompat/views.py @@ -7,11 +7,11 @@ import json import logging import os -import urllib from flask import abort from flask import flash from flask import g +from flask import make_response from flask import redirect from flask import render_template from flask import request @@ -21,6 +21,7 @@ from form import AUTH_REPORT from form import PROXY_REPORT +from helpers import cache_policy from helpers import get_browser_name from helpers import get_form from helpers import get_referer @@ -143,6 +144,7 @@ def index(): @app.route('/issues') +@cache_policy(private=True, uri_max_age=86400) def show_issues(): '''Route to display global issues view.''' if g.user: @@ -152,6 +154,7 @@ def show_issues(): @app.route('/issues/new', methods=['GET', 'POST']) +@cache_policy(private=True, uri_max_age=86400) def create_issue(): """Creates a new issue. @@ -201,6 +204,7 @@ def create_issue(): @app.route('/issues/') +@cache_policy(private=True, uri_max_age=86400) def show_issue(number): '''Route to display a single issue.''' if g.user: @@ -208,7 +212,9 @@ def show_issue(number): if session.get('show_thanks'): flash(number, 'thanks') session.pop('show_thanks') - return render_template('issue.html', number=number) + content = render_template('issue.html', number=number) + response = make_response(content) + return response @app.route('/me') @@ -279,6 +285,7 @@ def get_test_helper(filename): @app.route('/about') +@cache_policy(private=True, uri_max_age=86400) def about(): '''Route to display about page.''' if g.user: @@ -287,6 +294,7 @@ def about(): @app.route('/privacy') +@cache_policy(private=True, uri_max_age=86400) def privacy(): '''Route to display privacy page.''' if g.user: @@ -295,6 +303,7 @@ def privacy(): @app.route('/contributors') +@cache_policy(private=True, uri_max_age=86400) def contributors(): '''Route to display contributors page.''' if g.user: