Merge branch 'master' of git://github.com/racker/falcon into pythonic_rand_chars
Conflicts: test/helpers.py
This commit is contained in:
24
NOTES.md
24
NOTES.md
@@ -1,5 +1,27 @@
|
||||
* Keep-Alive is intentially disabled for HTTP/1.0 clients to mitigate problems with proxies. See also http://tools.ietf.org/html/rfc2616#section-19.6.2
|
||||
### Assumptions ###
|
||||
|
||||
In order to stay lean and fast, Falcon makes several assumptions.
|
||||
|
||||
First, Falcon assumes that request handlers will (for the most part) do the right thing. In other words, Falcon doesn't try very hard to protect handler code from itself.
|
||||
|
||||
This requires some discipline on the part of the developer.
|
||||
|
||||
1. Request handlers will set response variables to sane values.
|
||||
1. The application won't add extra junk to req and resp dicts (use the ctx instead)
|
||||
1. Request handlers are well-tested with high code coverage. It's not Falcon's job to babysit your code once it leaves the nest.
|
||||
1. ...
|
||||
|
||||
### Misc. ###
|
||||
|
||||
* Falcon probably isn't thread-safe; don't try it. Run multiple worker processes, each with a non-blocking I/O loop instead.
|
||||
* Falcon doesn't officially support Python 3; it's on our TODO list.
|
||||
* Falcon is based on byte strings, and does no conversions to UTF-16 (for example). If your app needs to use wide strings, you'll need to do the conversion manually. However, we recommend just keeping everything UTF-8 to avoid writing extra code and spinning CPU cycles.
|
||||
* resp.set_header assumes both params are strings. App may crash otherwise. Falcon trusts the caller. You *are* testing all your code paths, aren't you?
|
||||
* If you need the protocol (http vs https) to construct hrefs in your responses (hypermedia is good, trust me), you can get it from req.scheme
|
||||
* URI template and query string field names must include only ASCII a-z, A-Z, and the underscore '_' character. Try it; you'll like it. This simplifies parsing and helps speed things up a bit.
|
||||
* Query params must have a value. In other words, 'foo' or 'foo=' will result in the parameter being ignored.
|
||||
* If the WSGI server passes an empty path, Falcon will force it to '/', so you don't have to test for the empty string in your app.
|
||||
* If you are hosting multiple apps with a single WSGI server, the SCRIPT_NAME variable can read from req.app
|
||||
* If you have several headers to set, consider using set_headers to avoid function call overhead
|
||||
* Don't set content-length. It will only be overridden.
|
||||
* The order in which header fields are sent in the response is undefined. Headers are not grouped according to the recommendation in [RFC 2616](http://tools.ietf.org/html/rfc2616#section-4.2) in order to generate responses as quickly as possible.
|
||||
|
||||
17
README.md
17
README.md
@@ -11,7 +11,7 @@ Falcon is a fast, light-weight framework for building cloud APIs. It tries to do
|
||||
>
|
||||
> *- Antoine de Saint-Exupéry*
|
||||
|
||||
### Design ###
|
||||
### Design Goals ###
|
||||
|
||||
**Light-weight.** Only the essentials are included, with few dependencies. We work to keep the code lean-n-mean, making Falcon easier to test, optimize, and deploy.
|
||||
|
||||
@@ -19,18 +19,9 @@ Falcon is a fast, light-weight framework for building cloud APIs. It tries to do
|
||||
|
||||
**Cloud-friendly.** Falcon uses the web-friendly Python language, and speaks WSGI, so you can deploy it on your favorite stack. The framework is designed from the ground up to embrace HTTP, not work against it. Plus, diagnostics are built right in to make it easier to track down sneaky bugs and frustrating performance problems.
|
||||
|
||||
### Assumptions ###
|
||||
### Contributing ###
|
||||
|
||||
(Work in progress.)
|
||||
Pull requests are welcome. Just make sure to include tests and follow PEP 8 and your commit messages are formatted using [AngularJS conventions][ajs] (one-liners are OK for now but body and footer may be required as the project matures).
|
||||
|
||||
In order to stay lean and fast, Falcon makes several assumptions.
|
||||
|
||||
First of all, Falcon assumes that request handlers will (for the most part) do the right thing. In other words, Falcon doesn't try very hard to protect handler code from itself.
|
||||
|
||||
This requires some discipline on the part of the developer.
|
||||
|
||||
1. Request handlers will set response variables to sane values. This includes setting *status* to a valid HTTP status code and string (just use the provided constants), setting *headers* to a collection of tuples, and setting *body* (if not desired to be empty) to either a string or an iterable.
|
||||
1. The application won't add extra junk to req and resp dicts (use the ctx instead)
|
||||
1. Request handlers are well-tested with high code coverage. It's not Falcon's job to babysit your code once it leaves the nest.
|
||||
1. ...
|
||||
[ajs]: http://goo.gl/QpbS7
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""A fast micro-framework for building cloud APIs."""
|
||||
version_tuple = (0, 0, 1, '-dev')
|
||||
|
||||
|
||||
def get_version_string():
|
||||
if isinstance(version_tuple[-1], basestring):
|
||||
return '.'.join(map(str, version_tuple[:-1])) + version_tuple[-1]
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
from falcon.request import Request
|
||||
from falcon.response import Response
|
||||
|
||||
from falcon.default_request_handlers import *
|
||||
from falcon import responders
|
||||
from falcon.status_codes import *
|
||||
from falcon.api_helpers import *
|
||||
|
||||
HTTP_METHODS = (
|
||||
'CONNECT',
|
||||
'DELETE',
|
||||
'GET',
|
||||
'HEAD',
|
||||
'OPTIONS',
|
||||
'POST',
|
||||
'PUT',
|
||||
'TRACE'
|
||||
)
|
||||
|
||||
# TODO: __slots__
|
||||
# TODO: log exceptions, trace execution, etc.
|
||||
|
||||
class Api:
|
||||
"""Provides routing and such for building a web service application"""
|
||||
|
||||
__slots__ = ('routes')
|
||||
|
||||
def __init__(self):
|
||||
self.routes = {}
|
||||
self.routes = []
|
||||
|
||||
def __call__(self, env, start_response):
|
||||
"""WSGI protocol handler"""
|
||||
@@ -25,22 +36,28 @@ class Api:
|
||||
req = Request(env)
|
||||
resp = Response()
|
||||
|
||||
# PERF: Use try...except blocks when the key usually exists
|
||||
try:
|
||||
# TODO: Figure out a way to use codegen to make a state machine,
|
||||
# may have to in order to support URI templates.
|
||||
handler = self.routes[req.path]
|
||||
except KeyError:
|
||||
handler = path_not_found_handler
|
||||
path = req.path
|
||||
for path_template, method_map in self.routes:
|
||||
m = path_template.match(path)
|
||||
if m:
|
||||
req._params.update(m.groupdict())
|
||||
try:
|
||||
handler = method_map[req.method]
|
||||
except KeyError:
|
||||
handler = responders.bad_request
|
||||
else:
|
||||
break
|
||||
else:
|
||||
handler = responders.path_not_found
|
||||
|
||||
handler(ctx, req, resp)
|
||||
|
||||
#
|
||||
# Set status and headers
|
||||
#
|
||||
use_body = not self._should_ignore_body(resp.status)
|
||||
use_body = not should_ignore_body(resp.status)
|
||||
if use_body:
|
||||
self._set_content_length(env, req, resp)
|
||||
set_content_length(env, req, resp)
|
||||
|
||||
start_response(resp.status, resp._wsgi_headers())
|
||||
|
||||
@@ -51,27 +68,11 @@ class Api:
|
||||
# Ignore body based on status code
|
||||
return []
|
||||
|
||||
|
||||
def add_route(self, uri_template, handler):
|
||||
self.routes[uri_template] = handler
|
||||
pass
|
||||
if not uri_template:
|
||||
uri_template = '/'
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _should_ignore_body(self, status):
|
||||
return (status.startswith('204') or
|
||||
status.startswith('1') or
|
||||
status.startswith('304'))
|
||||
|
||||
def _set_content_length(self, env, req, resp):
|
||||
|
||||
# Set Content-Length when given a fully-buffered body or stream length
|
||||
if resp.body is not None:
|
||||
resp.set_header('Content-Length', str(len(resp.body)))
|
||||
elif resp.stream_len is not None:
|
||||
resp.set_header('Content-Length', resp.stream_len)
|
||||
else:
|
||||
resp.set_header('Content-Length', 0)
|
||||
path_template = compile_uri_template(uri_template)
|
||||
method_map = create_http_method_map(handler)
|
||||
|
||||
self.routes.append((path_template, method_map))
|
||||
|
||||
107
falcon/api.py.orig
Normal file
107
falcon/api.py.orig
Normal file
@@ -0,0 +1,107 @@
|
||||
import re
|
||||
|
||||
from falcon.request import Request
|
||||
from falcon.response import Response
|
||||
|
||||
from falcon.default_request_handlers import *
|
||||
from falcon.status_codes import *
|
||||
|
||||
# TODO: __slots__
|
||||
# TODO: log exceptions, trace execution, etc.
|
||||
|
||||
|
||||
class Api:
|
||||
"""Provides routing and such for building a web service application"""
|
||||
|
||||
def __init__(self):
|
||||
self.routes = []
|
||||
|
||||
def __call__(self, env, start_response):
|
||||
"""WSGI protocol handler"""
|
||||
|
||||
# PERF: Use literal constructor for dicts
|
||||
ctx = {}
|
||||
|
||||
# TODO
|
||||
# ctx.update(global_ctx_for_route)
|
||||
|
||||
req = Request(env)
|
||||
resp = Response()
|
||||
|
||||
path = req.path
|
||||
for path_template, handler in self.routes:
|
||||
m = path_template.match(path)
|
||||
if m:
|
||||
req._params.update(m.groupdict())
|
||||
break
|
||||
else:
|
||||
handler = path_not_found_handler
|
||||
|
||||
handler(ctx, req, resp)
|
||||
|
||||
#
|
||||
# Set status and headers
|
||||
#
|
||||
use_body = not self._should_ignore_body(resp.status)
|
||||
if use_body:
|
||||
self._set_content_length(env, req, resp)
|
||||
|
||||
start_response(resp.status, resp._wsgi_headers())
|
||||
|
||||
# Return an iterable for the body, per the WSGI spec
|
||||
if use_body:
|
||||
return [resp.body] if resp.body is not None else []
|
||||
|
||||
# Ignore body based on status code
|
||||
return []
|
||||
|
||||
def add_route(self, uri_template, handler):
|
||||
if not hasattr(handler, '__call__'):
|
||||
raise TypeError('handler is not callable')
|
||||
|
||||
if not uri_template:
|
||||
uri_template = '/'
|
||||
|
||||
path_template = self._compile_uri_template(uri_template)
|
||||
self.routes.append((path_template, handler))
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _should_ignore_body(self, status):
|
||||
return (status.startswith('204') or
|
||||
status.startswith('1') or
|
||||
status.startswith('304'))
|
||||
|
||||
def _set_content_length(self, env, req, resp):
|
||||
|
||||
# Set Content-Length when given a fully-buffered body or stream length
|
||||
if resp.body is not None:
|
||||
resp.set_header('Content-Length', str(len(resp.body)))
|
||||
elif resp.stream_len is not None:
|
||||
resp.set_header('Content-Length', resp.stream_len)
|
||||
else:
|
||||
resp.set_header('Content-Length', 0)
|
||||
|
||||
def _compile_uri_template(self, template):
|
||||
"""Compile the given URI template string into path and query string
|
||||
regex-based templates.
|
||||
|
||||
See also: http://tools.ietf.org/html/rfc6570
|
||||
"""
|
||||
if not isinstance(template, str):
|
||||
raise TypeError('uri_template is not a byte string')
|
||||
|
||||
# Convert Level 1 var patterns to equivalent named regex groups
|
||||
pattern = re.sub(r'{([a-zA-Z][a-zA-Z_]*)}', r'(?P<\1>[^/]+)', template)
|
||||
pattern = r'\A' + pattern + r'\Z'
|
||||
return re.compile(pattern, re.IGNORECASE)
|
||||
<<<<<<< Updated upstream
|
||||
=======
|
||||
|
||||
def _get_handler_http_methods(self, handler):
|
||||
pass
|
||||
|
||||
|
||||
>>>>>>> Stashed changes
|
||||
70
falcon/api_helpers.py
Normal file
70
falcon/api_helpers.py
Normal file
@@ -0,0 +1,70 @@
|
||||
import re
|
||||
from falcon import responders
|
||||
|
||||
HTTP_METHODS = (
|
||||
'CONNECT',
|
||||
'DELETE',
|
||||
'GET',
|
||||
'HEAD',
|
||||
'OPTIONS',
|
||||
'POST',
|
||||
'PUT',
|
||||
'TRACE'
|
||||
)
|
||||
|
||||
|
||||
def should_ignore_body(status):
|
||||
return (status.startswith('204') or
|
||||
status.startswith('1') or
|
||||
status.startswith('304'))
|
||||
|
||||
|
||||
def set_content_length(env, req, resp):
|
||||
|
||||
# Set Content-Length when given a fully-buffered body or stream length
|
||||
if resp.body is not None:
|
||||
resp.set_header('Content-Length', str(len(resp.body)))
|
||||
elif resp.stream_len is not None:
|
||||
resp.set_header('Content-Length', resp.stream_len)
|
||||
else:
|
||||
resp.set_header('Content-Length', 0)
|
||||
|
||||
|
||||
def compile_uri_template(template):
|
||||
"""Compile the given URI template string into path and query string
|
||||
regex-based templates.
|
||||
|
||||
See also: http://tools.ietf.org/html/rfc6570
|
||||
"""
|
||||
if not isinstance(template, str):
|
||||
raise TypeError('uri_template is not a byte string')
|
||||
|
||||
# Convert Level 1 var patterns to equivalent named regex groups
|
||||
pattern = re.sub(r'{([a-zA-Z][a-zA-Z_]*)}', r'(?P<\1>[^/]+)', template)
|
||||
pattern = r'\A' + pattern + r'\Z'
|
||||
return re.compile(pattern, re.IGNORECASE)
|
||||
|
||||
|
||||
def create_http_method_map(handler):
|
||||
method_map = {}
|
||||
|
||||
for method in HTTP_METHODS:
|
||||
try:
|
||||
func = getattr(handler, 'on_' + method.lower())
|
||||
except AttributeError:
|
||||
# Handler does not implement this method
|
||||
pass
|
||||
else:
|
||||
# Usually expect a method, but any callable will do
|
||||
if hasattr(func, '__call__'):
|
||||
method_map[method] = func
|
||||
|
||||
# Attach a handler for unsupported HTTP methods
|
||||
allowed_methods = method_map.keys()
|
||||
func = responders.create_method_not_allowed(allowed_methods)
|
||||
|
||||
for method in HTTP_METHODS:
|
||||
if method not in allowed_methods:
|
||||
method_map[method] = func
|
||||
|
||||
return method_map
|
||||
@@ -1,4 +0,0 @@
|
||||
from status_codes import *
|
||||
|
||||
def path_not_found_handler(ctx, req, resp):
|
||||
resp.status = HTTP_404
|
||||
@@ -1,37 +1,57 @@
|
||||
from falcon.request_helpers import *
|
||||
|
||||
|
||||
class Request:
|
||||
__slots__ = ('path', 'headers')
|
||||
__slots__ = (
|
||||
'app',
|
||||
'body',
|
||||
'_headers',
|
||||
'method',
|
||||
'_params',
|
||||
'path',
|
||||
'protocol',
|
||||
'query_string'
|
||||
)
|
||||
|
||||
def __init__(self, env):
|
||||
self.path = env['PATH_INFO']
|
||||
self.headers = headers = {}
|
||||
self.app = env['SCRIPT_NAME']
|
||||
self.body = env['wsgi.input']
|
||||
self.method = env['REQUEST_METHOD']
|
||||
self.path = env['PATH_INFO'] or '/'
|
||||
self.protocol = env['wsgi.url_scheme']
|
||||
self.query_string = query_string = env['QUERY_STRING']
|
||||
self._params = parse_query_string(query_string)
|
||||
self._headers = parse_headers(env)
|
||||
|
||||
# Extract HTTP headers
|
||||
for key in env:
|
||||
if key.startswith('HTTP_'):
|
||||
headers[key[5:]] = env[key]
|
||||
|
||||
if 'HOST' not in headers:
|
||||
host = env['SERVER_NAME']
|
||||
port = env['SERVER_PORT']
|
||||
|
||||
if port != '80':
|
||||
host = ''.join([host, ':', port])
|
||||
|
||||
headers['HOST'] = host
|
||||
|
||||
def get_header(self, name, default=None):
|
||||
def try_get_header(self, name, default=None):
|
||||
"""Return a header value as a string
|
||||
|
||||
name -- Header name, case-insensitive
|
||||
name -- Header name, case-insensitive (e.g., 'Content-Type')
|
||||
default -- Value to return in case the header is not found
|
||||
|
||||
"""
|
||||
|
||||
headers = self.headers
|
||||
|
||||
# Optimize for the header existing in most cases
|
||||
# Use try..except to optimize for the header existing in most cases
|
||||
try:
|
||||
return headers[name.upper()]
|
||||
except KeyError as e:
|
||||
# Don't take the time to cache beforehand, using HTTP naming.
|
||||
# This will be faster, assuming that most headers are looked
|
||||
# up only once, and not all headers will be requested.
|
||||
return self._headers[name.upper().replace('-', '_')]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def try_get_param(self, name, default=None):
|
||||
"""Return a URI parameter value as a string
|
||||
|
||||
name -- Parameter name as specified in the route template. Note that
|
||||
names are case-sensitive (e.g., 'Id' != 'id').
|
||||
default -- Value to return in case the header is not found
|
||||
|
||||
"""
|
||||
|
||||
# PERF: Use if..in since it is a good all-around performer; we don't
|
||||
# know how likely params are to be specified by clients.
|
||||
if name in self._params:
|
||||
return self._params[name]
|
||||
|
||||
return default
|
||||
|
||||
44
falcon/request_helpers.py
Normal file
44
falcon/request_helpers.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import re
|
||||
|
||||
QS_PATTERN = re.compile(r'([a-zA-Z_]+)=([^&]+)')
|
||||
|
||||
|
||||
def parse_query_string(query_string):
|
||||
# Parse query string
|
||||
# PERF: use for loop in lieu of the dict constructor
|
||||
params = {}
|
||||
for k, v in QS_PATTERN.findall(query_string):
|
||||
if ',' in v:
|
||||
v = v.split(',')
|
||||
|
||||
params[k] = v
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def parse_headers(env):
|
||||
# Parse HTTP_*
|
||||
headers = {}
|
||||
for key in env:
|
||||
if key.startswith('HTTP_'):
|
||||
headers[key[5:]] = env[key]
|
||||
|
||||
# Per the WSGI spec, Content-Type is not under HTTP_*
|
||||
if 'CONTENT_TYPE' in env:
|
||||
headers['CONTENT_TYPE'] = env['CONTENT_TYPE']
|
||||
|
||||
# Per the WSGI spec, Content-Length is not under HTTP_*
|
||||
if 'CONTENT_LENGTH' in env:
|
||||
headers['CONTENT_LENGTH'] = env['CONTENT_LENGTH']
|
||||
|
||||
# Fallback to SERVER_* vars if the Host header isn't specified
|
||||
if 'HOST' not in headers:
|
||||
host = env['SERVER_NAME']
|
||||
port = env['SERVER_PORT']
|
||||
|
||||
if port != '80':
|
||||
host = ''.join([host, ':', port])
|
||||
|
||||
headers['HOST'] = host
|
||||
|
||||
return headers
|
||||
15
falcon/responders.py
Normal file
15
falcon/responders.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from falcon.status_codes import *
|
||||
|
||||
|
||||
def path_not_found(ctx, req, resp):
|
||||
resp.status = HTTP_404
|
||||
|
||||
def bad_request(ctx, req, resp):
|
||||
resp.status = HTTP_400
|
||||
|
||||
def create_method_not_allowed(allowed_methods):
|
||||
def method_not_allowed(ctx, req, resp):
|
||||
resp.status = HTTP_405
|
||||
resp.set_header('Allow', ', '.join(allowed_methods))
|
||||
|
||||
return method_not_allowed
|
||||
@@ -14,12 +14,100 @@ HTTP_206 = '206 Partial Content'
|
||||
HTTP_226 = '226 IM Used'
|
||||
|
||||
# TODO: 3xx
|
||||
HTTP_300 = '300 Multiple Choices'
|
||||
HTTP_301 = '301 Moved Permanently'
|
||||
HTTP_302 = '302 Found'
|
||||
HTTP_303 = '303 See Other'
|
||||
HTTP_304 = '304 Not Modified'
|
||||
HTTP_305 = '305 Use Proxy'
|
||||
HTTP_307 = '307 Temporary Redirect'
|
||||
|
||||
# TODO: 4xx
|
||||
HTTP_400 = '400 Bad Request'
|
||||
HTTP_401 = '401 Unauthorized' # <-- Really means "unauthenticated"
|
||||
HTTP_402 = '402 Payment Required'
|
||||
HTTP_403 = '403 Forbidden' # <-- Really means "unauthorized"
|
||||
HTTP_404 = '404 Not Found'
|
||||
HTTP_405 = '405 Method Not Allowed'
|
||||
HTTP_406 = '406 Not Acceptable'
|
||||
HTTP_407 = '407 Proxy Authentication Required'
|
||||
HTTP_408 = '408 Request Time-out'
|
||||
HTTP_409 = '409 Conflict'
|
||||
HTTP_410 = '410 Gone'
|
||||
HTTP_411 = '411 Length Required'
|
||||
HTTP_412 = '412 Precondition Failed'
|
||||
HTTP_413 = '413 Request Entity Too Large'
|
||||
HTTP_414 = '414 Request-URI Too Large'
|
||||
HTTP_415 = '415 Unsupported Media Type'
|
||||
HTTP_416 = '416 Requested range not satisfiable'
|
||||
HTTP_417 = '417 Expectation Failed'
|
||||
|
||||
# TODO: 5xx
|
||||
HTTP_500 = '500 Internal Server Error'
|
||||
HTTP_501 = '501 Not Implemented'
|
||||
HTTP_502 = '502 Bad Gateway'
|
||||
HTTP_503 = '503 Service Unavailable'
|
||||
HTTP_504 = '504 Gateway Time-out'
|
||||
HTTP_505 = '505 HTTP Version not supported'
|
||||
|
||||
# TODO: 7xx
|
||||
# 70X - Inexcusable
|
||||
HTTP_701 = '701 Meh'
|
||||
HTTP_702 = '702 Emacs'
|
||||
HTTP_703 = '703 Explosion'
|
||||
|
||||
# 71X - Novelty Implementations
|
||||
HTTP_710 = '710 PHP'
|
||||
HTTP_711 = '711 Convenience Store'
|
||||
HTTP_712 = '712 NoSQL'
|
||||
HTTP_719 = '719 I am not a teapot'
|
||||
|
||||
# 72X - Edge Cases
|
||||
HTTP_720 = '720 Unpossible'
|
||||
HTTP_721 = '721 Known Unknowns'
|
||||
HTTP_722 = '722 Unknown Unknowns'
|
||||
HTTP_723 = '723 Tricky'
|
||||
HTTP_724 = '724 This line should be unreachable'
|
||||
HTTP_725 = '725 It works on my machine'
|
||||
HTTP_726 = "726 It's a feature, not a bug"
|
||||
HTTP_727 = '727 32 bits in plenty'
|
||||
|
||||
# 74X - Meme Driven
|
||||
HTTP_740 = '740 Computer says no'
|
||||
HTTP_741 = '741 Compiling'
|
||||
HTTP_742 = '742 A kitten dies'
|
||||
HTTP_743 = '743 I thought I knew regular expressions'
|
||||
HTTP_744 = '744 Y U NO write integration tests?'
|
||||
HTTP_745 = ("745 I don't always test my code, but when I do"
|
||||
"I do it in production")
|
||||
HTTP_748 = '748 Confounded by Ponies'
|
||||
HTTP_749 = '749 Reserved for Chuck Norris'
|
||||
|
||||
# 75X - Syntax Errors
|
||||
HTTP_750 = "750 Didn't bother to compile it"
|
||||
HTTP_753 = '753 Syntax Error'
|
||||
HTTP_754 = '754 Too many semi-colons'
|
||||
HTTP_755 = '755 Not enough semi-colons'
|
||||
HTTP_759 = '759 Unexpected T_PAAMAYIM_NEKUDOTAYIM'
|
||||
|
||||
# 77X - Predictable Problems
|
||||
HTTP_771 = '771 Cached for too long'
|
||||
HTTP_772 = '772 Not cached long enough'
|
||||
HTTP_773 = '773 Not cached at all'
|
||||
HTTP_774 = '774 Why was this cached?'
|
||||
HTTP_776 = '776 Error on the Exception'
|
||||
HTTP_777 = '777 Coincidence'
|
||||
HTTP_778 = '778 Off By One Error'
|
||||
HTTP_779 = '779 Off By Too Many To Count Error'
|
||||
|
||||
# 78X - Somebody Else's Problem
|
||||
HTTP_780 = '780 Project owner not responding'
|
||||
HTTP_781 = '781 Operations'
|
||||
HTTP_782 = '782 QA'
|
||||
HTTP_783 = '783 It was a customer request, honestly'
|
||||
HTTP_784 = '784 Management, obviously'
|
||||
HTTP_785 = '785 TPS Cover Sheet not attached'
|
||||
HTTP_786 = '786 Try it now'
|
||||
|
||||
# 79X - Internet crashed
|
||||
HTTP_791 = '791 The Internet shut down due to copyright restrictions.'
|
||||
HTTP_792 = '792 Climate change driven catastrophic weather event'
|
||||
HTTP_797 = '797 This is the last page of the Internet. Go back'
|
||||
HTTP_799 = '799 End of the world'
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
|
||||
|
||||
def application(environ, start_response):
|
||||
start_response("200 OK", [
|
||||
('Content-Type', 'text/plain')])
|
||||
|
||||
body = '\n{\n'
|
||||
for key, value in environ.items():
|
||||
if isinstance(value, basestring):
|
||||
body += ' "{0}": "{1}",\n'.format(key, value)
|
||||
#if isinstance(value, basestring):
|
||||
body += ' "{0}": "{1}",\n'.format(key, value)
|
||||
|
||||
body += '}\n\n'
|
||||
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import inspect
|
||||
import random
|
||||
from io import BytesIO
|
||||
|
||||
import testtools
|
||||
|
||||
import falcon
|
||||
|
||||
|
||||
def rand_string(min, max):
|
||||
int_gen = random.randint
|
||||
string_length = int_gen(min, max)
|
||||
return ''.join([chr(int_gen(9, 126))
|
||||
for i in range(string_length)])
|
||||
|
||||
|
||||
class StartResponseMock:
|
||||
def __init__(self):
|
||||
self._called = 0
|
||||
@@ -20,6 +28,29 @@ class StartResponseMock:
|
||||
def call_count(self):
|
||||
return self._called
|
||||
|
||||
|
||||
class RequestHandler:
|
||||
sample_status = "200 OK"
|
||||
sample_body = rand_string(0, 128 * 1024)
|
||||
resp_headers = {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'ETag': '10d4555ebeb53b30adf724ca198b32a2',
|
||||
'X-Hello': 'OH HAI'
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.called = False
|
||||
|
||||
def on_get(self, ctx, req, resp):
|
||||
self.called = True
|
||||
|
||||
self.ctx, self.req, self.resp = ctx, req, resp
|
||||
|
||||
resp.status = falcon.HTTP_200
|
||||
resp.body = self.sample_body
|
||||
resp.set_headers(self.resp_headers)
|
||||
|
||||
|
||||
class TestSuite(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
@@ -33,34 +64,34 @@ class TestSuite(testtools.TestCase):
|
||||
prepare()
|
||||
|
||||
def _simulate_request(self, path, **kwargs):
|
||||
return self.api(create_environ(path=path, **kwargs),
|
||||
self.srmock)
|
||||
if not path:
|
||||
path = '/'
|
||||
|
||||
def rand_string(min, max):
|
||||
int_gen = random.randint
|
||||
string_length = int_gen(min, max)
|
||||
return ''.join([chr(int_gen(9, 126))
|
||||
for i in range(string_length)])
|
||||
return self.api(create_environ(path=path, **kwargs),
|
||||
self.srmock)
|
||||
|
||||
|
||||
def create_environ(path='/', query_string='', protocol='HTTP/1.1', port='80',
|
||||
headers=None):
|
||||
headers=None, script='', body='', method='GET'):
|
||||
|
||||
env = {
|
||||
'SERVER_PROTOCOL': protocol,
|
||||
'SERVER_SOFTWARE': 'gunicorn/0.17.0',
|
||||
'SCRIPT_NAME': '',
|
||||
'REQUEST_METHOD': 'GET',
|
||||
'SCRIPT_NAME': script,
|
||||
'REQUEST_METHOD': method,
|
||||
'PATH_INFO': path,
|
||||
'QUERY_STRING': query_string,
|
||||
'HTTP_ACCEPT': '*/*',
|
||||
'HTTP_USER_AGENT': 'curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5',
|
||||
'HTTP_USER_AGENT': ('curl/7.24.0 (x86_64-apple-darwin12.0) '
|
||||
'libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5'),
|
||||
'REMOTE_PORT': '65133',
|
||||
'RAW_URI': '/',
|
||||
'REMOTE_ADDR': '127.0.0.1',
|
||||
'wsgi_url_scheme': 'http',
|
||||
'SERVER_NAME': 'localhost',
|
||||
'SERVER_PORT': port,
|
||||
|
||||
'wsgi.url_scheme': 'http',
|
||||
'wsgi.input': BytesIO(body)
|
||||
}
|
||||
|
||||
if protocol != 'HTTP/1.0':
|
||||
@@ -68,6 +99,13 @@ def create_environ(path='/', query_string='', protocol='HTTP/1.1', port='80',
|
||||
|
||||
if headers is not None:
|
||||
for name, value in headers.iteritems():
|
||||
env['HTTP_' + name.upper()] = value.strip()
|
||||
name = name.upper().replace('-', '_')
|
||||
|
||||
if name == 'CONTENT_TYPE':
|
||||
env[name] = value.strip()
|
||||
elif name == 'CONTENT_LENGTH':
|
||||
env[name] = value.strip()
|
||||
else:
|
||||
env['HTTP_' + name.upper()] = value.strip()
|
||||
|
||||
return env
|
||||
|
||||
@@ -1,31 +1,8 @@
|
||||
import testtools
|
||||
import random
|
||||
|
||||
from testtools.matchers import Equals, MatchesRegex, Contains, Not
|
||||
from testtools.matchers import Contains, Not
|
||||
|
||||
import falcon
|
||||
import test.helpers as helpers
|
||||
|
||||
class RequestHandler:
|
||||
sample_status = "200 OK"
|
||||
sample_body = helpers.rand_string(0, 128 * 1024)
|
||||
resp_headers = {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'ETag': '10d4555ebeb53b30adf724ca198b32a2',
|
||||
'X-Hello': 'OH HAI'
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.called = False
|
||||
|
||||
def __call__(self, ctx, req, resp):
|
||||
self.called = True
|
||||
|
||||
self.ctx, self.req, self.resp = ctx, req, resp
|
||||
|
||||
resp.status = falcon.HTTP_200
|
||||
resp.body = self.sample_body
|
||||
resp.set_headers(self.resp_headers)
|
||||
|
||||
class RequestHandlerTestStatus:
|
||||
sample_body = helpers.rand_string(0, 128 * 1024)
|
||||
@@ -33,7 +10,7 @@ class RequestHandlerTestStatus:
|
||||
def __init__(self, status):
|
||||
self.status = status
|
||||
|
||||
def __call__(self, ctx, req, resp):
|
||||
def on_get(self, ctx, req, resp):
|
||||
resp.status = self.status
|
||||
resp.body = self.sample_body
|
||||
|
||||
@@ -41,8 +18,8 @@ class RequestHandlerTestStatus:
|
||||
class TestHeaders(helpers.TestSuite):
|
||||
|
||||
def prepare(self):
|
||||
self.on_hello = RequestHandler()
|
||||
self.api.add_route(self.test_route, self.on_hello)
|
||||
self.reqhandler = helpers.RequestHandler()
|
||||
self.api.add_route(self.test_route, self.reqhandler)
|
||||
|
||||
def test_content_length(self):
|
||||
self._simulate_request(self.test_route)
|
||||
@@ -50,7 +27,7 @@ class TestHeaders(helpers.TestSuite):
|
||||
headers = self.srmock.headers
|
||||
|
||||
# Test Content-Length header set
|
||||
content_length = str(len(self.on_hello.sample_body))
|
||||
content_length = str(len(self.reqhandler.sample_body))
|
||||
content_length_header = ('Content-Length', content_length)
|
||||
self.assertThat(headers, Contains(content_length_header))
|
||||
|
||||
@@ -58,16 +35,16 @@ class TestHeaders(helpers.TestSuite):
|
||||
self._simulate_request(self.test_route)
|
||||
|
||||
# Make sure we picked up host from HTTP_HOST, not SERVER_NAME
|
||||
host = self.on_hello.req.get_header('host')
|
||||
self.assertThat(host, Equals('falconer'))
|
||||
host = self.reqhandler.req.try_get_header('host')
|
||||
self.assertEquals(host, 'falconer')
|
||||
|
||||
def test_host_fallback(self):
|
||||
# Set protocol to 1.0 so that we won't get a host header
|
||||
self._simulate_request(self.test_route, protocol='HTTP/1.0')
|
||||
|
||||
# Make sure we picked up host from HTTP_HOST, not SERVER_NAME
|
||||
host = self.on_hello.req.get_header('host')
|
||||
self.assertThat(host, Equals('localhost'))
|
||||
host = self.reqhandler.req.try_get_header('host')
|
||||
self.assertEquals(host, 'localhost')
|
||||
|
||||
def test_host_fallback_port8000(self):
|
||||
# Set protocol to 1.0 so that we won't get a host header
|
||||
@@ -75,8 +52,8 @@ class TestHeaders(helpers.TestSuite):
|
||||
port='8000')
|
||||
|
||||
# Make sure we picked up host from HTTP_HOST, not SERVER_NAME
|
||||
host = self.on_hello.req.get_header('host')
|
||||
self.assertThat(host, Equals('localhost:8000'))
|
||||
host = self.reqhandler.req.try_get_header('host')
|
||||
self.assertEquals(host, 'localhost:8000')
|
||||
|
||||
def test_no_body_on_1xx(self):
|
||||
self.request_handler = RequestHandlerTestStatus(falcon.HTTP_102)
|
||||
@@ -86,7 +63,7 @@ class TestHeaders(helpers.TestSuite):
|
||||
self.assertThat(self.srmock.headers_dict,
|
||||
Not(Contains('Content-Length')))
|
||||
|
||||
self.assertThat(body, Equals([]))
|
||||
self.assertEquals(body, [])
|
||||
|
||||
def test_no_body_on_101(self):
|
||||
self.request_handler = RequestHandlerTestStatus(falcon.HTTP_101)
|
||||
@@ -96,7 +73,7 @@ class TestHeaders(helpers.TestSuite):
|
||||
self.assertThat(self.srmock.headers_dict,
|
||||
Not(Contains('Content-Length')))
|
||||
|
||||
self.assertThat(body, Equals([]))
|
||||
self.assertEquals(body, [])
|
||||
|
||||
def test_no_body_on_204(self):
|
||||
self.request_handler = RequestHandlerTestStatus(falcon.HTTP_204)
|
||||
@@ -106,7 +83,7 @@ class TestHeaders(helpers.TestSuite):
|
||||
self.assertThat(self.srmock.headers_dict,
|
||||
Not(Contains('Content-Length')))
|
||||
|
||||
self.assertThat(body, Equals([]))
|
||||
self.assertEquals(body, [])
|
||||
|
||||
def test_no_body_on_304(self):
|
||||
self.request_handler = RequestHandlerTestStatus(falcon.HTTP_304)
|
||||
@@ -116,7 +93,7 @@ class TestHeaders(helpers.TestSuite):
|
||||
self.assertThat(self.srmock.headers_dict,
|
||||
Not(Contains('Content-Length')))
|
||||
|
||||
self.assertThat(body, Equals([]))
|
||||
self.assertEquals(body, [])
|
||||
|
||||
def test_passthrough_req_headers(self):
|
||||
req_headers = {
|
||||
@@ -126,13 +103,13 @@ class TestHeaders(helpers.TestSuite):
|
||||
self._simulate_request(self.test_route, headers=req_headers)
|
||||
|
||||
for name, expected_value in req_headers.iteritems():
|
||||
actual_value = self.on_hello.req.get_header(name)
|
||||
self.assertThat(actual_value, Equals(expected_value))
|
||||
actual_value = self.reqhandler.req.try_get_header(name)
|
||||
self.assertEquals(actual_value, expected_value)
|
||||
|
||||
def test_passthrough_resp_headers(self):
|
||||
self._simulate_request(self.test_route)
|
||||
|
||||
resp_headers = self.srmock.headers
|
||||
|
||||
for h in self.on_hello.resp_headers.iteritems():
|
||||
for h in self.reqhandler.resp_headers.iteritems():
|
||||
self.assertThat(resp_headers, Contains(h))
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import testtools
|
||||
from testtools.matchers import Equals, MatchesRegex
|
||||
|
||||
import falcon
|
||||
import test.helpers as helpers
|
||||
import helpers
|
||||
|
||||
|
||||
class HelloRequestHandler:
|
||||
sample_status = "200 OK"
|
||||
@@ -11,7 +9,7 @@ class HelloRequestHandler:
|
||||
def __init__(self):
|
||||
self.called = False
|
||||
|
||||
def __call__(self, ctx, req, resp):
|
||||
def on_get(self, ctx, req, resp):
|
||||
self.called = True
|
||||
|
||||
self.ctx, self.req, self.resp = ctx, req, resp
|
||||
@@ -26,18 +24,25 @@ class TestHelloWorld(helpers.TestSuite):
|
||||
self.on_hello = HelloRequestHandler()
|
||||
self.api.add_route(self.test_route, self.on_hello)
|
||||
|
||||
self.root_reqhandler = helpers.RequestHandler()
|
||||
self.api.add_route('', self.root_reqhandler)
|
||||
|
||||
def test_empty_route(self):
|
||||
self._simulate_request('')
|
||||
self.assertTrue(self.root_reqhandler.called)
|
||||
|
||||
def test_hello_route_negative(self):
|
||||
bogus_route = self.test_route + 'x'
|
||||
self._simulate_request(bogus_route)
|
||||
|
||||
# Ensure the request was NOT routed to on_hello
|
||||
self.assertFalse(self.on_hello.called)
|
||||
self.assertThat(self.srmock.status, Equals(falcon.HTTP_404))
|
||||
self.assertEquals(self.srmock.status, falcon.HTTP_404)
|
||||
|
||||
def test_hello_route(self):
|
||||
self._simulate_request(self.test_route)
|
||||
resp = self.on_hello.resp
|
||||
|
||||
self.assertThat(resp.status, Equals(self.on_hello.sample_status))
|
||||
self.assertEquals(resp.status, self.on_hello.sample_status)
|
||||
|
||||
self.assertThat(resp.body, Equals(self.on_hello.sample_body))
|
||||
self.assertEquals(resp.body, self.on_hello.sample_body)
|
||||
|
||||
90
test/test_http_method_routing.py
Normal file
90
test/test_http_method_routing.py
Normal file
@@ -0,0 +1,90 @@
|
||||
import helpers
|
||||
import falcon
|
||||
|
||||
from testtools.matchers import Contains
|
||||
|
||||
HTTP_METHODS = (
|
||||
'CONNECT',
|
||||
'DELETE',
|
||||
'GET',
|
||||
'HEAD',
|
||||
'OPTIONS',
|
||||
'POST',
|
||||
'PUT',
|
||||
'TRACE'
|
||||
)
|
||||
|
||||
|
||||
class RequestHandlerWithGet:
|
||||
def __init__(self):
|
||||
self.called = False
|
||||
|
||||
def on_get(self, ctx, req, resp):
|
||||
self.called = True
|
||||
|
||||
self.ctx, self.req, self.resp = ctx, req, resp
|
||||
resp.status = falcon.HTTP_200
|
||||
|
||||
|
||||
class RequestHandlerMisc:
|
||||
def __init__(self):
|
||||
self.called = False
|
||||
|
||||
def on_get(self, ctx, req, resp):
|
||||
self.called = True
|
||||
|
||||
self.ctx, self.req, self.resp = ctx, req, resp
|
||||
resp.status = falcon.HTTP_204
|
||||
|
||||
def on_head(self, ctx, req, resp):
|
||||
self.called = True
|
||||
|
||||
self.ctx, self.req, self.resp = ctx, req, resp
|
||||
resp.status = falcon.HTTP_204
|
||||
|
||||
def on_put(self, ctx, req, resp):
|
||||
self.called = True
|
||||
|
||||
self.ctx, self.req, self.resp = ctx, req, resp
|
||||
resp.status = falcon.HTTP_400
|
||||
|
||||
|
||||
class TestHttpMethodRouting(helpers.TestSuite):
|
||||
|
||||
def prepare(self):
|
||||
self.reqhandler_get = RequestHandlerWithGet()
|
||||
self.api.add_route('/get', self.reqhandler_get)
|
||||
|
||||
self.reqhandler_misc = RequestHandlerMisc()
|
||||
self.api.add_route('/misc', self.reqhandler_misc)
|
||||
|
||||
def test_get(self):
|
||||
self._simulate_request('/get')
|
||||
self.assertTrue(self.reqhandler_get.called)
|
||||
|
||||
def test_misc(self):
|
||||
for method in ['GET', 'HEAD', 'PUT']:
|
||||
self.reqhandler_misc.called = False
|
||||
self._simulate_request('/misc', method=method)
|
||||
self.assertTrue(self.reqhandler_misc.called)
|
||||
self.assertEquals(self.reqhandler_misc.req.method, method)
|
||||
|
||||
def test_method_not_allowed(self):
|
||||
for method in HTTP_METHODS:
|
||||
if method == 'GET':
|
||||
continue
|
||||
|
||||
self.reqhandler_get.called = False
|
||||
self._simulate_request('/get', method=method)
|
||||
|
||||
self.assertFalse(self.reqhandler_get.called)
|
||||
self.assertEquals(self.srmock.status, '405 Method Not Allowed')
|
||||
|
||||
headers = self.srmock.headers
|
||||
allow_header = ('Allow', 'GET')
|
||||
|
||||
self.assertThat(headers, Contains(allow_header))
|
||||
|
||||
def test_bogus_method(self):
|
||||
self._simulate_request('/get', method=self.getUniqueString())
|
||||
self.assertFalse(self.reqhandler_get.called)
|
||||
43
test/test_query_params.py
Normal file
43
test/test_query_params.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import testtools
|
||||
|
||||
import test.helpers as helpers
|
||||
|
||||
|
||||
class TestQueryParams(helpers.TestSuite):
|
||||
|
||||
def prepare(self):
|
||||
self.reqhandler = helpers.RequestHandler()
|
||||
self.api.add_route('/', self.reqhandler)
|
||||
|
||||
def test_none(self):
|
||||
query_string = ''
|
||||
self._simulate_request('/', query_string=query_string)
|
||||
|
||||
req = self.reqhandler.req
|
||||
self.assertEquals(req.try_get_param('marker'), None)
|
||||
self.assertEquals(req.try_get_param('limit'), None)
|
||||
|
||||
def test_simple(self):
|
||||
query_string = 'marker=deadbeef&limit=25'
|
||||
self._simulate_request('/', query_string=query_string)
|
||||
|
||||
req = self.reqhandler.req
|
||||
self.assertEquals(req.try_get_param('marker'), 'deadbeef')
|
||||
self.assertEquals(req.try_get_param('limit'), '25')
|
||||
|
||||
def test_list_type(self):
|
||||
query_string = 'colors=red,green,blue&limit=1'
|
||||
self._simulate_request('/', query_string=query_string)
|
||||
|
||||
req = self.reqhandler.req
|
||||
self.assertEquals(req.try_get_param('colors'), ['red', 'green', 'blue'])
|
||||
self.assertEquals(req.try_get_param('limit'), '1')
|
||||
|
||||
def test_bogus_input(self):
|
||||
query_string = 'colors=red,green,&limit=1&pickle'
|
||||
self._simulate_request('/', query_string=query_string)
|
||||
|
||||
req = self.reqhandler.req
|
||||
self.assertEquals(req.try_get_param('colors'), ['red', 'green', ''])
|
||||
self.assertEquals(req.try_get_param('limit'), '1')
|
||||
self.assertEquals(req.try_get_param('pickle'), None)
|
||||
46
test/test_req_vars.py
Normal file
46
test/test_req_vars.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from falcon.request import Request
|
||||
import test.helpers as helpers
|
||||
|
||||
|
||||
class TestReqVars(helpers.TestSuite):
|
||||
|
||||
def prepare(self):
|
||||
qs = '?marker=deadbeef&limit=10'
|
||||
headers = {
|
||||
'Content-Type': 'text/plain',
|
||||
'Content-Length': '4829'
|
||||
}
|
||||
|
||||
self.req = Request(helpers.create_environ(script='/test',
|
||||
path='/hello',
|
||||
query_string=qs,
|
||||
headers=headers))
|
||||
|
||||
def test_reconstruct_url(self):
|
||||
req = self.req
|
||||
|
||||
scheme = req.protocol
|
||||
host = req.try_get_header('host')
|
||||
app = req.app
|
||||
path = req.path
|
||||
query_string = req.query_string
|
||||
|
||||
expected_url = 'http://falconer/test/hello?marker=deadbeef&limit=10'
|
||||
actual_url = ''.join([scheme, '://', host, app, path, query_string])
|
||||
self.assertEquals(actual_url, expected_url)
|
||||
|
||||
def test_method(self):
|
||||
self.assertEquals(self.req.method, 'GET')
|
||||
|
||||
self.req = Request(helpers.create_environ(path='', method='HEAD'))
|
||||
self.assertEquals(self.req.method, 'HEAD')
|
||||
|
||||
def test_empty_path(self):
|
||||
self.req = Request(helpers.create_environ(path=''))
|
||||
self.assertEquals(self.req.path, '/')
|
||||
|
||||
def test_content_type(self):
|
||||
self.assertEquals(self.req.try_get_header('content-type'), 'text/plain')
|
||||
|
||||
def test_content_length(self):
|
||||
self.assertEquals(self.req.try_get_header('content-length'), '4829')
|
||||
58
test/test_req_vars.py.orig
Normal file
58
test/test_req_vars.py.orig
Normal file
@@ -0,0 +1,58 @@
|
||||
import testtools
|
||||
from testtools.matchers import Equals, MatchesRegex, Contains, Not
|
||||
|
||||
from falcon.request import Request
|
||||
import test.helpers as helpers
|
||||
|
||||
|
||||
class TestReqVars(helpers.TestSuite):
|
||||
|
||||
def prepare(self):
|
||||
qs = '?marker=deadbeef&limit=10'
|
||||
headers = {
|
||||
'Content-Type': 'text/plain',
|
||||
'Content-Length': '4829'
|
||||
}
|
||||
|
||||
self.req = Request(helpers.create_environ(script='/test',
|
||||
path='/hello',
|
||||
query_string=qs,
|
||||
headers=headers))
|
||||
|
||||
def test_reconstruct_url(self):
|
||||
req = self.req
|
||||
|
||||
scheme = req.protocol
|
||||
host = req.get_header('host')
|
||||
app = req.app
|
||||
path = req.path
|
||||
query_string = req.query_string
|
||||
|
||||
expected_url = 'http://falconer/test/hello?marker=deadbeef&limit=10'
|
||||
actual_url = ''.join([scheme, '://', host, app, path, query_string])
|
||||
self.assertThat(actual_url, Equals(expected_url))
|
||||
|
||||
def test_method(self):
|
||||
self.assertEquals(self.req.method, 'GET')
|
||||
|
||||
self.req = Request(helpers.create_environ(path='', method='HEAD'))
|
||||
self.assertEquals(self.req.method, 'HEAD')
|
||||
|
||||
def test_empty_path(self):
|
||||
self.req = Request(helpers.create_environ(path=''))
|
||||
self.assertEquals(self.req.path, '/')
|
||||
|
||||
def test_content_type(self):
|
||||
self.assertEquals(self.req.get_header('content-type'), 'text/plain')
|
||||
|
||||
def test_content_length(self):
|
||||
<<<<<<< Updated upstream
|
||||
self.assertThat(self.req.get_header('content-length'),
|
||||
Equals('4829'))
|
||||
|
||||
def test_http_request_method(self):
|
||||
self.assertThat(self.req.method, Equals('GET'))
|
||||
=======
|
||||
self.assertEquals(self.req.get_header('content-length'), '4829')
|
||||
|
||||
>>>>>>> Stashed changes
|
||||
48
test/test_request_body.py
Normal file
48
test/test_request_body.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import testtools
|
||||
|
||||
import helpers
|
||||
|
||||
|
||||
class TestRequestBody(helpers.TestSuite):
|
||||
|
||||
def prepare(self):
|
||||
self.reqhandler = helpers.RequestHandler()
|
||||
self.api.add_route('/', self.reqhandler)
|
||||
|
||||
def test_empty_body(self):
|
||||
self._simulate_request('/', body='')
|
||||
stream = self.reqhandler.req.body
|
||||
|
||||
stream.seek(0, 2)
|
||||
self.assertEquals(stream.tell(), 0)
|
||||
|
||||
def test_tiny_body(self):
|
||||
expected_body = '.'
|
||||
self._simulate_request('', body=expected_body)
|
||||
stream = self.reqhandler.req.body
|
||||
|
||||
actual_body = stream.read(1)
|
||||
self.assertEquals(actual_body, expected_body)
|
||||
|
||||
stream.seek(0, 2)
|
||||
self.assertEquals(stream.tell(), 1)
|
||||
|
||||
def test_read_body(self):
|
||||
expected_body = helpers.rand_string(2, 1 * 1024 * 1024)
|
||||
expected_len = len(expected_body)
|
||||
headers = {'Content-Length': str(expected_len)}
|
||||
|
||||
self._simulate_request('', body=expected_body, headers=headers)
|
||||
|
||||
content_len = self.reqhandler.req.try_get_header('content-length')
|
||||
self.assertEqual(content_len, str(expected_len))
|
||||
|
||||
stream = self.reqhandler.req.body
|
||||
|
||||
actual_body = stream.read()
|
||||
self.assertEquals(actual_body, expected_body)
|
||||
|
||||
stream.seek(0, 2)
|
||||
self.assertEquals(stream.tell(), expected_len)
|
||||
|
||||
self.assertEquals(stream.tell(), expected_len)
|
||||
61
test/test_uri_templates.py
Normal file
61
test/test_uri_templates.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import test.helpers as helpers
|
||||
|
||||
|
||||
class TestUriTemplates(helpers.TestSuite):
|
||||
|
||||
def prepare(self):
|
||||
self.reqhandler = helpers.RequestHandler()
|
||||
|
||||
def test_root_path(self):
|
||||
self.api.add_route('/', self.reqhandler)
|
||||
self._simulate_request('/')
|
||||
|
||||
self.assertTrue(self.reqhandler.called)
|
||||
req = self.reqhandler.req
|
||||
|
||||
self.assertEquals(req.try_get_param('id'), None)
|
||||
|
||||
def test_no_vars(self):
|
||||
self.api.add_route('/hello/world', self.reqhandler)
|
||||
self._simulate_request('/hello/world')
|
||||
|
||||
self.assertTrue(self.reqhandler.called)
|
||||
req = self.reqhandler.req
|
||||
|
||||
self.assertEquals(req.try_get_param('world'), None)
|
||||
|
||||
def test_single(self):
|
||||
self.api.add_route('/widgets/{id}', self.reqhandler)
|
||||
|
||||
self._simulate_request('/widgets/123')
|
||||
self.assertTrue(self.reqhandler.called)
|
||||
|
||||
req = self.reqhandler.req
|
||||
self.assertEquals(req.try_get_param('id'), '123')
|
||||
self.assertEquals(req.try_get_param('Id'), None)
|
||||
|
||||
def test_single_trailing_slash(self):
|
||||
self.api.add_route('/widgets/{id}/', self.reqhandler)
|
||||
|
||||
self._simulate_request('/widgets/123')
|
||||
self.assertFalse(self.reqhandler.called)
|
||||
|
||||
self._simulate_request('/widgets/123/')
|
||||
self.assertTrue(self.reqhandler.called)
|
||||
|
||||
req = self.reqhandler.req
|
||||
self.assertEquals(req.try_get_param('id'), '123')
|
||||
|
||||
def test_multiple(self):
|
||||
self.api.add_route('/messages/{Id}/names/{Name}', self.reqhandler)
|
||||
|
||||
test_id = self.getUniqueString()
|
||||
test_name = self.getUniqueString()
|
||||
path = '/messages/' + test_id + '/names/' + test_name
|
||||
self._simulate_request(path)
|
||||
self.assertTrue(self.reqhandler.called)
|
||||
|
||||
req = self.reqhandler.req
|
||||
self.assertEquals(req.try_get_param('Id'), test_id)
|
||||
self.assertEquals(req.try_get_param('Name'), test_name)
|
||||
self.assertEquals(req.try_get_param('name'), None)
|
||||
@@ -4,6 +4,7 @@ from testtools.matchers import Equals, MatchesRegex
|
||||
import falcon
|
||||
import test.helpers as helpers
|
||||
|
||||
|
||||
def _is_iterable(thing):
|
||||
try:
|
||||
for i in thing:
|
||||
@@ -13,6 +14,7 @@ def _is_iterable(thing):
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
class TestWsgi(testtools.TestCase):
|
||||
|
||||
def test_pep333(self):
|
||||
|
||||
Reference in New Issue
Block a user