From 7061616db859e0a6ae469647dd380c6b97e4af60 Mon Sep 17 00:00:00 2001 From: Mike Mattozzi Date: Sat, 22 Apr 2017 16:26:09 -0400 Subject: [PATCH] Adding an etag URL for testing If-Match and If-None-Match logic --- httpbin/core.py | 19 +++++++- httpbin/helpers.py | 11 +++++ httpbin/templates/httpbin.1.html | 1 + test_httpbin.py | 78 ++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 1 deletion(-) diff --git a/httpbin/core.py b/httpbin/core.py index ef0ef3f..705179d 100644 --- a/httpbin/core.py +++ b/httpbin/core.py @@ -23,7 +23,7 @@ from werkzeug.wrappers import BaseResponse from raven.contrib.flask import Sentry from . import filters -from .helpers import get_headers, status_code, get_dict, get_request_range, check_basic_auth, check_digest_auth, secure_cookie, H, ROBOT_TXT, ANGRY_ASCII +from .helpers import get_headers, status_code, get_dict, get_request_range, check_basic_auth, check_digest_auth, secure_cookie, H, ROBOT_TXT, ANGRY_ASCII, parse_multi_value_header from .utils import weighted_choice from .structures import CaseInsensitiveDict @@ -533,6 +533,23 @@ def cache(): else: return status_code(304) +@app.route('/etag/', methods=('GET',)) +def etag(etag): + """Assumes the resource has the given etag and responds to If-None-Match and If-Match headers appropriately.""" + if_none_match = parse_multi_value_header(request.headers.get('If-None-Match')) + if_match = parse_multi_value_header(request.headers.get('If-Match')) + + if if_none_match: + if etag in if_none_match or '*' in if_none_match: + return status_code(304) + elif if_match: + if etag not in if_match and '*' not in if_match: + return status_code(412) + + # Special cases don't apply, return normal response + response = view_get() + response.headers['ETag'] = etag + return response @app.route('/cache/') def cache_control(value): diff --git a/httpbin/helpers.py b/httpbin/helpers.py index 181dc7f..bd909c8 100644 --- a/httpbin/helpers.py +++ b/httpbin/helpers.py @@ -9,6 +9,7 @@ This module provides helper functions for httpbin. import json import base64 +import re from hashlib import md5, sha256 from werkzeug.http import parse_authorization_header @@ -420,3 +421,13 @@ def get_request_range(request_headers, upper_bound): return first_byte_pos, last_byte_pos +def parse_multi_value_header(header_str): + """Break apart an HTTP header string that is potentially a quoted, comma separated list as used in entity headers in RFC2616.""" + parsed_parts = [] + if header_str: + parts = header_str.split(',') + for part in parts: + match = re.search('\s*(W/)?\"?([^"]*)\"?\s*', part) + if match is not None: + parsed_parts.append(match.group(2)) + return parsed_parts diff --git a/httpbin/templates/httpbin.1.html b/httpbin/templates/httpbin.1.html index 0635d5b..eb9403e 100644 --- a/httpbin/templates/httpbin.1.html +++ b/httpbin/templates/httpbin.1.html @@ -40,6 +40,7 @@
  • /robots.txt Returns some robots.txt rules.
  • /deny Denied by robots.txt file.
  • /cache Returns 200 unless an If-Modified-Since or If-None-Match header is provided, when it returns a 304.
  • +
  • /etag/:etag Assumes the resource has the given etag and responds to If-None-Match header with a 200 or 304 and If-Match with a 200 or 412 as appropriate.
  • /cache/:n Sets a Cache-Control header for n seconds.
  • /bytes/:n Generates n random bytes of binary data, accepts optional seed integer parameter.
  • /stream-bytes/:n Streams n random bytes of binary data, accepts optional seed and chunk_size integer parameters.
  • diff --git a/test_httpbin.py b/test_httpbin.py index 9bbd3d8..d3a2218 100755 --- a/test_httpbin.py +++ b/test_httpbin.py @@ -11,6 +11,7 @@ from hashlib import md5, sha256 from six import BytesIO import httpbin +from httpbin.helpers import parse_multi_value_header @contextlib.contextmanager @@ -564,6 +565,83 @@ class HttpbinTestCase(unittest.TestCase): data = response.data.decode('utf-8') self.assertIn('perfectaudience', data) + def test_etag_if_none_match_matches(self): + response = self.app.get( + '/etag/abc', + headers={ 'If-None-Match': 'abc' } + ) + self.assertEqual(response.status_code, 304) + + def test_etag_if_none_match_matches_list(self): + response = self.app.get( + '/etag/abc', + headers={ 'If-None-Match': '"123", "abc"' } + ) + self.assertEqual(response.status_code, 304) + + def test_etag_if_none_match_matches_star(self): + response = self.app.get( + '/etag/abc', + headers={ 'If-None-Match': '*' } + ) + self.assertEqual(response.status_code, 304) + + def test_etag_if_none_match_w_prefix(self): + response = self.app.get( + '/etag/c3piozzzz', + headers={ 'If-None-Match': 'W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"' } + ) + self.assertEqual(response.status_code, 304) + + def test_etag_if_none_match_has_no_match(self): + response = self.app.get( + '/etag/abc', + headers={ 'If-None-Match': '123' } + ) + self.assertEqual(response.status_code, 200) + + def test_etag_if_match_matches(self): + response = self.app.get( + '/etag/abc', + headers={ 'If-Match': 'abc' } + ) + self.assertEqual(response.status_code, 200) + + def test_etag_if_match_matches_list(self): + response = self.app.get( + '/etag/abc', + headers={ 'If-Match': '"123", "abc"' } + ) + self.assertEqual(response.status_code, 200) + + def test_etag_if_match_matches_star(self): + response = self.app.get( + '/etag/abc', + headers={ 'If-Match': '*' } + ) + self.assertEqual(response.status_code, 200) + + def test_etag_if_match_has_no_match(self): + response = self.app.get( + '/etag/abc', + headers={ 'If-Match': '123' } + ) + self.assertEqual(response.status_code, 412) + + def test_etag_with_no_headers(self): + response = self.app.get( + '/etag/abc' + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.headers.get('ETag'), 'abc') + + def test_parse_multi_value_header(self): + self.assertEqual(parse_multi_value_header('xyzzy'), [ "xyzzy" ]) + self.assertEqual(parse_multi_value_header('"xyzzy"'), [ "xyzzy" ]) + self.assertEqual(parse_multi_value_header('W/"xyzzy"'), [ "xyzzy" ]) + self.assertEqual(parse_multi_value_header('"xyzzy", "r2d2xxxx", "c3piozzzz"'), [ "xyzzy", "r2d2xxxx", "c3piozzzz" ]) + self.assertEqual(parse_multi_value_header('W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"'), [ "xyzzy", "r2d2xxxx", "c3piozzzz" ]) + self.assertEqual(parse_multi_value_header('*'), [ "*" ]) if __name__ == '__main__': unittest.main()